OpenWeatherMap

Purpose

This rule will create and link the ~700 Items and groups needed to get daily forecasts from the OWM binding using a free API key. It will also install the Scale transformation service, if it is not already installed. There is also a function that was used for testing purposes, which can be used to remove all the Items and groups. As the day progresses, there will be fewer hourly forecasts included in the gForecast_1 group.

You may want to comment out several groups and Items that you do not plan on using, but the Forecast_Timestamp Items are required for the script to function. The rule will also run when the script file is saved, or OH is restarted, so that you don’t have to wait for the trigger for the Item states to be populated.

The rule will arrange Items into the following group structure for days(X) 1-5. gForecast_1 contains the forecast Items for the rest of the current day, and the current Items, since at some time there won’t be any more forecasts left in the day. The other groups contain Items for subsequent days. The rule is set to trigger every time the binding pulls in new values.

Group labels will be updated to reflect the name of the day, and Item labels will be updated to show the time of the forecast, based on the time provided in the Forecast_Timestamp_XX Item.

gWeather
    gOpenWeatherMap
        gCurrent
        gForecast_X
            gForecast_Temperature_X
            gForecast_Pressure_X
            gForecast_Humidity_X
            gForecast_WindSpeed_X
            gForecast_WindDirection_X
            gForecast_Cloudiness_X
            gForecast_RainVolume_X
            gForecast_SnowVolume_X

Upgrading the Script

When upgrading this script, remove the comment from the line containing removeOWMItems(), so that all of the Items and Groups can be recreated using the updated definitions. If you do not do this, erros are likely to occur. In previous versions of the helper libraries, Items created using the libraries were not persisted after an OH restart. This has now been corrected. It is always best to upgrade the libraries and community scripts at the same time.

Requires

  • OpenWeatherMap binding

  • OpenWeatherMap Account Thing, configured with a free API key

  • OpenWeatherMap Weather and Forecast Thing, configured with ‘Number of Hours’ set to 120 and ‘Number of Days’ set to 0. The hours could be less (this is maxed out for a free API key), but you’ll need to adjust the script.

  • All OWM Items should be removed before using rule

  • The SCALE transformation service is required, but it will be installed for you. If you manually editted the ‘transformation’ line in addons.cfg, be sure to add it there, or it will uninstall next OH update or cache clearing.

Known Issues

  • ArithmeticGroupFunction.Avg does not properly average angles. An ESH issue has been opened for this… https://github.com/eclipse/smarthome/issues/6792. I noticed the units for the Cloudiness and Humidity groups display as ‘one’, but the Items display properly as ‘%’.

Change Log

  • 01/11/19: Corrected an issue where the Items were not linking.

  • 01/12/19: Removed Forecast_Temperature_X, and added Forecast_Temperature_High_X and Forecast_Temperature_Low_X

  • 01/16/19: Restructured how the rule is created

  • 01/16/19: Fixed SCALE transformation install

  • 01/16/19: Fixed Item existence check

  • 01/16/19: Changed to using values of Timestamp Items for calculating the number of remaining forecasts, and the Item label times

  • 01/16/19: Added group label changes to reflect current day of week

  • 01/16/19: Changed Item label to use time from Timestamp Items

  • 01/16/19: Added IconID

  • 01/16/19: Added manual group aggregation for Condition, ConditionID, Icon, IconID, and WindDirection

  • 01/18/19: Fixed issue with Items not being added to groups properly

  • 01/18/19: Added a NULL check when manually setting group aggregation values

  • 01/20/19: Fixed improper log entry after SCALE transform has been installed

  • 02/04/19: Added check to make sure a Thing with ThingUID ‘openweathermap:forecast-and-weather’ exists and is ONLINE

  • 02/06/19: Added verification that the forecastHours and forecastDays are configured properly in the Thing

  • 05/31/19: Fixed Cloudiness and Humidity units and group function (no more unit of ‘one’!)

from core.log import logging, LOG_PREFIX, log_traceback

@log_traceback
def removeOWMItems():
    removeOWMItems.log = logging.getLogger("{}.removeOWMItems".format(LOG_PREFIX))
    from core.items import remove_item

    for item in ir.getItemsByTag("OpenWeatherMap"):
        removeOWMItems.log.debug("removeOWMItems: [{}]".format(item))
        remove_item(item)
    '''
    # use this as a last resort, but make sure it's not removing any Items not
    created by this script

    for item in ir.getAll():
        if "Forecast_" in item.name or "Current_" in item.name:
            removeOWMItems.log.debug("removeOWMItems: [{}]".format(item))
            remove_item(item)
    '''
#removeOWMItems()

