Design Pattern: Associated Items

Please see Design Pattern: What is a Design Pattern and How Do I Use Them for how to read and use DPs.

Problem Statement

Often one will have a number of separate Items which are all related to each other in some way. For example, one might have a Contact on a door and a DateTime to represent when the last time the door was opened. This is easy enough to keep track of if there are only one or two such Items, but if one has a lot of similar Items or is using a lot of generically coded rules where one cannot hard code the names of the associated Items inside the rule mapping between the Items becomes difficult.

Concept

Name Items that are associated with each other such that given the name of one Item, the names of the other Items can be created with just a little bit of String manipulation. With the associated Item’s name, one can get a reference to that Item by pulling it out of a Group’s members using the findFirst method. Or one can pull it out of the Item registry directly.

Examples

postUpdate Action

In this example there are two door Contacts and two associated DateTime Items to store the last time the Door was opened or closed. A Rule triggers when one of the Contacts is updated and it updates the associated DateTime Item.

Items

Group gDoors:Contact

Contact Front "Front Door" <frontdoor> (gDoors)

DateTime Front_LastUpdate "Front Door Last Update [%1$tm/%1$td %1tH:%1tM]" <clock> 

Contact Back "Back Door" <frontdoor> (gDoors)

DateTime Back_LastUpdate "Back Door Last Update [%1$tm/%1$td %1tH:%1tM]" <clock> 

Python

from core.rules import rule
from core.triggers import when
from org.joda.time import DateTime

@rule("A Door's State Changed", description="Tracks the date/time when a door changes state", tags=["entry"])
@when("Member of gDoors changed")
def door_changed(event):
    if isinstance(items[event.itemName], UnDefType): return
    events.postUpdate(event.itemName+"_LastUpdate", str(DateTime.now())) # TODO find non-joda equivalent

Rules DSL

rule "A Door's State Changed"
    Member of gDoors changed
when
then
    if(previousState == NULL) return;
    postUpdate(triggeringItem.name+"_LastUpdate", now.toString)
end

Theory of Operation

The Rule gets triggered when any member of gDoors changes. If the door changed from NULL we don’t care about this change so we exit the Rule. We then use the name of the door Item to create the LastUpdate Item’s name and send it and update with now.toString.

The key is the Item’s names and the line:

    # Python
    events.postUpdate(event.itemName+"_LastUpdate", str(DateTime.now()))
    // Rules DSL
    postUpdate(triggeringItem.name+"_LastUpdate", now.toString)

Note: This is one of the few examples where the postUpdate Action is recommended for use.

Group findFirst

In this example there are a bunch of Items that represent the online/offline status of devices and network services. When any of these Items change state to offline, generate an alert and set a flag indicating that the Item went offline. When that Item comes back online generate an alert and reset the flag. Use a timer to avoid flapping.

Items

Group:Switch:AND(ON, OFF) gSensorStatus "Sensor's Status [MAP(admin.map):%s]"
  <network>

Group:Switch gOfflineAlerted

Switch vNest_Online "Nest Status [MAP(hvac.map):%s]"
    <network> (gSensorStatus)
    { nest="<[thermostats(Entryway).is_online]" }

Switch vNest_Online_Alerted (gOfflineAlerted)

Switch vNetwork_Cerberos "Cerberos Network [MAP(admin.map):%s]"
  <network> (gSensorStatus, gResetExpire)
  { channel="network:servicedevice:cerberos:online", expire="2m" }

Switch vNetwork_Cerberos_Alerted (gOfflineAlerted)

// Dozens more Item pairs

Rules DSL

import java.util.Map

val Map<String, Timer> timers = newHashMap

rule "A sensor changed its online state2"
when
    Member of gSensorStatus changed
