Edit: Added link to item_init library.
Please see Design Pattern: What is a Design Pattern and How Do I Use Them for how to read and use a DP.
Problem Statement
Often a user has the desire to be able to store and retrieve a series of constants or other values in a Rule. For example, a user may have an RFID reader and want to store the list of authorized and disallowed IDs in a flexible manner. As another example, one may be sending commands to a gateway and need to include the ID of the target actuator as part of the message. Yet another example may be a set of target temperatures for a series of radiators.
Approach 1: Item Metadata
When using Scripting Automation one has access to Item Metadata in Rules. You can statically define metadata on Items or dynamically set and update metadata which is a great way to define constants and other configuration data. See Design Pattern: Using Item Metadata as an Alternative to Several DPs for more details.
There is a Python library, init_items, located at https://github.com/rkoshak/openhab-rules-tools that lets you define Item metadata used to initialize the Item at OH startup or in response to a command to the Item InitItems
which will be automatically created if it doesn’t already exist. For example:
// Set Presence to OFF at boot every time
Switch Presence { init="OFF"[override="True"] }
// Initialize MyLamp to 123,45,67 if it's NULL or UNDEF at start
Color MyLamp { init="123,45,67" }
// Initialize TerhermostSetpoint to 70 but only if it's NULL or UNDEF, remove
// the init metadata after start.
Number ThermostatSetpoint { init="70"[override="False", clear="True"] }
Approach 2: Global Variables
Concept
Create one or more global vals that store the desired information in a way that it is easy to retrieve.
This approach is particularly well suited for situations where
- the data is simple
- the data has a one to one relationship with an associated Item
- the data is static and not likely to change much
Limitations include
- the data is hard coded
- it must be recreated at startup
- it must be kept in sync with Items
Example
Let’s assume we have a number of thermostats and want to have a set of target temps for each one. Each thermostat has its own Item and is a member of the Group Thermostats.
import java.util.Map
val Map<String, Number> presentTargets = newHashMap
val Map<String, Number> awayTargets = newHashMap
// there is a way to initialize the Map statically but I always forget it so prefer a System started Rule
rule "Populate targets"
when
System started
then
presentTargets.put("Room1_Thermostat", 65)
presentTargets.put("Room2_Thermostat", 67)
presentTargets.put("Room2_Thermostat", 66)
awayTargets.put("Room1_Thermostat", 55)
awayTargets.put("Room2_Thermostat", 57)
awayTargets.put("Room2_Thermostat", 56)
end
rule "Presence changed, update target temps"
when
Item Presence changed
then
Thermostats.members.forEach[thermostat |
val tgt = if(Presence.state == ON) presentTargets.get(thermostat.name) else awayTargets.get(thermostat.name)
thermostat.sendCommand(tgt)
]
end
Approach 3: Data Items
Concept
Apply Design Pattern: Associated Items and store the values in Items.
This approach is well suited for situations where
- the encoded values need to be adjusted from the UI
Limitations include
- must have a separate Item for each value which can become unwieldy
- sitemap/HABpanel can become overwhelming if there are a lot of values
Example
Using the same example from above we create a Group to hold the target temps and a new Item for each target temp.
// This rule is only needed during the first run if you are using persistence with restoreOnStartup on the target temp Items
rule "Initialize the target temps"
when
System started
then
// we will initialize them all to the same value
Thermostats.members.forEach[thermostat |
val presentTgt = Targets.findFirst[tgt | tgt.name == thermostat.name + "_PresentTarget"]
val awayTgt = Targets.findFirst[tgt | tgt.name == thermostat.name+"_AwayTarget"]
presentTgt.postUpdate(65)
awayTgt.postUpdate(55)
]
end
rule "Presence changed, update target temps"
when
Item Presence changed
then
Thermostats.members.forEach[thermostat |
val tgtName = thermostat.name + if(Presence.state == ON) "_PresentTarget" else "_AwayTarget"
val target = Targets.findFirst[tgt | tgt.name == tgtName]
thermostat.sendCommand(target.state as Number)
]
end
The key takeaway with this approach is that the System started Rule is required to boot strap the value of the data Items. If you are using restoreOnStartup for those Items, which I recommend, you should remove this Rule after the first run.
Approach 4: Encode Value in Item Name
Concept
Append the value that you care about as part of an Associated Item’s name and parse the value out of the name.
This approach is well suited for situations where
- each piece of data is static but the list of data may need to change (e.g. authorized and unauthorized RFID IDs)
- no system started rule required to boot strap the value, value is coded into the Item’s name so only the Items need to be changed to add, remove, modify the values.
Limitations include
- can only change values by modifying, adding, or removing Items, though this can be seen as an advantage in some circumstances
Example
This example will switch to an RFID reader. We will use Items with the ID encoded in the name to represent each ID card and an Authorized and Unauthorized Group to control access based on the presented ID.
Items
Group:Switch Authorized
Group:Switch Unauthorized
Switch RFID_12345 "Bob's ID" (Authorized) { expire="1m,state=OFF" }
Switch RFID_67890 "Ann's ID" (Authorized) { expire="1m,state=OFF" }
Switch RFID_09876 "Guest ID" (Unauthorized) { expire="1m,state=OFF" }
String RFID { ... } // when the card reader reads a card this Item receives a command with the ID
Rules
rule "RFID triggered"
when
Item RFID received command
then
val itemName = "RFID_"+receivedCommand
val authorized = Authorized.members.findFirst[ id | id.name == itemName ]
val unauthorized = Unauthorized.members.findFirst[ id | id.name == itemName ]
// Purposefully putting unauthorized first so if an ID is in both Groups the unauthorized Group takes precedence
if(unauthorized != null) {
// alert attempted unauthorized access
}
else if(authorized != null) {
// unlock the door
authorized.sendCommand(ON) // we can do something special for individual IDs if desired
}
else {
// alert attempted unkown access
}
end
// We can do something special for certain IDs
rule "Bob came home"
when
Item RFID_12345 received command ON
then
say("Welcome home Bob!")
end
Approach 5: Scripting Automation Configuration
This approach is mainly suitable for Scripting Automation (e.g. JSR223 Python). When using the Helper Libraries there is a file located in automation/lib/<langauge>/configuration.<language extension>
. This file can be imported into your scripts and your modules and makes the perfect place to define data structures like those discussed in Approach 1.
configuration.py
present_targets = {"Room1_Thermostat": 65,
"Room2_Thermostat": 67,
"Room3_Thermostat": 66}
away_targets = {"Room1_Thermostat": 55,
"Room2_Thermostat": 57,
"Room3_Thermostat": 56}
usage:
from core.rules import rule
from core.triggers import when
from configuration import present_targets, away_targets
@rule("Presence changed, update target temps")
@when("Item Presence changed")
def prese_thermo(event):
for thermostat in ir.getItem("Thermostats").members:
tgt = present_targets[thermostat.name] if Presence.state == ON else away_targets[thermostat.name]
events.sendCommand(thermostat, tgt)
Related Design Patterns
Design Pattern | How Used |
---|---|
Design Pattern: Associated Items | Naming of Items in Approach 2 and 3 |
Design Pattern: Unbound Item (aka Virtual Item) | RFID Items in Approach 3 |
Design Pattern: Working with Groups in Rules | Looping through thermostats in Approach 1 and 2, getting the associated Items from a Group in Approach 3 |
Design Pattern: Using Item Metadata as an Alternative to Several DPs | Approach 5 |