OpenHAB Community Tutorial: Multiple Location Weather Warnings with DWD Integration

Introduction
This tutorial shows how to create an OpenHAB rule that monitors German Weather Service (DWD) weather warnings for multiple locations simultaneously. The rule regularly checks if there are current weather warnings for defined coordinates and logs them in detail.

Prerequisites
JS Scripting Add-on installed
Internet access for DWD API

Rule in Detail
Basic Configuration
The rule uses an array of locations with names and coordinates:

var locations = [
    { name: "Location 1", lat: 48.8302, lon: 9.1212 },
    { name: "Location 2", lat: 48.7758, lon: 9.1829 },
    { name: "Location 3", lat: 47.4911, lon: 11.0958 }
];

Rule Definition

rules.JSRule({
    name: "Log Multiple Locations Weather Warnings",
    description: "Check and log weather warnings for multiple locations",
    triggers: [
        triggers.GenericCronTrigger("0 */1 * * * ?"),  // Runs every minute
        triggers.SystemStartlevelTrigger(100)           // Runs on system start
    ],
    // ... additional configuration
});

Core Functionality
HTTP Request for DWD Data:

const url = "https://maps.dwd.de/geoserver/dwd/ows?service=WFS&version=2.0.0&request=GetFeature&typeName=dwd%3AWarnungen_Gemeinden_vereinigt&outputFormat=application%2Fjson";
var response = actions.HTTP.sendHttpGetRequest(url);

Point-in-Polygon Check:
The isPointInPolygon() function checks if a location is within a warning area:

function isPointInPolygon(x, y, polygon) {
    var inside = false;
    for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
        var xi = polygon[i][0], yi = polygon[i][1];
        var xj = polygon[j][0], yj = polygon[j][1];
        
        var intersect = ((yi > y) !== (yj > y)) 
            && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
        
        if (intersect) inside = !inside;
    }
    return inside;
}

Implementation Steps
Step 1: Configure Locations
Adapt the locations array to your needs:

var locations = [
    { name: "Home", lat: YOUR_LATITUDE, lon: YOUR_LONGITUDE },
    { name: "Work", lat: YOUR_LATITUDE, lon: YOUR_LONGITUDE }
];

Step 2: Create the Rule
Add the complete code to your JS-Scripting rule. The rule will automatically:

  • Run on system startup

  • Execute again every minute

  • Check all defined locations

Step 3: Dynamic Location Management
You can add locations at runtime:

// Add new locations
addLocation("Vacation Spot", 47.604, 9.84);

Log Output
The rule generates detailed logs:

  • Info: Number of checked locations and features

  • Warnings: Found weather warnings with details

  • Errors: Query problems

  • Example output:

⚠️ Found 2 warning(s) for Location 1
πŸ“ Location 1 - Warning 1:
   Severity: Moderate
   Event: STURMBΓ–EN
   Description: Storm gusts with speeds around 70 km/h
   Urgency: Immediate
   Certainty: Likely

Customization Options

  1. Adjust Check Interval
    Change the cron trigger for different intervals:
"0 */5 * * * ?" - Every 5 minutes

"0 0 */1 * * ?" - Hourly
  1. Additional Warning Information
    Extend the warning object with additional properties:
var warning = {
    name: location.name,
    severity: properties.SEVERITY,
    // ... existing properties
    area: properties.AREA, // New property
    sender: properties.SENDER
};
  1. Update OpenHAB Items
    Add item updates to the rule:
// Example for item update
events.sendCommand("WeatherWarning_Active", locationWarnings.length > 0 ? "ON" : "OFF");

Error Handling
The rule includes comprehensive error handling:

  • Timeouts for HTTP queries

  • JSON parsing errors

  • Missing geometry data

  • Coordinate checking errors

  • Benefits of This Solution

  • Multiple Locations: Monitor several positions simultaneously

  • Real-time Checking: Minute-precise updates

  • Robust Geometry Check: Precise polygon verification

  • Detailed Logging: Comprehensive warning information

  • Easy Extensibility: Dynamic addition of more locations

  • Typical Use Cases

  • Monitoring home and work locations

  • Tracking family members at different locations

  • Commercial applications with multiple sites

  • Vacation home or weekend house monitoring

  • Troubleshooting

  • Problem: No warnings found

  • Solution: Check coordinates and ensure they are in Germany

Problem: HTTP errors
Solution: Check internet connection and DWD API accessibility

Problem: Rule doesn’t run
Solution: Verify JS-Scripting installation and cron syntax

This solution provides a robust foundation for monitoring weather warnings at multiple locations and can be easily adapted to individual needs.

Complete Code