then
    if(previousState == NULL) return;

    var n = transform("MAP", "admin.map", triggeringItem.name)
    val name = if(n == "") triggeringItem.name else n

    val alerted = gOfflineAlerted.members.findFirst[ a | a.name == triggeringItem.name+"_Alerted"] as SwitchItem
    if(alerted === null) {
        logError("admin", "Cannot find Item " + triggeringItem.name+"_Alerted")
        aInfo.sendCommand(triggeringItem.name + " doesn't have an alerted flag, it is now " + transform("MAP", "admin.map", triggeringItem.state.toString) + "!")
        return;
    }

    // If we are flapping, reschedule the timer and exit
    if(timers.get(triggeringItem.name) !== null) {
        timers.get(triggeringItem.name).reschedule(now.plusMinutes(1))
        logWarn("admin", name + " is flapping!")
        return;
    }

    // If alerted == OFF and triggeringItem == OFF then sensor went offline and we have not yet alerted
    // If alerted == ON and triggeringItem == ON then the sensor came back online after we alerted that it was offline
    if(alerted.state == triggeringItem.state) {
        val currState = triggeringItem.state
        // wait one minute before alerting to make sure it isn't flapping
        timers.put(triggeringItem.name, createTimer(now.plusMinutes(1), [ |
            // If the current state of the Item matches the saved state after 5 minutes send the alert
            if(triggeringItem.state == currState) {
                aInfo.sendCommand(name + " is now " + transform("MAP", "admin.map", triggeringItem.state.toString) + "!")
                alerted.postUpdate(if(currState == ON) OFF else ON)
            }
            timers.put(triggeringItem.name, null)
        ]))
    }
end

Theory of Operation

See below for the JSR223 Python approach. There is no need to use findFirst in JSR223 Rules.

If the sensor changed from NULL or UNDEF we don’t care so immediately return.

Get the friendly name for the Item using Design Pattern: Human Readable Names in Messages.

Use the findFirst method to pull the associated Alerted Item from gOfflineAlerted based on the name of the triggeringItem. If the Item doesn’t exist, log the error and send an alert to indicate the configuration error (i.e. missing Alerted Item) and exit.

If there is already a timer set for this Item reschedule the timer.

If the alerted Item’s state equals the Item’s state it means we need to alert. Create a timer. If the timer doesn’t get cancelled and the triggering Item has remained unchanged send an alert.

Item Registry

Many thanks and full credit goes to @5iver for discovering this option. We can access Items from the Item registry directly using the ScriptServiceUtil. In JSR223 we have direct access to the Item registry.

In this example we have a Rule that turns on/off some lights depending on whether the weather says it is cloudy or not.

Items

Switch vIsCloudy // set to ON when the weather says it is cloudy
String vTimeOfDay // represents the current time of day

Group gLights_ON_WEATHER // Group of lights to turn on when it is cloudy

Python

from core.triggers import when
from time import sleep

@rule("Cloudy Lights", description="Turns ON or OFF some lights when it is cloudy during the day", tags=["lights"])
@when("Item vIsCloudy changed")
@when("Item vTimeOfDay changed")
def cloudy_lights(event):
    if items["vTimeOfDay"] != "DAY" or isinstance(items["vIsCloudy"], UnDefType): return

    if event.itemName == "vTimeOfDay": sleep(0.5)

    cloudy_lights.log.info("It is {} and cloudy changed: {}".format(items["vTimeOfDay"], items["vIsCloudy"]))

    for light in ir.getItem("gLights_ON_WEATHER").members:
        if items[light.name+"_Override"] != ON and light.state != items["vIsCloudy"]: events.sendCommand(light, items["vIsCloudy"])
        if items[light.name+"_Override"] == ON: cloudy_lights("{} is overridden".format(light.name))

Rules DSL

import org.eclipse.smarthome.model.script.ScriptServiceUtil

// Thoery of operation: If it is day time, turn on/off the weather lights when cloudy conditions
// change. Trigger the rule when it first becomes day so we can apply cloudy to lights then as well.
rule "Turn on lights when it is cloudy"
when
    Item vIsCloudy changed or
    Item vTimeOfDay changed to "DAY"
