Design Pattern: Associated Items

groups
Tags: #<Tag:0x00007fadf6cd5090>

(Rich Koshak) #1

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 genetically coded rules where one cannot hard code the names of the associated Items inside the rule mapping between the Items becomes difficult.

Concept

By naming Items that are related to each other such that given the name of one Item the name of the associated Item or Items can be constructed. 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 filter method.

Simple Example

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 if it has been.

This example relies upon the gDoor Items being persisted so we can get the lastUpdate. But that is just part of this particular example. The two lines of code where we “Get associated DateTime Item” are the main part of the example.

Items

Group gDoors:Contact
Group gDoorsLastUpdate:DateTime

Contact Front "Front Door" <frontdoor> (gDoors)
DateTime Front_LastUpdate "Front Door Last Update [%1$tm/%1$td %1tH:%1tM]" <clock> (gDoorsLastUpdate)

Contact Back "Back Door" <frontdoor> (gDoors)
DateTime Back_LastUpdate "Back Door Last Update [%1$tm/%1$td %1tH:%1tM]" <clock> (gDoorsLastUpdate)

Rule:

rule "A Door's State Changed"
    Item gDoors received update // NOTE: the rule will trigger multiple times per event
when
then
    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

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

The rule loops through all the Items in gDoors, pulls each door’s associated LastUpdate Item and updates that Item with the door’s lastUpdate time.

Note that each LastUpdate Item will be updated every time any door receives an update and the rule will trigger more than once for each update. While this results in a lot of unnecessary work the behavior of the rule doesn’t change.


Design Pattern: Working with Groups in Rules
Whole house lighting
How can I build a dynamic Item name for itemValue?
OpenHAB 2.0 Rules: Create list of HSBTypes
2 items in one line
Cancel timer doesn`t work
Use an string item state as a name and state for another item
Suggestion for controlling my underfloor heating, Roth Touchline
Use variable for item
Cron Heating Rules
Alarm Notification "only" for the First OFF
Roku Support
Design Pattern: Gate Keeper
Intergas Incomfort Lan2RF rules
Open the closed Rollershutter after Window opens
Rule optimization: Window OPEN reminder
Monoprice 6-zone Audio amp items, sitemap & rules
Simplifying Rule
Squeezebox Player WIP (Help Appreciated)
Array of type switch
Light switches
Make my rule files shorter --> working with groups
Iterating over a group, want to check an alternate item, sometimes
How to have a history of commands in Basic UI
Setting light themes in rules
Taking A Rule To The Next Level
Design Pattern: Encoding and Accessing Values in Rules
How to store additional info in ITEM?
Type Conversions
Openhab.log goes crazy
Create a custom Item
Looking for optimal datastructure
JSR223 Jython Openhab Imports Erroring?
Match name of similiar items in two different groups
Design Pattern: Human Readable Names in Messages
MQTT device initial state checking
YAGSA - Yet Another Group Structure Approach
Wildcard for Group.members.filter - Use String Patterns and ConfigItems
"First Time" Rule
Fire Rule when groupmember receives command
Best practice for .rules
Persistence query in habpanel
How to create a complex light status check rule?
reelyActive Smart Spaces Revisited
Basic switch questions, clarification about returned value
Design Pattern: Manual Trigger Detection
Notifications in group design pattern
How to model location of an item?
Item or Variable Name with Variable as part of the name
Design Pattern: Cascading Timers
Time and temperature spreadsheet schedule
No working trigger functions openHAB 2.1
Rule not working anymore
Switch with 2 SendCommand actions
Style Guide for openHAB?
MQTT long press design pattern (dimmer)
Alert when item in a group has not been updated for x hours?
How to check if an item exists?
Identify itemname based on part of a name
Unable to get DateTime to work
Make rule with 196 if-statements more generic
Help Needed: sendCommand not working with arrayList of String
Construct Other Item Names from another
Current and Correct Documentation of Syntax for Things and Items Needed
An Approach for Better Light Timers
Automation/Orchestration Design Patterns
Lambda Procedure error: java.lang.NullPointerException
Arrays in Openhab 1.8.3
Set last tripped/alarm date, or last action date to an item - best practice?
Set last tripped/alarm date, or last action date to an item - best practice?
Return to earlier state
Send Modbus Data to File
Delay on item action exec binding
Add a property to a item or type
Individual Alarm clock for each Day of week, with adjustable duration
A More Clever System for "Locking" Lights?
Turning off devices based on Presence
How to make rules easy?
Help with better HVAC rule design
3 different methods to use scenes with Google Home & openHAB
Consolidation help
( ) #2

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


(Ben Jones) #3

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


(Rich Koshak) #4

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.


(Ben Jones) #5

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


(Rich Koshak) #6

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.


(Ben Jones) #7

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.


(Tomme) #8

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?


(Rich Koshak) #9

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

(Dries) #10

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]


(Rossko57) #11

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


(Rich Koshak) #12

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.


(Dries) #13

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.


(Luke Corkill) #14

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.


(Rich Koshak) #15

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.


(Luke Corkill) #16

Thanks the .toString seems to be working


(Kees Van Gelder) #17

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


(rzylius) #18

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.


(Rich Koshak) #19

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

(rzylius) #20

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