// Array of coordinates to check
var locations = [
    { name: "Location 1", lat: 48.8302, lon: 9.1212 },
    { name: "Location 2", lat: 48.7758, lon: 9.1829 },
    { name: "Location 3", lat: 47.4911, lon: 11.0958 },
    { name: "Test Location", lat: 47.604, lon: 9.84 }
];

rules.JSRule({
    name: "Log Multiple Locations Weather Warnings",
    description: "Check and log weather warnings for multiple locations",
    triggers: [
        triggers.GenericCronTrigger("0 */1 * * * ?"),
        triggers.SystemStartlevelTrigger(100)
    ],
    execute: (data) => {
        logger.info("πŸ” Fetching DWD weather warnings for " + locations.length + " locations...");
        
        const url = "https://maps.dwd.de/geoserver/dwd/ows?service=WFS&version=2.0.0&request=GetFeature&typeName=dwd%3AWarnungen_Gemeinden_vereinigt&outputFormat=application%2Fjson";
        
        try {
            // Use simple HTTP request without options
            var response = actions.HTTP.sendHttpGetRequest(url);
            
            if (response === null) {
                logger.error("❌ Failed to fetch warnings: Response is null");
                return;
            }
            
            var data = JSON.parse(response);
            
            if (!data || !data.features) {
                logger.warn("No features found in DWD response");
                return;
            }
            
            logger.info("Processing " + data.features.length + " warning features for " + locations.length + " locations");
            
            // Check each location
            for (var locIndex = 0; locIndex < locations.length; locIndex++) {
                var location = locations[locIndex];
                var locationWarnings = [];
                
                // Check each feature for this location's coordinates
                for (var i = 0; i < data.features.length; i++) {
                    var feature = data.features[i];
                    try {
                        if (feature.geometry && feature.geometry.type === "MultiPolygon") {
                            var isInside = false;
                            var multiPolygon = feature.geometry.coordinates;
                            
                            // Check each polygon in the MultiPolygon
                            for (var p = 0; p < multiPolygon.length; p++) {
                                var polygon = multiPolygon[p];
                                var exteriorRing = polygon[0]; // First ring is exterior
                                
                                if (isPointInPolygon(location.lon, location.lat, exteriorRing)) {
                                    isInside = true;
                                    break;
                                }
                            }
                            
                            if (isInside) {
                                var properties = feature.properties || {};
                                
                                // Use UPPERCASE property names from DWD response
                                var warning = {
                                    name: location.name,
                                    severity: properties.SEVERITY || 'Unknown',
                                    description: properties.HEADLINE || properties.DESCRIPTION || 'No description',
                                    urgency: properties.URGENCY || 'Unknown',
                                    certainty: properties.CERTAINTY || 'Unknown',
                                    event: properties.EVENT || 'Unknown event',
                                    effective: properties.EFFECTIVE,
                                    expires: properties.EXPIRES,
                                    instruction: properties.INSTRUCTION || '',
                                    onset: properties.ONSET
                                };
                                
                                locationWarnings.push(warning);
                                logger.info("βœ… Found matching warning for " + location.name + ": " + warning.severity + " - " + warning.event);
                            }
                        }
                    } catch (error) {
                        logger.warn("Error processing feature " + i + " for " + location.name + ": " + error.message);
                    }
                }
                
                // Log results for this location
                if (locationWarnings.length > 0) {
                    logger.warn("⚠️ Found " + locationWarnings.length + " warning(s) for " + location.name);
                    
                    for (var w = 0; w < locationWarnings.length; w++) {
                        var warn = locationWarnings[w];
                        logger.warn("πŸ“ " + warn.name + " - Warning " + (w + 1) + ":");
                        logger.warn("   Severity: " + warn.severity);
                        logger.warn("   Event: " + warn.event);
                        logger.warn("   Description: " + warn.description);
                        logger.warn("   Urgency: " + warn.urgency);
                        logger.warn("   Certainty: " + warn.certainty);
                        if (warn.effective) logger.warn("   Effective: " + warn.effective);
                        if (warn.onset) logger.warn("   Onset: " + warn.onset);
                        if (warn.expires) logger.warn("   Expires: " + warn.expires);
                        if (warn.instruction) logger.warn("   Instruction: " + warn.instruction);
                        logger.warn("   ―――――――――――――――――――――――――――");
                    }
                } else {
                    logger.info("❌ No warnings found for " + location.name + " at coordinates " + location.lat + ", " + location.lon);
                }
            }
            
        } catch (error) {
            logger.error("🚨 Error checking weather warnings: " + error.message);
        }
    },
    tags: ["Weather", "Warnings", "Multiple"],
    id: "log-multiple-locations-weather-warnings"
});