then
    // We only care about daytime and vIsCloudy isn't NULL
    if(vTimeOfDay.state != "DAY" || vIsCloudy.state == NULL) return;

    // give the side effects of time of day time to complete
    if(triggeringItem.name == "vTimeOfDay") Thread::sleep(500)

    logInfo("lighting", "It is " + vTimeOfDay.state.toString + " and cloudy changed: " + vIsCloudy.state.toString +", adjusting lighting")

    // Apply the cloudy state to all the lights in the weather group
    gLights_ON_WEATHER.members.forEach[ SwitchItem l |

        val overrideName = l.name+"_Override"
        val override = ScriptServiceUtil.getItemRegistry.getItem(overrideName)

        if(override.state != ON && l.state != vIsCloudy.state) l.sendCommand(vIsCloudy.state as OnOffType)
 
        if(override.state == ON) logInfo("lighting", l.name + " is overridden")
    ]
end

For Rules DSL, the key things of import are the Item’s name and the lines:

import org.eclipse.smarthome.model.script.ScriptServiceUtil
...
    val override = ScriptServiceUtil.getItemRegistry.getItem(overrideName)

For the Python version, an Item’s state is accessible by name using the items dict. If you have a need to get access to the Item itself, the equivalent of the ScriptServiceUtil line from above is ir.getItem("ItemName").

Alternative using Metadata

This is only an option for JSR223 Rules as Rules DSL does not provide a mechanism to access the metadata of an Item. See Design Pattern: Using Item Metadata as an Alternative to Several DPs for an example of the above complex example using metadata.

I particular, if you don’t need to show the last time that a door changed state on your sitemap, you could store that timestamp as metadata on the Item. In the findFirst example both the human friendly name access from the MAP and the alerted Item can be replaced with metadata (the example code at the link above is the JSR223 Python equivalent Rules using metadata for both of these). In the lights example, the Override flag could be replaces with metadata.

This does not mean that all cases where an associated Item would be used should be implemented as metadata. For example, if you have an irrigation system and you want to be able to configure the run time of each zone from your sitemap, this DP would apply.

Advantages and Disadvantages

Advantages

  • Allows for the creation of very generic Rules
  • Allows for the addition and subtraction of Items that get processed by a given Rule using Group membership

Disadvantages

  • Generates a proliferation of Items
  • Constrains Item naming schemes

Related Design Patterns

Design Pattern How It’s Used
[Rules DSL] Get item from string name! Source for the Item Registry Example
Design Pattern: Unbound Item (aka Virtual Item) Most associated Items are Unbound Items
Design Pattern: Working with Groups in Rules More examples of findFirst and forEach and other Group manipulations
Design Patterns: Generic Is Alive The Group findFirst example is a specific implementation of this DP
Design Pattern: Motion Sensor Timer The anti-flapping timer in the Group findFirst example is a specific implementation of this DP
Design Pattern: Human Readable Names in Messages Used in the Group findFirst example to transform the Item name to a more human readable name for logs and alerts.
Design Pattern: Separation of Behaviors Alerting implementation in Group findFirst and the setting of vIsCloudy in the Item Registry example
Design Pattern: Time Of Day Used to calculate the time of day in the Item Registry example
Design Pattern: Using Item Metadata as an Alternative to Several DPs Alternative approach using metadata.

Edit: added JSR223 Python examples, minor grammar updates, created a picture.

29 Likes

Nice! I did not know that’s possible… or at least I didn’t know how. That would totally simplify one workaround I have in my setup. Thanks

I wasn’t aware of this item.lastUpdate, is that an OH2 only thing?

It’s been around as long as I’ve been using OH so at least since 1.6. It’s one of the standard persistence methods on all Items along with maxSince, minSince, previousState, etc. See the “Persistence Extensions in Scripts and Rules” section of the [Persistence wiki page](Persistence Extensions in Scripts and Rules).

If you don’t have persistence setup for the Item it returns null though.

Great - learning every day! Thanks @rlkoshak. Does it work with any persistence, i.e. MapDB, or just historical based stuff like InfluxDB?

It works for any queryable persistence so MapDB yes as well as InfuxDB et al but not with write only persistence services like MQTT or my.openhab.

I primarily use lastUpdate with MapDB myself.

Great stuff - thanks again, and thanks for your very useful tutorials on rule design patterns. I am sure many are gaining a lot of value from these.

