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.