Design Pattern: Associated Items

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”]
]

You have it backwards.

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

by the way, on the same note - is there a way to remove letters from the room.name?

like I have Kithchen_Temp and Kitchen_TargetTemp, so I would need to remove from Temp and add TargetTemp. otherwise, I guess I just need to create items with plain room names?

Of course, you can split things apart and recombine them all you want. See https://docs.oracle.com/javase/7/docs/api/java/lang/String.html

However, you will have to weight the pretty significant increase in code complexity for, IMHO, dubious benefit. Given that the names of Items are something that only you see in the code, I would recommend sticking with a naming scheme that makes your code simpler rather than making the names more aesthetically pleasing to the reader.

1 Like

In the first post, the first DSL example have a small error. The trigger line is before the ‘when’ and ‘then’…
I believe it should be:

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

Lights in my home automation setup may be controlled multiple ways: by a physical switch hardwired to the light, by a button on a UI screen, or by a rule in response to some other events. To keep it simple, I combine the design patterns for Proxy Items, for Groups and for Associated Items. I define rules for the desired behavior at the level of a group, and then assign the lights to that group.

With this setup, the proxy item will always correctly reflect the status of the light, independent of what caused that status (command from a rule, gesture on a physical control, gesture on a UI element).

A more detailed description can be found here.

1 Like