// Correct point-in-polygon function
function isPointInPolygon(x, y, polygon) {
    var inside = false;
    
    for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
        var xi = polygon[i][0], yi = polygon[i][1];
        var xj = polygon[j][0], yj = polygon[j][1];
        
        var intersect = ((yi > y) !== (yj > y)) 
            && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
        
        if (intersect) inside = !inside;
    }
    
    return inside;
}

// Function to add new locations dynamically
function addLocation(name, lat, lon) {
    locations.push({ name: name, lat: lat, lon: lon });
    logger.info("πŸ“ Added new location: " + name + " (" + lat + ", " + lon + ")");
}

logger.info("πŸ“ Multiple locations weather warning rule loaded - monitoring " + locations.length + " locations");

This would make a good candidate for a rule template. The advantage to end users is they can install and use it without needing to edit code. And they can set the locations (in this case) using a map picker.

Apologies but this is to much of my programming skills :confused:

You’ve pretty much already done the hard part. Converting it to a tempalte means just changing the format a bit.

If you are interested I can help with the template part. It really just boils down to converting it to a managed rule (i.e. ya, adding the properties, and making a post in the marketplace.

It would look something like this (I’m just typing this stuff in, there may be errors, I’m just posting ti for demonstration).

uid: log-multiple-locations-weather-warnings
label: Log Multiple Locations Weather Warnings
description: An OpenHAB rule that monitors German Weather Service (DWD) weather warnings for multiple locations simultaneously. The rule regularly checks if there are current weather warnings for defined coordinates and logs them in detail.
configDescriptions:
  - description: Location 1 name
    label: Location 1 name
    name: location1name
    required: true
    type: TEXT
  - description: Location 1 lat/lon coordinates
    label: Location 1
    name: location1
    required: true
    type: TEXT
    context: location
  - description: Location 2 name
    label: Location 2 name
    name: location2name
    required: false
    type: TEXT
    defaultValue: ''
  - description: Location 2 lat/lon coordinates
    label: Location 2
    name: location2
    required: false
    type: TEXT
    context: location
    defauultValue: 0,0
  - description: Location 3 name
    label: Location 3 name
    name: locatio32name
    required: false
    type: TEXT
    defaultValue: ''
  - description: Location 3 lat/lon coordinates
    label: Location 3
    name: location3
    required: false
    type: TEXT
    context: location
    defauultValue: 0,0
  - description: Location 4 name
    label: Location 4 name
    name: location4name
    required: false
    type: TEXT
    defaultValue: ''
  - description: Location 4 lat/lon coordinates
    label: Location 4
    name: location4
    required: false
    type: TEXT
    context: location
    defauultValue: 0,0
triggers:
triggers:
  - id: "1"
    configuration:
      cronExpression: 0 */1 * * * ?
    type: timer.GenericCronTrigger
  - id: "2"
    configuration:
      startlevel: 70
    type: core.SystemStartlevelTrigger
conditions: []
actions:
  - inputs: {}
    id: "1"
    configuration:
      type: application/javascript
      script: >
         var locations = [
            { name: "{{location1name}}", lat: parseFloat("{{location1}}".split(',')[0]), lon: parseFloat("{{location1}}".split(',')[1]) },
            { name: "{{location2name}}", lat: parseFloat("{{location2}}".split(',')[0]), lon: parseFloat("{{location2}}".split(',')[1]) },
            { name: "{{location3name}}", lat: parseFloat("{{location3}}".split(',')[0]), lon: parseFloat("{{location3}}".split(',')[1]) }
            { name: "{{location3name}}", lat: parseFloat("{{location4}}".split(',')[0]), lon: parseFloat("{{location4}}".split(',')[1]) }
        ];

        // Filter out the locations which are not used
        locations = locations.filter(l => l.name != '');

        logger.info("πŸ” Fetching DWD weather warnings for " + locations.length + " locations...");
        
        const url = "https://maps.dwd.de/geoserver/dwd/ows?service=WFS&version=2.0.0&request=GetFeature&typeName=dwd%3AWarnungen_Gemeinden_vereinigt&outputFormat=application%2Fjson";
        
        try {
            // Use simple HTTP request without options
            var response = actions.HTTP.sendHttpGetRequest(url);
            
            if (response === null) {
                logger.error("❌ Failed to fetch warnings: Response is null");
                return;
            }
            
            var data = JSON.parse(response);
            
            if (!data || !data.features) {
                logger.warn("No features found in DWD response");
                return;
            }
            
            logger.info("Processing " + data.features.length + " warning features for " + locations.length + " locations");
            
            // Check each location
            for (var locIndex = 0; locIndex < locations.length; locIndex++) {
                var location = locations[locIndex];
                var locationWarnings = [];
                
                // Check each feature for this location's coordinates
                for (var i = 0; i < data.features.length; i++) {
                    var feature = data.features[i];
                    try {
                        if (feature.geometry && feature.geometry.type === "MultiPolygon") {
                            var isInside = false;
                            var multiPolygon = feature.geometry.coordinates;
                            
                            // Check each polygon in the MultiPolygon
                            for (var p = 0; p < multiPolygon.length; p++) {
                                var polygon = multiPolygon[p];
                                var exteriorRing = polygon[0]; // First ring is exterior
                                
                                if (isPointInPolygon(location.lon, location.lat, exteriorRing)) {
                                    isInside = true;
                                    break;
                                }
                            }
                            
                            if (isInside) {
                                var properties = feature.properties || {};
                                
                                // Use UPPERCASE property names from DWD response
                                var warning = {
                                    name: location.name,
                                    severity: properties.SEVERITY || 'Unknown',
                                    description: properties.HEADLINE || properties.DESCRIPTION || 'No description',
                                    urgency: properties.URGENCY || 'Unknown',
                                    certainty: properties.CERTAINTY || 'Unknown',
                                    event: properties.EVENT || 'Unknown event',
                                    effective: properties.EFFECTIVE,
                                    expires: properties.EXPIRES,
                                    instruction: properties.INSTRUCTION || '',
                                    onset: properties.ONSET
                                };
                                
                                locationWarnings.push(warning);
                                logger.info("βœ… Found matching warning for " + location.name + ": " + warning.severity + " - " + warning.event);
                            }
                        }
                    } catch (error) {
                        logger.warn("Error processing feature " + i + " for " + location.name + ": " + error.message);
                    }
                }
                
                // Log results for this location
                if (locationWarnings.length > 0) {
                    logger.warn("⚠️ Found " + locationWarnings.length + " warning(s) for " + location.name);
                    
                    for (var w = 0; w < locationWarnings.length; w++) {
                        var warn = locationWarnings[w];
                        logger.warn("πŸ“ " + warn.name + " - Warning " + (w + 1) + ":");
                        logger.warn("   Severity: " + warn.severity);
                        logger.warn("   Event: " + warn.event);
                        logger.warn("   Description: " + warn.description);
                        logger.warn("   Urgency: " + warn.urgency);
                        logger.warn("   Certainty: " + warn.certainty);
                        if (warn.effective) logger.warn("   Effective: " + warn.effective);
                        if (warn.onset) logger.warn("   Onset: " + warn.onset);
                        if (warn.expires) logger.warn("   Expires: " + warn.expires);
                        if (warn.instruction) logger.warn("   Instruction: " + warn.instruction);
                        logger.warn("   ―――――――――――――――――――――――――――");
                    }
                } else {
                    logger.info("❌ No warnings found for " + location.name + " at coordinates " + location.lat + ", " + location.lon);
                }
            }
            
        } catch (error) {
            logger.error("🚨 Error checking weather warnings: " + error.message);
        }

        // Correct point-in-polygon function
        function isPointInPolygon(x, y, polygon) {
            var inside = false;
    
            for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
                var xi = polygon[i][0], yi = polygon[i][1];
                var xj = polygon[j][0], yj = polygon[j][1];
        
                var intersect = ((yi > y) !== (yj > y)) 
                    && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
        
                if (intersect) inside = !inside;
            }
    
            return inside;
        }
    type: script.ScriptAction

I only made very tiny differences between your original and the template.

The first section defines properties for four locations, a name and a lat/lon. The β€œcontext: location” will bring up a map to choose the coordinate.

The properties are applied to the rule using a simple string find and replace. So where you see {{location1}}, when the rule is created that will be replaced with what ever the user selected for that property.

In order to minimize the changes to your code I manipulate the lat,lon returned by the map picker to match the array as you’ve originally defined it. And then I remove any location that the user didn’t select. I figure your locations is reasonable, but it’s not hard to add more. Though now that the users can add locations through the properties the addLocation function is no longer needed.

But that’s it. Everything else is just reformatting the rule to be a managed rule instead of a file based rule.

The template can be tested by saving the above into a .yaml file placed in $OH_CONF/templates (IIRC). Then you can create a new rule and the template should be available to base the new rule on. Enter the properties and it’s ready to run.

Then you can create a marketplace entry following the template and adding the YAML and users will be able to find and install this rule as if it were an add-on under Add-on store β†’ Automation β†’ Rule Templates.

This topic was automatically closed 41 days after the last reply. New replies are no longer allowed.