def addOWMItems():
    addOWMItems.log = logging.getLogger("{}.addOWMItems".format(LOG_PREFIX))

    # create OWM Items and groups, if they do not exist
    scriptExtension.importPreset("RuleSupport")

    from javax.measure.quantity import Dimensionless, Temperature, Length, Speed, Pressure#, Angle

    try:
        from org.openhab.core.thing import ThingTypeUID
        from org.openhab.core.thing import ChannelUID
    except:
        from org.eclipse.smarthome.core.thing import ThingTypeUID
        from org.eclipse.smarthome.core.thing import ChannelUID

    try:
        from org.eclipse.smarthome.core.library.types import QuantityTypeArithmeticGroupFunction
    except:
        from org.openhab.core.library.types import QuantityTypeArithmeticGroupFunction

    from core.items import add_item
    from core.links import add_link

    try:
        owmThingUID = None
        for thing in things.getAll():
            if thing.getThingTypeUID() == ThingTypeUID("openweathermap:weather-and-forecast"):
                if str(thing.statusInfo) == "ONLINE":
                    thingConfiguration = thing.getConfiguration()
                    forecastHours = thingConfiguration.get("forecastHours")
                    if str(forecastHours) == "120":
                        forecastDays = thingConfiguration.get("forecastDays")
                        if str(forecastDays) == "0":
                            owmThingUID = str(thing.getUID())
                            break
                        else:
                            addOWMItems.log.warn("Thing found, but forecastDays is not set to [0]: forecastDays=[{}]".format(forecastDays))
                    else:
                        addOWMItems.log.warn("Thing found, but forecastHours is not set to [120]: forecastHours=[{}]".format(forecastHours))
                else:
                    addOWMItems.log.warn("Thing found, but statusInfo was not [ONLINE]: statusInfo=[{}]".format(thing.statusInfo))
        if owmThingUID is None:
            addOWMItems.log.warn("No Thing found with ThingTypeUID 'openweathermap:weather-and-forecast', or it was not ONLINE, or it was improperly configured for the free API. Exiting script.")
        else:
            addOWMItems.log.debug("owmThingUID set to [{}]".format(owmThingUID))

            # install Scale transformation service, if not already
            from org.eclipse.smarthome.model.script.actions.Exec import executeCommandLine
            import json

            scaleCheckResult = json.loads(executeCommandLine("/bin/sh@@-c@@/usr/bin/curl -s --connect-timeout 10 -m 10 -X GET -H \"Accept: application/json\" \"http://localhost:8080/rest/extensions/transformation-scale\"",15000))['installed']
            if not scaleCheckResult:
                installScaleResult = executeCommandLine("/bin/sh@@-c@@/usr/bin/curl -o /dev/null -s -w \"%{http_code}\" --connect-timeout 10 -m 10 -X POST -H \"Content-Type: application/json\" -H \"Accept: application/json\" \"http://localhost:8080/rest/extensions/transformation-scale/install\"",15000)
                if installScaleResult != "200":
                    addOWMItems.log.debug("Scale transformation service installation failed")
                    return
                else:
                    addOWMItems.log.debug("Scale transformation service has been installed")
            else:
                addOWMItems.log.debug("Scale transformation service is already installed")

            # create Current group and Items
            if ir.getItems("gOpenWeatherMap") == []:
                add_item("gOpenWeatherMap", item_type="Group", groups=["gWeather"], label="OpenWeatherMap", tags=["OpenWeatherMap"])
            if ir.getItems("gCurrent") == []:
                add_item("gCurrent", item_type="Group", groups=["gOpenWeatherMap"], label="Current", tags=["OpenWeatherMap"])
            if ir.getItems("Current_Timestamp") == []:
                add_item("Current_Timestamp", item_type="DateTime", groups=["gCurrent", "gForecast_Timestamp_1"], label="Current: Timestamp [%1$tY-%1$tm-%1$td %1$tI:%1$tM%1$tp]", category="Time", tags=["OpenWeatherMap"])
                add_link("Current_Timestamp", ChannelUID(owmThingUID + ":current#time-stamp"))
            if ir.getItems("Current_Condition") == []:
                add_item("Current_Condition", item_type="String", groups=["gCurrent", "gForecast_Condition_1"], label="Current: Condition [%s]", category="Sun_Clouds", tags=["OpenWeatherMap"])
                add_link("Current_Condition", ChannelUID(owmThingUID + ":current#condition"))
            if ir.getItems("Current_ConditionID") == []:
                add_item("Current_ConditionID", item_type="String", groups=["gCurrent", "gForecast_ConditionID_1"], label="Current: Condition ID [%s]", tags=["OpenWeatherMap"])
                add_link("Current_ConditionID", ChannelUID(owmThingUID + ":current#condition-id"))
            if ir.getItems("Current_IconID") == []:
                add_item("Current_IconID", item_type="String", groups=["gCurrent", "gForecast_IconID_1"], label="Current: Icon ID [%s]", tags=["OpenWeatherMap"])
                add_link("Current_IconID", ChannelUID(owmThingUID + ":current#icon-id"))
            if ir.getItems("Current_Icon") == []:
                add_item("Current_Icon", item_type="Image", groups=["gCurrent", "gForecast_Icon_1"], label="Current: Icon", tags=["OpenWeatherMap"])
                add_link("Current_Icon", ChannelUID(owmThingUID + ":current#icon"))
            if ir.getItems("Current_Temperature") == []:
                add_item("Current_Temperature", item_type="Number:Temperature", groups=["gCurrent", "gForecast_Temperature_High_1", "gForecast_Temperature_Low_1"], label="Current: Temperature [%.0f %unit%]", category="Temperature", tags=["OpenWeatherMap"])
                add_link("Current_Temperature", ChannelUID(owmThingUID + ":current#temperature"))
            if ir.getItems("Current_Pressure") == []:
                add_item("Current_Pressure", item_type="Number:Pressure", groups=["gCurrent", "gForecast_Pressure_1"], label="Current: Pressure [%.1f %unit%]", category="Pressure", tags=["OpenWeatherMap"])
                add_link("Current_Pressure", ChannelUID(owmThingUID + ":current#pressure"))
            if ir.getItems("Current_Humidity") == []:
                add_item("Current_Humidity", item_type="Number:Dimensionless", groups=["gCurrent", "gForecast_Humidity_1"], label="Current: Humidity [%d %%]", category="Humidity", tags=["OpenWeatherMap"])
                add_link("Current_Humidity", ChannelUID(owmThingUID + ":current#humidity"))
            if ir.getItems("Current_WindSpeed") == []:
                add_item("Current_WindSpeed", item_type="Number:Speed", groups=["gCurrent", "gForecast_WindSpeed_1"], label="Current: Wind speed [%.0f %unit%]", category="Wind", tags=["OpenWeatherMap"])
                add_link("Current_WindSpeed", ChannelUID(owmThingUID + ":current#wind-speed"))
            if ir.getItems("Current_GustSpeed") == []:
                add_item("Current_GustSpeed", item_type="Number:Speed", groups=["gCurrent", "gForecast_GustSpeed_1"], label="Current: Gust speed [%.0f %unit%]", category="Wind", tags=["OpenWeatherMap"])
                add_link("Current_GustSpeed", ChannelUID(owmThingUID + ":current#gust-speed"))
            if ir.getItems("Current_WindDirection") == []:
                add_item("Current_WindDirection", item_type="Number:Angle", groups=["gCurrent", "gForecast_WindDirection_1"], label="Current: Wind direction [SCALE(windDirection.scale):%s]", category="Wind", tags=["OpenWeatherMap"])
                add_link("Current_WindDirection", ChannelUID(owmThingUID + ":current#wind-direction"))
            if ir.getItems("Current_Cloudiness") == []:
                add_item("Current_Cloudiness", item_type="Number:Dimensionless", groups=["gCurrent", "gForecast_Cloudiness_1"], label="Current: Cloudiness [%d %%]", category="Sun_Clouds", tags=["OpenWeatherMap"])
                add_link("Current_Cloudiness", ChannelUID(owmThingUID + ":current#cloudiness"))
            if ir.getItems("Current_RainVolume") == []:
                add_item("Current_RainVolume", item_type="Number:Length", groups=["gCurrent", "gForecast_RainVolume_1"], label="Current: Rain volume [%.1f %unit%]", category="Rain", tags=["OpenWeatherMap"])
                add_link("Current_RainVolume", ChannelUID(owmThingUID + ":current#rain"))
            if ir.getItems("Current_SnowVolume") == []:
                add_item("Current_SnowVolume", item_type="Number:Length", groups=["gCurrent", "gForecast_SnowVolume_1"], label="Current: Snow volume [%.1f %unit%]", category="Snow", tags=["OpenWeatherMap"])
                add_link("Current_SnowVolume", ChannelUID(owmThingUID + ":current#snow"))

            # create Forecast groups
            import calendar
            from org.joda.time import DateTime
            lastReading = DateTime(str(items["Current_Timestamp"])).getDayOfWeek() - 1
            for index in range(1, 6):
                dayOfWeek = "Today" if index == 1 else calendar.day_name[(lastReading + index - 1) % 7]
                if ir.getItems("gForecast_" + str(index)) == []:
                    add_item("gForecast_" + str(index), item_type="Group", groups=["gOpenWeatherMap"], label=dayOfWeek, tags=["OpenWeatherMap"])
                if ir.getItems("gForecast_Timestamp_" + str(index)) == []:
                    add_item("gForecast_Timestamp_" + str(index), item_type="Group", groups=["gForecast_" + str(index)], label=dayOfWeek + ": Timestamp [%1$tY-%1$tm-%1$td %1$tI:%1$tM%1$tp]", category="Time", tags=["OpenWeatherMap"])
                if ir.getItems("gForecast_Condition_" + str(index)) == []:
                    add_item("gForecast_Condition_" + str(index), item_type="Group", groups=["gForecast_" + str(index)], label=dayOfWeek + ": Condition [%s]", gi_base_type="String", category="Sun_Clouds", tags=["OpenWeatherMap"])
                if ir.getItems("gForecast_ConditionID_" + str(index)) == []:
                    add_item("gForecast_ConditionID_" + str(index), item_type="Group", groups=["gForecast_" + str(index)], label=dayOfWeek + ": Condition ID [%s]", gi_base_type="String", tags=["OpenWeatherMap"])
                if ir.getItems("gForecast_IconID_" + str(index)) == []:
                    add_item("gForecast_IconID_" + str(index), item_type="Group", groups=["gForecast_" + str(index)], label=dayOfWeek + ": Icon ID [%s]", gi_base_type="String", tags=["OpenWeatherMap"])
                if ir.getItems("gForecast_Icon_" + str(index)) == []:
                    add_item("gForecast_Icon_" + str(index), item_type="Group", groups=["gForecast_" + str(index)], label=dayOfWeek + ": Icon", gi_base_type="Image", tags=["OpenWeatherMap"])
                if ir.getItems("gForecast_Temperature_High_" + str(index)) == []:
                    add_item("gForecast_Temperature_High_" + str(index), item_type="Group", groups=["gForecast_" + str(index)], label=dayOfWeek + ": Temperature (high) [%.0f %unit%]", category="Temperature_Hot", gi_base_type="Number:Temperature", group_function=QuantityTypeArithmeticGroupFunction.Max(Temperature), tags=["OpenWeatherMap"])
                if ir.getItems("gForecast_Temperature_Low_" + str(index)) == []:
                    add_item("gForecast_Temperature_Low_" + str(index), item_type="Group", groups=["gForecast_" + str(index)], label=dayOfWeek + ": Temperature (low) [%.0f %unit%]", category="Temperature_Cold", gi_base_type="Number:Temperature", group_function=QuantityTypeArithmeticGroupFunction.Min(Temperature), tags=["OpenWeatherMap"])
                if ir.getItems("gForecast_Pressure_" + str(index)) == []:
                    add_item("gForecast_Pressure_" + str(index), item_type="Group", groups=["gForecast_" + str(index)], label=dayOfWeek + ": Pressure [%.1f %unit%]", category="Pressure", gi_base_type="Number:Pressure", group_function=QuantityTypeArithmeticGroupFunction.Max(Pressure), tags=["OpenWeatherMap"])
                if ir.getItems("gForecast_Humidity_" + str(index)) == []:
                    add_item("gForecast_Humidity_" + str(index), item_type="Group", groups=["gForecast_" + str(index)], label=dayOfWeek + ": Humidity [%d %%]", category="Humidity", gi_base_type="Number:Dimensionless", group_function=QuantityTypeArithmeticGroupFunction.Max(Dimensionless), tags=["OpenWeatherMap"])
                if ir.getItems("gForecast_WindSpeed_" + str(index)) == []:
                    add_item("gForecast_WindSpeed_" + str(index), item_type="Group", groups=["gForecast_" + str(index)], label=dayOfWeek + ": Wind Speed [%.0f %unit%]", category="Wind", gi_base_type="Number:Speed", group_function=QuantityTypeArithmeticGroupFunction.Max(Speed), tags=["OpenWeatherMap"])
                if ir.getItems("gForecast_GustSpeed_" + str(index)) == []:
                    add_item("gForecast_GustSpeed_" + str(index), item_type="Group", groups=["gForecast_" + str(index)], label=dayOfWeek + ": Gust Speed [%.0f %unit%]", category="Wind", gi_base_type="Number:Speed", group_function=QuantityTypeArithmeticGroupFunction.Max(Speed), tags=["OpenWeatherMap"])
                if ir.getItems("gForecast_WindDirection_" + str(index)) == []:
                    #add_item("gForecast_WindDirection_" + str(index), item_type="Group", groups=["gForecast_" + str(index)], label=dayOfWeek + ": Wind direction [SCALE(windDirection.scale):%s]", category="Wind", gi_base_type="Number:Angle", group_function=QuantityTypeArithmeticGroupFunction.Avg(Angle), tags=["OpenWeatherMap"])# this doesn't work properly yet
                    add_item("gForecast_WindDirection_" + str(index), item_type="Group", groups=["gForecast_" + str(index)], label=dayOfWeek + ": Wind direction [SCALE(windDirection.scale):%s]", category="Wind", gi_base_type="Number:Angle", tags=["OpenWeatherMap"])
                if ir.getItems("gForecast_Cloudiness_" + str(index)) == []:
                    add_item("gForecast_Cloudiness_" + str(index), item_type="Group", groups=["gForecast_" + str(index)], label=dayOfWeek + ": Cloudiness [%d %%]", category="Sun_Clouds", gi_base_type="Number:Dimensionless", group_function=QuantityTypeArithmeticGroupFunction.Max(Dimensionless), tags=["OpenWeatherMap"])
                if ir.getItems("gForecast_RainVolume_" + str(index)) == []:
                    add_item("gForecast_RainVolume_" + str(index), item_type="Group", groups=["gForecast_" + str(index)], label=dayOfWeek + ": Rain Volume [%.1f %unit%]", category="Rain", gi_base_type="Number:Length", group_function=QuantityTypeArithmeticGroupFunction.Sum(Length), tags=["OpenWeatherMap"])
                if ir.getItems("gForecast_SnowVolume_" + str(index)) == []:
                    add_item("gForecast_SnowVolume_" + str(index), item_type="Group", groups=["gForecast_" + str(index)], label=dayOfWeek + ": Snow Volume [%.1f %unit%]", category="Snow", gi_base_type="Number:Length", group_function=QuantityTypeArithmeticGroupFunction.Sum(Length), tags=["OpenWeatherMap"])

            # create Forecast Items
            for index in range(1, 41):
                if ir.getItems("Forecast_Timestamp_{:02d}".format(3 * index)) == []:
                    add_item("Forecast_Timestamp_{:02d}".format(3 * index), item_type="DateTime", label="Forecast ({:02d}): Timestamp [%1$tY-%1$tm-%1$td %1$tI:%1$tM%1$tp]".format(3 * index), category="Time", tags=["OpenWeatherMap"])
                    add_link("Forecast_Timestamp_{:02d}".format(3 * index), ChannelUID(owmThingUID + ":forecastHours{:02d}#time-stamp".format(3 * index)))
                if ir.getItems("Forecast_Condition_{:02d}".format(3 * index)) == []:
                    add_item("Forecast_Condition_{:02d}".format(3 * index), item_type="String", label="Forecast ({:02d}): Condition [%s]".format(3 * index), category="Sun_Clouds", tags=["OpenWeatherMap"])
                    add_link("Forecast_Condition_{:02d}".format(3 * index), ChannelUID(owmThingUID + ":forecastHours{:02d}#condition".format(3 * index)))
                if ir.getItems("Forecast_ConditionID_{:02d}".format(3 * index)) == []:
                    add_item("Forecast_ConditionID_{:02d}".format(3 * index), item_type="String", label="Forecast ({:02d}): Condition ID [%s]".format(3 * index), tags=["OpenWeatherMap"])
                    add_link("Forecast_ConditionID_{:02d}".format(3 * index), ChannelUID(owmThingUID + ":forecastHours{:02d}#condition-id".format(3 * index)))
                if ir.getItems("Forecast_IconID_{:02d}".format(3 * index)) == []:
                    add_item("Forecast_IconID_{:02d}".format(3 * index), item_type="String", label="Forecast ({:02d}): Icon ID [%s]".format(3 * index), tags=["OpenWeatherMap"])
                    add_link("Forecast_IconID_{:02d}".format(3 * index), ChannelUID(owmThingUID + ":forecastHours{:02d}#icon-id".format(3 * index)))
                if ir.getItems("Forecast_Icon_{:02d}".format(3 * index)) == []:
                    add_item("Forecast_Icon_{:02d}".format(3 * index), item_type="Image", label="Forecast ({:02d}): Icon".format(3 * index), tags=["OpenWeatherMap"])
                    add_link("Forecast_Icon_{:02d}".format(3 * index), ChannelUID(owmThingUID + ":forecastHours{:02d}#icon".format(3 * index)))
                if ir.getItems("Forecast_Temperature_{:02d}".format(3 * index)) == []:
                    add_item("Forecast_Temperature_{:02d}".format(3 * index), item_type="Number:Temperature", label="Forecast ({:02d}): Temperature [%.0f %unit%]".format(3 * index), category="Temperature", tags=["OpenWeatherMap"])
                    add_link("Forecast_Temperature_{:02d}".format(3 * index), ChannelUID(owmThingUID + ":forecastHours{:02d}#temperature".format(3 * index)))
                if ir.getItems("Forecast_Pressure_{:02d}".format(3 * index)) == []:
                    add_item("Forecast_Pressure_{:02d}".format(3 * index), item_type="Number:Pressure", label="Forecast ({:02d}): Pressure [%.1f %unit%]".format(3 * index), category="Pressure", tags=["OpenWeatherMap"])
                    add_link("Forecast_Pressure_{:02d}".format(3 * index), ChannelUID(owmThingUID + ":forecastHours{:02d}#pressure".format(3 * index)))
                if ir.getItems("Forecast_Humidity_{:02d}".format(3 * index)) == []:
                    add_item("Forecast_Humidity_{:02d}".format(3 * index), item_type="Number:Dimensionless", label="Forecast ({:02d}): Humidity [%d %%]".format(3 * index), category="Humidity", tags=["OpenWeatherMap"])
                    add_link("Forecast_Humidity_{:02d}".format(3 * index), ChannelUID(owmThingUID + ":forecastHours{:02d}#humidity".format(3 * index)))
                if ir.getItems("Forecast_WindSpeed_{:02d}".format(3 * index)) == []:
                    add_item("Forecast_WindSpeed_{:02d}".format(3 * index), item_type="Number:Speed", label="Forecast ({:02d}): Wind speed [%.0f %unit%]".format(3 * index), category="Wind", tags=["OpenWeatherMap"])
                    add_link("Forecast_WindSpeed_{:02d}".format(3 * index), ChannelUID(owmThingUID + ":forecastHours{:02d}#wind-speed".format(3 * index)))
                if ir.getItems("Forecast_GustSpeed_{:02d}".format(3 * index)) == []:
                    add_item("Forecast_GustSpeed_{:02d}".format(3 * index), item_type="Number:Speed", label="Forecast ({:02d}): Gust speed [%.0f %unit%]".format(3 * index), category="Wind", tags=["OpenWeatherMap"])
                    add_link("Forecast_GustSpeed_{:02d}".format(3 * index), ChannelUID(owmThingUID + ":forecastHours{:02d}#gust-speed".format(3 * index)))
                if ir.getItems("Forecast_WindDirection_{:02d}".format(3 * index)) == []:
                    add_item("Forecast_WindDirection_{:02d}".format(3 * index), item_type="Number:Angle", label="Forecast ({:02d}): Wind direction [SCALE(windDirection.scale):%s]".format(3 * index), category="Wind", tags=["OpenWeatherMap"])
                    add_link("Forecast_WindDirection_{:02d}".format(3 * index), ChannelUID(owmThingUID + ":forecastHours{:02d}#wind-direction".format(3 * index)))
                if ir.getItems("Forecast_Cloudiness_{:02d}".format(3 * index)) == []:
                    add_item("Forecast_Cloudiness_{:02d}".format(3 * index), item_type="Number:Dimensionless", label="Forecast ({:02d}): Cloudiness [%d %%]".format(3 * index), category="Sun_Clouds", tags=["OpenWeatherMap"])
                    add_link("Forecast_Cloudiness_{:02d}".format(3 * index), ChannelUID(owmThingUID + ":forecastHours{:02d}#cloudiness".format(3 * index)))
                if ir.getItems("Forecast_RainVolume_{:02d}".format(3 * index)) == []:
                    add_item("Forecast_RainVolume_{:02d}".format(3 * index), item_type="Number:Length", label="Forecast ({:02d}): Rain volume [%.1f %unit%]".format(3 * index), category="Rain", tags=["OpenWeatherMap"])
                    add_link("Forecast_RainVolume_{:02d}".format(3 * index), ChannelUID(owmThingUID + ":forecastHours{:02d}#rain".format(3 * index)))
                if ir.getItems("Forecast_SnowVolume_{:02d}".format(3 * index)) == []:
                    add_item("Forecast_SnowVolume_{:02d}".format(3 * index), item_type="Number:Length", label="Forecast ({:02d}): Snow volume [%.1f %unit%]".format(3 * index), category="Snow", tags=["OpenWeatherMap"])
                    add_link("Forecast_SnowVolume_{:02d}".format(3 * index), ChannelUID(owmThingUID + ":forecastHours{:02d}#snow".format(3 * index)))

            from core.rules import rule
            from core.triggers import when

            @rule("Add OpenWeatherMap Items to daily forecast groups")
            @when("Item Current_Timestamp changed")
            def addOWMItemsToGroups(event):
                # remove hourly forecast Items from groups
                for groupIndex in range(1, 6):
                    for member in filter(lambda item: "Current" not in item.name, ir.getItem("gForecast_Timestamp_{}".format(groupIndex)).getMembers()):
                        ir.getItem("gForecast_Timestamp_{}".format(groupIndex)).removeMember(member)
                    for member in filter(lambda item: "Current" not in item.name, ir.getItem("gForecast_Condition_{}".format(groupIndex)).getMembers()):
                        ir.getItem("gForecast_Condition_{}".format(groupIndex)).removeMember(member)
                    for member in filter(lambda item: "Current" not in item.name, ir.getItem("gForecast_ConditionID_{}".format(groupIndex)).getMembers()):
                        ir.getItem("gForecast_ConditionID_{}".format(groupIndex)).removeMember(member)
                    for member in filter(lambda item: "Current" not in item.name, ir.getItem("gForecast_IconID_{}".format(groupIndex)).getMembers()):
                        ir.getItem("gForecast_IconID_{}".format(groupIndex)).removeMember(member)
                    for member in filter(lambda item: "Current" not in item.name, ir.getItem("gForecast_Icon_{}".format(groupIndex)).getMembers()):
                        ir.getItem("gForecast_Icon_{}".format(groupIndex)).removeMember(member)
                    for member in filter(lambda item: "Current" not in item.name, ir.getItem("gForecast_Temperature_High_{}".format(groupIndex)).getMembers()):
                        ir.getItem("gForecast_Temperature_High_{}".format(groupIndex)).removeMember(member)
                    for member in filter(lambda item: "Current" not in item.name, ir.getItem("gForecast_Temperature_Low_{}".format(groupIndex)).getMembers()):
                        ir.getItem("gForecast_Temperature_Low_{}".format(groupIndex)).removeMember(member)
                    for member in filter(lambda item: "Current" not in item.name, ir.getItem("gForecast_Pressure_{}".format(groupIndex)).getMembers()):
                        ir.getItem("gForecast_Pressure_{}".format(groupIndex)).removeMember(member)
                    for member in filter(lambda item: "Current" not in item.name, ir.getItem("gForecast_Humidity_{}".format(groupIndex)).getMembers()):
                        ir.getItem("gForecast_Humidity_{}".format(groupIndex)).removeMember(member)
                    for member in filter(lambda item: "Current" not in item.name, ir.getItem("gForecast_WindSpeed_{}".format(groupIndex)).getMembers()):
                        ir.getItem("gForecast_WindSpeed_{}".format(groupIndex)).removeMember(member)
                    for member in filter(lambda item: "Current" not in item.name, ir.getItem("gForecast_GustSpeed_{}".format(groupIndex)).getMembers()):
                        ir.getItem("gForecast_GustSpeed_{}".format(groupIndex)).removeMember(member)
                    for member in filter(lambda item: "Current" not in item.name, ir.getItem("gForecast_WindDirection_{}".format(groupIndex)).getMembers()):
                        ir.getItem("gForecast_WindDirection_{}".format(groupIndex)).removeMember(member)
                    for member in filter(lambda item: "Current" not in item.name, ir.getItem("gForecast_Cloudiness_{}".format(groupIndex)).getMembers()):
                        ir.getItem("gForecast_Cloudiness_{}".format(groupIndex)).removeMember(member)
                    for member in filter(lambda item: "Current" not in item.name, ir.getItem("gForecast_RainVolume_{}".format(groupIndex)).getMembers()):
                        ir.getItem("gForecast_RainVolume_{}".format(groupIndex)).removeMember(member)
                    for member in filter(lambda item: "Current" not in item.name, ir.getItem("gForecast_SnowVolume_{}".format(groupIndex)).getMembers()):
                        ir.getItem("gForecast_SnowVolume_{}".format(groupIndex)).removeMember(member)

                # update group labels to reflect week day
                from org.joda.time import DateTime
                import calendar
                lastReading = DateTime(str(items["Current_Timestamp"])).getDayOfWeek() - 1
                for index in range(1, 6):
                    dayOfWeek = "Today" if index == 1 else calendar.day_name[(lastReading + index - 1) % 7]
                    ir.getItem("gForecast_" + str(index)).setLabel(dayOfWeek)
                    ir.getItem("gForecast_Timestamp_" + str(index)).setLabel(dayOfWeek + ": Timestamp")
                    ir.getItem("gForecast_Condition_" + str(index)).setLabel(dayOfWeek + ": Condition [%s]")
                    ir.getItem("gForecast_ConditionID_" + str(index)).setLabel(dayOfWeek + ": Condition ID [%s]")
                    ir.getItem("gForecast_IconID_" + str(index)).setLabel(dayOfWeek + ": Icon ID [%s]")
                    ir.getItem("gForecast_Icon_" + str(index)).setLabel(dayOfWeek + ": Icon")
                    ir.getItem("gForecast_Temperature_High_" + str(index)).setLabel(dayOfWeek + ": Temperature (high) [%.0f %unit%]")
                    ir.getItem("gForecast_Temperature_Low_" + str(index)).setLabel(dayOfWeek + ": Temperature (low) [%.0f %unit%]")
                    ir.getItem("gForecast_Pressure_" + str(index)).setLabel(dayOfWeek + ": Pressure [%.1f %unit%]")
                    ir.getItem("gForecast_Humidity_" + str(index)).setLabel(dayOfWeek + ": Humidity [%d %%]")
                    ir.getItem("gForecast_WindSpeed_" + str(index)).setLabel(dayOfWeek + ": Wind Speed [%.0f %unit%]")
                    ir.getItem("gForecast_GustSpeed_" + str(index)).setLabel(dayOfWeek + ": Gust Speed [%.0f %unit%]")
                    ir.getItem("gForecast_WindDirection_" + str(index)).setLabel(dayOfWeek + ": Wind direction [SCALE(windDirection.scale):%s]")
                    ir.getItem("gForecast_Cloudiness_" + str(index)).setLabel(dayOfWeek + ": Cloudiness [%d %%]")
                    ir.getItem("gForecast_RainVolume_" + str(index)).setLabel(dayOfWeek + ": Rain Volume [%.1f %unit%]")
                    ir.getItem("gForecast_SnowVolume_" + str(index)).setLabel(dayOfWeek + ": Snow Volume [%.1f %unit%]")

                # add Forecast Items to groups, and update the labels to reflect time
                groupIndex = 1
                for index in range(1, 41):
                    if DateTime(str(items["Forecast_Timestamp_{:02}".format(3 * index)])).getDayOfWeek() - 1 != (DateTime.now().getDayOfWeek() + groupIndex - 2) % 7:
                        if groupIndex == 5:
                            break# we're at the end of the forecasts that fit into 5 days
                        else:
                            groupIndex += 1
                    labelTime = items["Forecast_Timestamp_{:02}".format(3 * index)].format("%1$tl:%1$tM%1$tp")

                    ir.getItem("gForecast_Timestamp_{}".format(groupIndex)).addMember(ir.getItem("Forecast_Timestamp_{:02d}".format(3 * index)))
                    ir.getItem("Forecast_Timestamp_{:02d}".format(3 * index)).setLabel("Forecast ({}): Timestamp [%1$tY-%1$tm-%1$td %1$tI:%1$tM%1$tp]".format(labelTime))

                    ir.getItem("gForecast_Condition_{}".format(groupIndex)).addMember(ir.getItem("Forecast_Condition_{:02d}".format(3 * index)))
                    ir.getItem("Forecast_Condition_{:02d}".format(3 * index)).setLabel("Forecast ({}): Condition [%s]".format(labelTime))

                    ir.getItem("gForecast_ConditionID_{}".format(groupIndex)).addMember(ir.getItem("Forecast_ConditionID_{:02d}".format(3 * index)))
                    ir.getItem("Forecast_ConditionID_{:02d}".format(3 * index)).setLabel("Forecast ({}): Condition ID [%s]".format(labelTime))

                    ir.getItem("gForecast_IconID_{}".format(groupIndex)).addMember(ir.getItem("Forecast_IconID_{:02d}".format(3 * index)))
                    ir.getItem("Forecast_IconID_{:02d}".format(3 * index)).setLabel("Forecast ({}): Icon ID [%s]".format(labelTime))

                    ir.getItem("gForecast_Icon_{}".format(groupIndex)).addMember(ir.getItem("Forecast_Icon_{:02d}".format(3 * index)))
                    ir.getItem("Forecast_Icon_{:02d}".format(3 * index)).setLabel("Forecast ({}): Icon".format(labelTime))

                    ir.getItem("gForecast_Temperature_High_{}".format(groupIndex)).addMember(ir.getItem("Forecast_Temperature_{:02d}".format(3 * index)))
                    ir.getItem("Forecast_Temperature_{:02d}".format(3 * index)).setLabel("Forecast ({}): Temperature [%.0f %unit%]".format(labelTime))

                    ir.getItem("gForecast_Temperature_Low_{}".format(groupIndex)).addMember(ir.getItem("Forecast_Temperature_{:02d}".format(3 * index)))
                    ir.getItem("Forecast_Temperature_{:02d}".format(3 * index)).setLabel("Forecast ({}): Temperature [%.0f %unit%]".format(labelTime))

                    ir.getItem("gForecast_Pressure_{}".format(groupIndex)).addMember(ir.getItem("Forecast_Pressure_{:02d}".format(3 * index)))
                    ir.getItem("Forecast_Pressure_{:02d}".format(3 * index)).setLabel("Forecast ({}): Pressure [%.1f %unit%]".format(labelTime))

                    ir.getItem("gForecast_Humidity_{}".format(groupIndex)).addMember(ir.getItem("Forecast_Humidity_{:02d}".format(3 * index)))
                    ir.getItem("Forecast_Humidity_{:02d}".format(3 * index)).setLabel("Forecast ({}): Humidity [%d %%]".format(labelTime))

                    ir.getItem("gForecast_WindSpeed_{}".format(groupIndex)).addMember(ir.getItem("Forecast_WindSpeed_{:02d}".format(3 * index)))
                    ir.getItem("Forecast_WindSpeed_{:02d}".format(3 * index)).setLabel("Forecast ({}): Wind speed [%.0f %unit%]".format(labelTime))

                    ir.getItem("gForecast_GustSpeed_{}".format(groupIndex)).addMember(ir.getItem("Forecast_GustSpeed_{:02d}".format(3 * index)))
                    ir.getItem("Forecast_GustSpeed_{:02d}".format(3 * index)).setLabel("Forecast ({}): Gust speed [%.0f %unit%]".format(labelTime))

                    ir.getItem("gForecast_WindDirection_{}".format(groupIndex)).addMember(ir.getItem("Forecast_WindDirection_{:02d}".format(3 * index)))
                    ir.getItem("Forecast_WindDirection_{:02d}".format(3 * index)).setLabel("Forecast ({}): Wind direction [SCALE(windDirection.scale):%s]".format(labelTime))

                    ir.getItem("gForecast_Cloudiness_{}".format(groupIndex)).addMember(ir.getItem("Forecast_Cloudiness_{:02d}".format(3 * index)))
                    ir.getItem("Forecast_Cloudiness_{:02d}".format(3 * index)).setLabel("Forecast ({}): Cloudiness [%d %%]".format(labelTime))

                    ir.getItem("gForecast_RainVolume_{}".format(groupIndex)).addMember(ir.getItem("Forecast_RainVolume_{:02d}".format(3 * index)))
                    ir.getItem("Forecast_RainVolume_{:02d}".format(3 * index)).setLabel("Forecast ({}): Rain volume [%.1f %unit%]".format(labelTime))

                    ir.getItem("gForecast_SnowVolume_{}".format(groupIndex)).addMember(ir.getItem("Forecast_SnowVolume_{:02d}".format(3 * index)))
                    ir.getItem("Forecast_SnowVolume_{:02d}".format(3 * index)).setLabel("Forecast ({}): Snow volume [%.1f %unit%]".format(labelTime))

                # set Condition, Icon and WindDirection group values
                for index in range(1, 6):
                    for group in [600, 200, 500, 300, 700, 800]:# the Conditions are organized into groups (https://openweathermap.org/weather-conditions), which I have prioritized
                        forecastItems = filter(lambda item: int(item.state.toString()) in range(group, group + 100), ir.getItem("gForecast_ConditionID_" + str(index)).getMembers())
                        if len(forecastItems) > 0:
                            sortedItems = sorted(forecastItems, key = lambda item: int(item.state.toString()))
                            selectedItem = sortedItems.pop()# this will provide the highest value in the sorted list of Items, which is usually the most severe condition
                            events.postUpdate("gForecast_ConditionID_" + str(index), selectedItem.state.toString())
                            events.postUpdate("gForecast_Condition_" + str(index), items[selectedItem.name.replace("ID", "")].toString())
                            events.postUpdate("gForecast_IconID_" + str(index), items[selectedItem.name.replace("Condition", "Icon")].toString())
                            events.postUpdate(ir.getItem("gForecast_Icon_" + str(index)), items[selectedItem.name.replace("ConditionID", "Icon")])
                            break
                    # this can be removed when QuantityTypeArithmeticGroupFunction.Avg() is fixed for Number:Angle
                    windDirectionItemStates = map(lambda item: item.state.intValue(), filter(lambda member: member.state != NULL and member.state != UNDEF, ir.getItem("gForecast_WindDirection_" + str(index)).getMembers()))
                    if len(windDirectionItemStates) > 0:
                        windDirectionAvg = reduce(lambda x, y: (((x + y) / 2) if y - x < 180 else (x + y + 360) / 2) % 360, windDirectionItemStates)
                        events.postUpdate("gForecast_WindDirection_" + str(index), str(windDirectionAvg))

                addOWMItemsToGroups.log.debug("Updated groups and Items")

            addOWMItemsToGroups(None)
    except:
        import traceback
        addOWMItems.log.error(traceback.format_exc())

def scriptLoaded(id):
    addOWMItems()