1 Like

Thank you for your very helpful post about associated items!
I am now trying to create a generic rule that works for any room in the house but I have one problem:
I want to use a Timer in that rule. You showed how to get an Item by name that had been defined in an Items file before (ie DateTime). But I cannot define a Timer in an Items file nor is it possible to add a Timer to a group programmatically (?).
I do not want to use a single hard coded timer variable that is shared between all rooms because it might be the case that multiple timers are active at the same time.
Any suggestions how I could solve this?

That is correct.

Create a hashMap of Timers using the Item name as the key.

import java.util.Map

val Map<String, Timer> timers = newHashMap

rule "Rule that creates some Timers"
when
    Item MyGroup received update // or whatever
then
    val i = blah blah blah // what ever you do to get the Item

    val Timer t = timers.get(i.name)
    if(t == null) {
        timers.put(i.name, createTimer(now.plusMinutes(1), [|
            // do timer stuff
            timers.put(i.name, null)
        ])
    }
    else {
        timers.get(i.name).reschedule(now.plusMinutes(1))
    }
end
1 Like

I’m currently struggling with a rule. The rule needs to be triggered by any item in a specific group. So the trigger is similar as in your example.

However, in your example, the rule processes all items in that group. I would like my rule only to do stuff with the single item that was triggered.

So how do I identify the item (in group gDoors in your example) that triggered the rule?

Thanks,
Dries

[edit]
I may have found a solution, not sure if it is “best practice”, but initial tests seem to point out it is reliable.

when   
    Item gRaamcontact received update
then
	val LastWindowContact = gRaamcontact.members.sortBy[lastUpdate].last
	logInfo("Window","Last contact =" + LastWindowContact)
end

I’m not sure if it is always reliable when 2 contacts are changed at the same time…

[/edit]

It’s worth looking further in the Tutorials & Example forum section

Depending on the speed of your persistence you man need to add a sleep before the sortby.

It isn’t a best practice so much as the only way to do it in this case. The alternative is one rule per switch which each call a lambda.

Thank you both.

I thought I had read all the rule-tutorials by now, I guess I missed that one.

@rlkoshak: So far I didn’t had any persistence-issues. I guess my mapDB is fast enough. I just added a small sleep just to be sure (100ms). I didn’t want to make it too big, because then the chance of two contacts being changed at the same time will increase.

Hi.
I’ve been working through a few of the Design Pattern articles - they’re helping me get a better understanding of Openhab2. Thank-you for taking the time to write them up.

I have been trying to add the LastUpdate feature to my setup, using this as a guide. I think I’m nearly there, but I get an error at the assocDT.postUpdate(new DateTimeType(door.lastUpdate)) stage;
This gives the output (full detail further below)

2017-07-02 18:40:24.019 [ERROR] [.script.engine.ScriptExecutionThread] - Rule 'A Door's State Changed': Could not invoke constructor: org.eclipse.smarthome.core.library.types.DateTimeType.DateTimeType(java.lang.String)

The code I am using is very similar to the original post in this article (and the associated one on persistence), with a few log lines for debugging and a temporary workaround for Groups following a recent OH update: Groups seem to be broken

2017-07-02 18:32:47.040 [INFO ] [rthome.model.script.associated items] - Door state change rule started
2017-07-02 18:32:47.190 [INFO ] [rthome.model.script.associated items] - dtStr = testDoor_LastUpdate
2017-07-02 18:32:47.215 [INFO ] [rthome.model.script.associated items] - assocDT = testDoor_LastUpdate (Type=DateTimeItem, State=NULL, Label=test Door Last Update, Category=clock, Groups=[gDoorsLastUpdate])
2017-07-02 18:32:47.279 [INFO ] [rthome.model.script.associated items] - door.lastUpdate = 2017-07-02T18:32:46.000+01:00
2017-07-02 18:32:47.330 [ERROR] [.script.engine.ScriptExecutionThread] - Rule 'A Door's State Changed': Could not invoke constructor: org.eclipse.smarthome.core.library.types.DateTimeType.DateTimeType(java.lang.String)

Key snippets below - wondering if anyone can suggest how to fix?;

From .rules

//from: https://community.openhab.org/t/design-pattern-associated-items/15790

val logName = "associated items"

rule "A Door's State Changed"
when
    Item gDoors received update // NOTE: the rule will trigger multiple times per event
then
        logInfo(logName, "Door state change rule started")
        gDoors.members.forEach[door |
        // Get the associated DateTime Item
        val dtStr = door.name + "_LastUpdate"
        val assocDT = gDoorsLastUpdate.members.filter[dt|dt.name == dtStr].head as DateTimeItem
        logInfo(logName, "dtStr = " + dtStr)
        logInfo(logName, "assocDT = " + assocDT )
        logInfo(logName, "door.lastUpdate = "+ door.lastUpdate)

        // Update assocDT with the door's lastUpdate
        assocDT.postUpdate(new DateTimeType(door.lastUpdate))

    ]
end


from .items

//from: https://community.openhab.org/t/design-pattern-associated-items/15790


Group:Contact  gDoors                           // temporary workaround following recent OH update:https://community.openhab.org/t/groups-seem-to-be-broken/29307
Group gDoorsLastUpdate

Contact  testDoor               "test Door"                             <frontdoor>     (gDoors,GarageDoorGroup,gHistory,gNewDoorGroup)   {mqtt="<[mysensorsMQTT:mysensors/in/100/2/1/0/16:state:MAP(PIR.map)]"}
DateTime testDoor_LastUpdate    "test Door Last Update [%1$tm/%1$td %1tH:%1tM]" <clock> (gDoorsLastUpdate)

This gives the output:

2017-07-02 18:40:23.807 [INFO ] [rthome.model.script.associated items] - Door state change rule started
2017-07-02 18:40:23.952 [INFO ] [rthome.model.script.associated items] - dtStr = testDoor_LastUpdate
2017-07-02 18:40:23.970 [INFO ] [rthome.model.script.associated items] - assocDT = testDoor_LastUpdate (Type=DateTimeItem, State=NULL, Label=test Door Last Update, Category=clock, Groups=[gDoorsLastUpdate])
2017-07-02 18:40:23.996 [INFO ] [rthome.model.script.associated items] - door.lastUpdate = 2017-07-02T18:40:23.000+01:00
2017-07-02 18:40:24.019 [ERROR] [.script.engine.ScriptExecutionThread] - Rule 'A Door's State Changed': Could not invoke constructor: org.eclipse.smarthome.core.library.types.DateTimeType.DateTimeType(java.lang.String)

To be honest I understand enough of the syntax for the assocDT.postUpdate to work out what’s causing the error.

Any ideas on how to fix, or dig deeper into the logs?

Thanks

Luke.

lastUpdate returns a Joda DateTime object. You can not update a DateTimeItem with a Joda DateTime object. You either need to create a new DateTimeType using the last update.millis or you can try using door.lastUpdate.toString in your call to postUpdate. I think the default toString is the right format for OH to parse it into a DateTimeType.

1 Like

Thanks the .toString seems to be working

Recently I wondered what more goodies OH had in store to surprise me with.
I think you just showed one. This is totally useful and will apply on many things other than just door updates. Thanks

Great article.

Could you advise how your design proposal could work if I need to use pairs of objects. Like temperature measuring (room_temp) and target temperature (room_target_temp)? if I have 8 rooms, I would like to have a rule iterating every room and comparing corresponding room temperature with target temperature.

thanks.

Just like you describe. Just make sure you can name the Items in such a way that you can easily reconstruct the name of the associated Items using the name of the Item you are iterating over.

So, if you have a Bedroom_Temp name the associated Item Bedroom_Temp_Target and your loop would look something like:

Rooms.members.forEach[room |
    val target = TargetTemps.members.findFirst[room | room.name == room.name + "_Target"]
]

Thank you, Rich, makes perfect sense! as I understand in two loops the name variable “room” should not duplicate, so updated:

Rooms.members.forEach[room |
val target = TargetTemps.members.findFirst[r | room.name == r.name + “_Target”]
]