import org.eclipse.smarthome.model.script.ScriptServiceUtil import java.util.List val int MAX_DAYS = 5 /* Return the set of all weather forecast readings for a specific reading (stored in forecastTypeStr). * This is possible since all weather forecast items obey the same naming convention: * Wx_OWM_Forecast__h * where hourInterval starts at 03 and goes up to 120. */ val getForecastItems = [ String forecastTypeStr | // "Temperature", "Humidity", "Pressure", "Wind_Speed", "Rain", "Snow", "Cloudiness" gWeatherForecast.allMembers.filter[ i | i.name.startsWith("Wx_OWM_Forecast_" + forecastTypeStr + "_") ] as Iterable ] /* Update the state of the daily Wx_OWM_Forecast_[_]_Day by setting the proper values from the provided list. * When the first forecast value (_03h) is no longer for the current day, we must skip the forecast for the current day (N=0). */ val setDailyForecastNumberItemValue = [ String forecastTypeStr, // "Temperature", "Humidity", "Pressure", "Wind_Speed", "Rain", "Snow", "Cloudiness" String metricName, // "Min", "Max", "Sum" Boolean forecast_offset, // True if no forecast info for today (offset 1 day) List valueList | val int MAX_DAYS = 5 // Lambda functions don't resolve global constants! val String itemNameBase = "Wx_OWM_Forecast_" + forecastTypeStr + ( if (metricName == "") {""} else {"_" + metricName} ) + "_Day" var int i = 0 var int index = 0 if (forecast_offset) index += 1 while ( (i < MAX_DAYS) && (index < MAX_DAYS) ) { val String itemName = itemNameBase + i.toString val Boolean postUndef = ( (forecast_offset == true) && (i == 1) ) val Number value = ( if ( postUndef ) { -1 } else { valueList.get(index) } ) val GenericItem forecastItem = ScriptServiceUtil.getItemRegistry.getItem(itemName) as GenericItem logDebug("setDailyForecastNumberItemValue", "Processing '" + forecastItem + "'") logDebug("setDailyForecastNumberItemValue", "Value of '" + itemName + "' is: " + forecastItem.state.toString + " - will be set to " + value.toString) if (postUndef) { postUpdate( forecastItem, UNDEF ) } else { postUpdate( forecastItem, value ) } i += 1 index += 1 } ] /* Same as setDailyForecastNumberItemValue, but generates Range strings 'min' - 'max' */ val setDailyForecastRangeValue = [ String forecastTypeStr, // "Temperature", "Humidity", "Pressure", "Wind_Speed", "Rain", "Snow", "Cloudiness" Boolean forecast_offset, // True if no forecast info for today (offset 1 day) List valueListMin, // List of daily low values List valueListMax | // List of daily high values val int MAX_DAYS = 5 // Lambda functions don't resolve global constants! val String itemNameBase = "Wx_OWM_Forecast_" + forecastTypeStr + "_Range_Day" var int i = 0 var int index = 0 if (forecast_offset) index += 1 while ( (i < MAX_DAYS) && (index < MAX_DAYS) ) { val String itemName = itemNameBase + i.toString val Boolean postUndef = ( (forecast_offset == true) && (i == 1) ) val String valueMin = ( if ( postUndef ) { "N/A" } else { valueListMin.get(index).toString() } ) val String valueMax = ( if ( postUndef ) { "N/A" } else { valueListMax.get(index).toString() } ) val String valueRange = valueMin + " - " + valueMax val GenericItem forecastRangeItem = ScriptServiceUtil.getItemRegistry.getItem(itemName) as GenericItem logInfo("setDailyForecastRangeValue", "Processing '" + forecastRangeItem.name + "'") logInfo("setDailyForecastRangeValue", "Value of '" + itemName + "' is: " + forecastRangeItem.state.toString + " - will be set to " + valueRange) if (postUndef) { postUpdate( forecastRangeItem, UNDEF ) } else { postUpdate( forecastRangeItem, valueRange ) } i += 1 index += 1 } ] /* Return the subset of weather forecast items that apply for the day specified (from midnight to midnight), * where day 0 is the current day. With a free OWM account, this results in at most 8 items per day (24h / 3h forecasts = 8 daily forecasts) */ val filterWxItemsForDay = [ Iterable items, // The weather forecast items for a given forecast type (all time values) Number forecast_03h_hour, // The hour-in-day offset for the first forecast values Number theDay | // The forecast day to filter the forecast values from for subsequent per-day processing val Number fromHour = 0 + 24 * theDay val Number toHour = 24 + 24 * theDay logInfo("Lambda:filterWxItemsForDay", "About to start processing the item filtering process (" + items.length + " item(s))") items.filter [ i | val String ext = i.name.substring(i.name.length() - 4).substring(0,3) var Number hr = Integer::parseInt( ext.substring(1,3) ) if (! ext.startsWith("_") ) { hr = hr + ( 100 * Integer::parseInt( ext.substring(0,1) ) ) } logDebug("Lambda:filterWxItemsForDay", i.name + ": ext '" + ext + "' - hr = " + hr.toString + " - day " + theDay.toString ) val Number hourInDay = hr + forecast_03h_hour - 3 if ( (hourInDay >= fromHour) && (hourInDay <= toHour) ) { logInfo("Lambda:filterWxItemsForDay", i.name + ": forecast hour = " + hr.toString + ", maps to day " + theDay.toString + " @ " + (hourInDay - 24 * theDay).toString + " h - value: " + i.state.toString) } // Filter per-item result (Boolean): (hourInDay >= fromHour) && (hourInDay <= toHour) ] // Lambda return type is: Iterable ] /* Return the maximum from a list of Number values (preserving the Units of Measurement) */ val maxStateValueFrom = [ Iterable items | items.map[ state as Number ].reduce[ max, v | if (v > max) v else max ] // Lambda return type is: Number ] /* Return the minimum from a list of Number values (preserving the Units of Measurement) */ val minStateValueFrom = [ Iterable items | items.map[ state as Number ].reduce[ min, v | if (v < min) v else min ] // Lambda return type is: Number ] /* Return the sum from a list of Number values (preserving the Units of Measurement) */ val sumStateValueFrom = [ Iterable items | items.map[ (state as QuantityType).doubleValue ].reduce[ sum, v | sum + v ] ] /* Set Wx_OWM_Current_NightState at startup */ rule "OpenHAB system started - astro" when System started then createTimer(now.plusSeconds(180)) [ | if (now.isAfter((Astro_Sunset_End.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli) || now.isBefore((Astro_Sunrise_Start.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli) ) { postUpdate(Wx_OWM_Current_NightState, ON) } else { postUpdate(Wx_OWM_Current_NightState, OFF) } ] end /* Update Wx_OWM_Current_NightState based on the elevation of the sun (night: elevation < 0). * Also set the weather condition mapping from day to night based on Wx_OWM_Current_NightState */ rule "Update Night State" when Item Astro_Sun_Elevation changed then // UoM | (e.g. 20|°C) if (Astro_Sun_Elevation.state > 0|°){ if (Wx_OWM_Current_NightState.state != OFF) { postUpdate(Wx_OWM_Current_NightState, OFF) // if Wx_OWM_Current_NightState was ON we need to update Wx_OWM_Current_Condition_Formatted too Wx_OWM_Current_Condition_Formatted.postUpdate(transform("MAP", "openweathermap_day.map", Wx_OWM_Current_Condition_Id.state.toString())) } } else { if (Wx_OWM_Current_NightState.state != ON) { postUpdate(Wx_OWM_Current_NightState, ON) Wx_OWM_Current_Condition_Formatted.postUpdate(transform("MAP", "openweathermap_night.map", Wx_OWM_Current_Condition_Id.state.toString())) } } end /* Update the formatted current condition ID (for use in sitemaps or HabPanel UI), ensuring the use of the proper day/night transform */ rule "Update conditions for HABPanel icon selection" when Item Wx_OWM_Current_ConditionId changed then // val url = "http://192.168.0.10:8080/icon/climacon?iconset=climacons&format=svg&state=" + Wx_OWM_Current_Condition_Formatted.state.toString() if (Wx_OWM_Current_NightState.state == ON) { Wx_OWM_Current_Condition_Formatted.postUpdate(transform("MAP", "openweathermap_night.map", Wx_OWM_Current_Condition_Id.state.toString())) // Wx_OWM_Current_Condition_Img_URL.postUpdate(url) } if (Wx_OWM_Current_NightState.state == OFF) { Wx_OWM_Current_Condition_Formatted.postUpdate(transform("MAP", "openweathermap_day.map", Wx_OWM_Current_Condition_Id.state.toString())) // Wx_OWM_Current_Condition_Img_URL.postUpdate(url) } end /* Provide the compass wind direction in a proxy Item (Wx_OWM_Current_Wind_Direction_Simplified) for displaying on the sitemap. * This is needed as the SCALE transform only accepts numeric values, whereas the OWM binding sometimes reports non-numeric values * in cases where the wind direction is undefined or hard to define. */ rule "Update simplified wind direction" when Item Wx_OWM_Current_Wind_Direction changed then val String ruleTitle = "UpdateOWMCurrentWindDirection" // Update Wx_OWM_Current_Wind_Direction_Simplified if Wx_OWM_Wind_Speed is numeric var String status = "" var boolean error = true if (Wx_OWM_Current_Wind_Direction.state == NULL) { status = "(not set)" } else if (Wx_OWM_Current_Wind_Direction.state == UNDEF) { status = "(undefined)" } else if (Wx_OWM_Current_Wind_Direction.state instanceof Number) { status = transform("SCALE", "wind.scale", Wx_OWM_Current_Wind_Direction.state.toString()) error = false } else { // Unexpected state type status = "(invalid: " + Wx_OWM_Current_Wind_Direction.state.toString() + ")" } if (error) { logWarn(ruleTitle, "{} has state '{}', expecting Number", Wx_OWM_Current_Wind_Direction.name, Wx_OWM_Current_Wind_Direction.state.toString()) } else { logDebug(ruleTitle, "{} has Number state '{}'", Wx_OWM_Current_Wind_Direction.name, Wx_OWM_Current_Wind_Direction.state.toString()) } postUpdate(Wx_OWM_Current_Wind_Direction_Simplified, status) end /* Compute daiy forecast values from the hourly forecast values provided by the OpenWeatherMap binding. * */ rule "Group-based weather forecast processing" when Item DBG_Test_Weather_Forecast changed or // Debugging Time cron "0 0 0 * * ?" or Item Wx_OWM_Forecast_Time_03h changed then val String ruleTitle = "UpdateOWMForecastInfo_Groups" // Compute the rule execution time, making sure we use the same 'now' value for computing month, day and hour val DateTime dtNow = now val Number now_d = dtNow.getDayOfMonth val Number now_m = dtNow.getMonthOfYear val Number now_h = dtNow.getHourOfDay // Compute the month, day and hour of the first forecast time (3 hour offset) val DateTime dtForecast_03h = (new DateTime(Wx_OWM_Forecast_Time_03h.state.toString)).toDateTime val Number forecast_03h_d = dtForecast_03h.getDayOfMonth val Number forecast_03h_m = dtForecast_03h.getMonthOfYear val Number forecast_03h_h = dtForecast_03h.getHourOfDay // If the first forecast value doesn't apply for the current day (e.g., at 22:00h the 1st forecast is for the next day at 22:00 +3h = 01:00), // then set 'forecastDayOffset' to 'true' for further processing. In this edge case, we won't display today's forecast as it is nonexistent. val Boolean forecastDayOffset = ( if (forecast_03h_d > now_d) true else false ) // Store the forecastDayOffset value in the proxy item Wx_OWM_Forecast_Day_Offset so it is available in sitemaps and UIs: if (forecastDayOffset) { // No forecast for today logInfo(ruleTitle, "No forecast for today ({}/{} - at or after {}h", now_d, now_m, now_h) if (Wx_OWM_Forecast_Day_Offset.state !== ON) { postUpdate(Wx_OWM_Forecast_Day_Offset, ON) } } else { if (Wx_OWM_Forecast_Day_Offset.state !== OFF) { postUpdate(Wx_OWM_Forecast_Day_Offset, OFF) } } logInfo(ruleTitle, "Update Wx forecast info - today is {}/{} @ {}h - 03h forecast is for {}/{} at {}h", now_d, now_m, now_h, forecast_03h_d, forecast_03h_m, forecast_03h_h) // Initialize the lists that will contain the daily forecast items we want to compute. // With a free OWM account, you can get 5 day forecast values per 3 hours, which covers 120 hours. // Only if the first forecast timestamp falls within the first 3 hours of a day (the day after the current day), // we can compute the daily forecast for 5 days in a row. In other cases, we will compute the daily forecast for today + 4 days. // Today's forecast is computed by considering the current values plus the set of forecast values that fit in the current day. // The daily forecast values will be stored in array lists, hence we initialize them: // // - val Iterable forecast[Quantity] contains the list of [Quantity] forecast Items, filtered from // the 'gWeatherForecast' Item group by means of the lambda 'getForecastItems' // - val List> [quantity] will contain the forecast[Quantity] list, but ordered in sets of forecast values // for a given forecast day (0 = today, 1 = tomorrow, etc), with the lambda 'filterWxItemsForDay' // - val List [quantity]_min will contain the minimum value of [quantity] for each forecast day (Temperature, Pressure...) // - val List [quantity]_max will contain the maximum value of [quantity] for each forecast day (Temperature, Wind Speed...) // - val List [quantity]_sum will contain the daily sum of [quantity] for each forecast day (Rain, Snow) // // The min, max and sum are also comuted with lambda functions. // Temperature: compute daily min, max val Iterable forecastTemperature = getForecastItems.apply("Temperature") val List> temperature = newArrayList( null,null,null,null,null ) val List temperature_min = newArrayList( 0,0,0,0,0 ) val List temperature_max = newArrayList( 0,0,0,0,0 ) // Pressure: compute daily min, max val Iterable forecastPressure = getForecastItems.apply("Pressure") val List> pressure = newArrayList( null,null,null,null,null ) val List pressure_min = newArrayList( 0,0,0,0,0 ) val List pressure_max = newArrayList( 0,0,0,0,0 ) // Humidity: compute daily min, max val Iterable forecastHumidity = getForecastItems.apply("Humidity") val List> humidity = newArrayList( null,null,null,null,null ) val List humidity_min = newArrayList( 0,0,0,0,0 ) val List humidity_max = newArrayList( 0,0,0,0,0 ) // Cloudiness: compute daily min, max val Iterable forecastCloudiness = getForecastItems.apply("Cloudiness") val List> cloudiness = newArrayList( null,null,null,null,null ) val List cloudiness_min = newArrayList( 0,0,0,0,0 ) val List cloudiness_max = newArrayList( 0,0,0,0,0 ) // Rain: compute daily sum val Iterable forecastRain = getForecastItems.apply("Rain") val List> rain = newArrayList( null,null,null,null,null ) val List rain_sum = newArrayList( 0,0,0,0,0 ) // Snow: compute daily sum val Iterable forecastSnow = getForecastItems.apply("Snow") val List> snow = newArrayList( null,null,null,null,null ) val List snow_sum = newArrayList( 0,0,0,0,0 ) // Wind Speed: compute daily min, max val Iterable forecastWind_Speed = getForecastItems.apply("Wind_Speed") val List> wind_speed = newArrayList( null,null,null,null,null ) val List wind_speed_min = newArrayList( 0,0,0,0,0 ) val List wind_speed_max = newArrayList( 0,0,0,0,0 ) val forecast_offset = forecastDayOffset logInfo(ruleTitle, "INFO - forecastDayOffset is {}", forecastDayOffset) // Process the forecast values, a calendar day (from midnight to midnight) at a time var int i = 0 while (i < MAX_DAYS) { logInfo(ruleTitle, "INFO - iteration {}", i) // First create the daily set of forecast values for day i temperature.set(i, filterWxItemsForDay.apply(forecastTemperature, forecast_03h_h, i)) // Calculate the min / max / sum for day i: temperature_min.set(i, minStateValueFrom.apply(temperature.get(i))) temperature_max.set(i, maxStateValueFrom.apply(temperature.get(i))) logInfo(ruleTitle,"RESULT: {} FORECAST FOR DAY {} : {} -- {}", "Temperature", i, temperature_min.get(i), temperature_max.get(i) ) pressure.set(i, filterWxItemsForDay.apply(forecastPressure, forecast_03h_h, i)) pressure_min.set(i, minStateValueFrom.apply(pressure.get(i))) pressure_max.set(i, maxStateValueFrom.apply(pressure.get(i))) logInfo(ruleTitle,"RESULT: {} FORECAST FOR DAY {} : {} -- {}", "Pressure", i, pressure_min.get(i), pressure_max.get(i) ) humidity.set(i, filterWxItemsForDay.apply(forecastHumidity, forecast_03h_h, i)) humidity_min.set(i, minStateValueFrom.apply(humidity.get(i))) humidity_max.set(i, maxStateValueFrom.apply(humidity.get(i))) logInfo(ruleTitle,"RESULT: {} FORECAST FOR DAY {} : {} -- {}", "Humidity", i, humidity_min.get(i), humidity_max.get(i) ) cloudiness.set(i, filterWxItemsForDay.apply(forecastCloudiness, forecast_03h_h, i)) cloudiness_min.set(i, minStateValueFrom.apply(cloudiness.get(i))) cloudiness_max.set(i, maxStateValueFrom.apply(cloudiness.get(i))) logInfo(ruleTitle,"RESULT: {} FORECAST FOR DAY {} : {} -- {}", "Cloudiness", i, cloudiness_min.get(i), cloudiness_max.get(i) ) rain.set(i, filterWxItemsForDay.apply(forecastRain, forecast_03h_h, i)) rain_sum.set(i, sumStateValueFrom.apply(rain.get(i))) logInfo(ruleTitle,"RESULT: {} FORECAST FOR DAY {} : SUM {}", "Rain", i, rain_sum.get(i) ) snow.set(i, filterWxItemsForDay.apply(forecastSnow, forecast_03h_h, i)) snow_sum.set(i, sumStateValueFrom.apply(snow.get(i))) logInfo(ruleTitle,"RESULT: {} FORECAST FOR DAY {} : SUM {}", "Snow", i, snow_sum.get(i) ) wind_speed.set(i, filterWxItemsForDay.apply(forecastWind_Speed, forecast_03h_h, i)) wind_speed_min.set(i, minStateValueFrom.apply(wind_speed.get(i))) wind_speed_max.set(i, maxStateValueFrom.apply(wind_speed.get(i))) logInfo(ruleTitle,"RESULT: {} FORECAST FOR DAY {} : {} -- {}", "Wind_Speed", i, wind_speed_min.get(i), wind_speed_max.get(i) ) // Advance one day i += 1 } logInfo(ruleTitle, "INFO - Updating the item values") // Now the daily forecast values can be propagated to the proxy Items, using the lambda 'setDailyForecastNumberItemValue'. // This lambda will take care of the forecast offset in case it is needed setDailyForecastNumberItemValue.apply("Temperature", "Min", forecast_offset, temperature_min) setDailyForecastNumberItemValue.apply("Temperature", "Max", forecast_offset, temperature_max) setDailyForecastNumberItemValue.apply("Pressure", "Min", forecast_offset, pressure_min) setDailyForecastNumberItemValue.apply("Pressure", "Max", forecast_offset, pressure_max) setDailyForecastNumberItemValue.apply("Humidity", "Min", forecast_offset, humidity_min) setDailyForecastNumberItemValue.apply("Humidity", "Max", forecast_offset, humidity_max) setDailyForecastNumberItemValue.apply("Cloudiness", "Min", forecast_offset, cloudiness_min) setDailyForecastNumberItemValue.apply("Cloudiness", "Max", forecast_offset, cloudiness_max) setDailyForecastNumberItemValue.apply("Rain", "", forecast_offset, rain_sum) setDailyForecastNumberItemValue.apply("Snow", "", forecast_offset, snow_sum) setDailyForecastNumberItemValue.apply("Wind_Speed", "Min", forecast_offset, wind_speed_min) setDailyForecastNumberItemValue.apply("Wind_Speed", "Max", forecast_offset, wind_speed_max) setDailyForecastRangeValue.apply("Temperature", forecast_offset, temperature_min, temperature_max) setDailyForecastRangeValue.apply("Pressure", forecast_offset, pressure_min, pressure_max) setDailyForecastRangeValue.apply("Humidity", forecast_offset, humidity_min, humidity_max) setDailyForecastRangeValue.apply("Cloudiness", forecast_offset, cloudiness_min, cloudiness_max) setDailyForecastRangeValue.apply("Wind_Speed", forecast_offset, wind_speed_min, wind_speed_max) end