Edit: Updates for OH 3
Please see Design Pattern: What is a Design Pattern and How Do I Use Them for details on what a DP is and how to use it.
Problem Statement
Sometimes you are in a situation where you want to know where a certain command came from but there is no way built into the binding or the device to tell the difference between a manually initiated command at the device or a command initiated by a rule or the sitemap. For example, when one wants to override a Rule’s behavior when a light switch is manually triggered.
Concept
There are multiple ways to implement this design pattern. This DP will show three of them.
In the first approach, create a deadman’s switch which gets set to one value at all times except when a rule is running. When a rule runs, it temporarily sets the deadman’s switch Item to something besides the default value then changes it back to the default value. A Rule gets triggered for ALL changes to the device’s Item. This rule check’s the deadman’s switch and if it is set to the default value we know the Item was manually triggered. Then pair this with a Rule that triggers on any update to the Items one is watching, check the deadman’s switch and if it is set you know that a Rule has triggered the change.
The second approach is to use the timestamp on some Item which would correspond to a Rule triggering the switch and use that timestamp to determine whether the switch was toggled in response to that event or manually.
The third approach is to use a Proxy Item with separate controlling Items for each way to control the device (sitemap, Rule, device itself). The Rule that synchronizes all the Items will know where the command came from based on the Item that changed.
Approach 1: Deadman’s Switch
In this approach we have a flag, an Item or shared variable accessible by the manual trigger checking rule. Under normal circumstances the flag is set to a default value (e.g. MANUAL
). When a rule that commands the Item runs, it sets the flag so something else (e.g. RULE
) before sending the command and then back to MANUAL
when it’s done. Rules that care how the Item was commanded will check that flag.
JS Scripting (ECMAScript 11)
Many supported rules languages provide the ability to access Item metadata. We can use Item metadata on the monitored Item to hold the flag. The Rules that command the Item will still need to set and unset the metadata.
I’m just going to show the lines of code to set and check the flag here instead of a full rule. No sleeps or other timing is required as setting the metadata is blocking unlike commanding/updating an Item.
items.getItem(event.itemName).upsertMetadataValue('deadmansswitch', 'RULE'); // set the switch at top of rule
items.getItem(event.itemName).upsertMetadataValue('deadmansswitch', 'MANUAL'); // set the switch back at the end of the rule
if(items.getItem(event.itemName).getMetadataValue('deadmansswitch') == 'MANUAL'); // Check if Item was manually commanded
Note, upsert
will update the metadata if it doesn’t exist, or update the value if it does exist. Also note that metadata survives a restart of OH.
Theory of Operation
Any rule that needs to know if an Item was manually commanded or commanded from another rule can check the Item’s metadata. If it’s anything but 'MANUAL'
we know that the Item was commanded from a rule.
Rules DSL Text File Implementation
Note, this approach cannot (yet) be implemented through the UI using Rules DSL.
Items
String DeadMansSwitch { expire="30s,state=MANUAL" }
Group:Switch gWatchItems
// add Items to watch to gWatchItems, change the Group's Type if appropriate
The expire ensures that even if something goes wrong and a rule doesn’t set the switch back to MANUAL
that the Item does return to that state on it’s own.
Rule
rule "A Rule that changes a gWatchItem member"
when
// some trigger
then
DeadMansSwitch.sendCommand("RULE")
Thread::sleep(50) // give it time to populate to the Item registry, may not be needed, may need to be longer on your system
// do stuff that commands a watched Item
Thread::sleep(200) // give "do stuff" time to complete, may not be needed, may need to be longer
DeadMansSwitch.sendCommand("MANUAL")
end
rule "Is Manually Triggered?"
when
Member of gWatchItems received update
then
if(DeadMansSwitch.state.toString == "MANUAL") {
// triggeringItem was manually triggered
}
end
Theory of operation
When a Rule triggers that will cause an update or command to a member of gWatchItems first change the deadman’s switch to “RULE”, wait a bit for the Item’s state to be updated, issue the update or command, wait a bit for the activity to complete and/or the “Is Manually Triggered?” rule to complete, then return deadman’s switch to “MANUAL”.
The rule triggered by gWatchItems gets triggered for all detected changes in the state of any of its members. It checks to see if the deadman’s switch is set to MANUAL. This would be implemented in your rules that care about whether the Item was triggered by a rule or manually.
Advantages and Disadvantages
Advantages:
- Simple to implement, especially with the Expire binding.
- Works very well when an update to one member of gWatchItems results in an override to all Items. Also works well for separately tracking multiple Items, but you’ll need a separate variable for each.
Disadvantages:
- Rules DSL implementation relies on timing which can be unreliable.
- If there are a lot of events occurring simultaneously the wrong Items may be detected as manually triggered thanks to timing issues.
- Does not differentiate between manually interacting with the device and commanding from the UI.
Approach 2: Proxy Items
This approach uses Design Pattern: Proxy Item to determine the source of a change to a device. Each way to control the device will have its own Item with one Proxy Item to represent the “true” state of the device. Then there is a Rule that gets triggered when any of those Items change and based on what Item changed we know the source of the change.
Items
Group:Switch LightControls
Switch HallLight_Proxy (LightControls// represents the synchronized state
Switch HallLight_Device (LightControls) { binding config } // controls the device, updates from device
Switch HallLight_UI (LightControls) // control for the UIs (sitemap, HABPanel, etc)
Switch HallLight_Rules (LightControls) // control for the Rules
Switch PorchLight_Proxy (LightControls// represents the synchronized state
Switch PorchLight_Device (LightControls) { binding config } // controls the device, updates from device
Switch PorchLight_UI (LightControls) // control for the UIs (sitemap, HABPanel, etc)
Switch PorchLight_Rules (LightControls) // control for the Rules
Rules
rule "Light control received command"
when
Member of LightControls received command
then
// Get access to all the relevant Items
val lightName = triggeringItem.name.split("_").get(0)
val source = triggeringItem.name.split("_").get(1)
val proxy = LightControls.members.findFirst[ l | l.name == lightName + "_Proxy" ]
val device = LightControls.members.findFirst[ l | l.name == lightName + "_Device" ]
val ui = LightControls.members.findFirst[ l | l.name == lightName + "_UI" ]
val rules = LightControls.members.findFirst[ l | l.name == lightName + "_Rules" ]
// The Proxy Item should never receive a command
if(source == "Proxy") {
logWarn("light", "Received command on " + triggeringItem.name + ", this should not occur, ignoring.")
return;
}
// When a command comes in from any source not the Device, a command gets sent to the Device
// This let's us skip this command so we don't end up in an infinite loop.
if(source == "Device") {
Thread::sleep(50) // experiment, may not be needed, may need to be longer, give the Item registry time to update the proxy
if(receivedCommand == proxy.state) return;
}
// Detect whether the light was triggered manually or automatically and do what needs to be done
if(source == "Device" || source == "UI") {
// manually controlled
}
else {
// automatically controlled
}
// Forward the new state to those Items that are not already in the new state. Use sendCommand
// for the device so the light actually turns on/off.
if(proxy.state != receivedCommand) proxy.postUpdate(receivedCommand)
if(ui.state != receivedCommand) ui.postUpdate(receivedCommand)
if(rules.state != receivedCommand) rules.postUpdate(receivedCommand)
if(device.state != recevivedCommand) device.sendCommand(receivedCommand)
end
rule "Change the light"
when
// some trigger
then
// some code
HallLight_Rules.sendCommand(ON) // always use the Rules Item to send commands in your Rules
// some code
end
Switch item=HallLight_UI // always use the UI Item on your sitemap
Theory of Operation
There is a separate Item to represent all the different ways one can control the light. And one proxy Item to aggregate the the state of the controlling Items. All the Items are in a Group.
We trigger a Rule when any member of the Group receives a command. First we get a reference to all of the relevant Items.
If the command came from the Proxy we log an error and exit. The Proxy should never receive a command, only updates.
Next we check to see if the source of the command is the Device. If it is we check to see if the receivedCommand is the same as the state of the proxy. If it is we know that this Rule was triggered by the sendCommand to the Device at the end of the Rule and can be ignored.
Now we know this is a command we need to act upon we can determine whether the command came from manual change or an automated change.
Advanages and Disadvantages
Finally, synchronize all of the states for all the related Items using a postUpdate, except for the Device Item which needs to be a commands so the light actually changes if necessary.
Advanages and Disadvantages
Advantages:
- May not depend on timing, or at least not to the same degree as the other two approaches.
Disadvantages:
- Requires a proliferation of new Items
[Deprecated] Approach 3: Timestamp
Deprecated I’ve used this approach for some time and it’s just not reliable enough to remain here. I’ll leave it for now but recommend against using it.
This approach will be a little more concrete by necessity because whether this approach will work depends heavily on the specific environment the Rules work in.
In this case, I’m posting my live Lighting rules. These rules watch for manual updates to light switches so it can override a rule that changes the lights based on weather conditions. It uses the Design Pattern: Associated Items to access an Override Switch. It uses [Deprecated] Design Pattern: Time Of Day to trigger changing the lights based on the time period. Determining whether it is cloudy is implemented using Design Pattern: Separation of Behaviors.
Items
Group:Switch:OR(ON,OFF) gLights_ALL "All Lights" <light>
Group:Switch:OR(ON, OFF) gLights_ON
Group:Switch:OR(ON, OFF) gLights_OFF
Group:Switch:OR(ON, OFF) gLights_ON_MORNING (gLights_ON)
Group:Switch:OR(ON, OFF) gLights_OFF_MORNING (gLights_OFF)
Group:Switch:OR(ON, OFF) gLights_ON_DAY (gLights_ON)
Group:Switch:OR(ON, OFF) gLights_OFF_DAY (gLights_OFF)
Group:Switch:OR(ON, OFF) gLights_ON_AFTERNOON (gLights_ON)
Group:Switch:OR(ON, OFF) gLights_OFF_AFTERNOON (gLights_OFF)
Group:Switch:OR(ON, OFF) gLights_ON_EVENING (gLights_ON)
Group:Switch:OR(ON, OFF) gLights_OFF_EVENING (gLights_OFF)
Group:Switch:OR(ON, OFF) gLights_ON_NIGHT (gLights_ON)
Group:Switch:OR(ON, OFF) gLights_OFF_NIGHT (gLights_OFF)
Group:Switch:OR(ON, OFF) gLights_ON_BED (gLights_ON)
Group:Switch:OR(ON, OFF) gLights_OFF_BED (gLights_OFF)
Group:Switch:OR(ON, OFF) gLights_ON_WEATHER
Group:Switch:OR(ON, OFF) gLights_WEATHER_OVERRIDE
Switch aFrontLamp "Front Room Lamp"
(gLights_ALL, gLights_ON_MORNING, gLights_OFF_DAY, gLights_ON_AFTERNOON, gLights_ON_EVENING, gLights_OFF_NIGHT, gLights_OFF_BED, gLights_ON_WEATHER)
Switch aFamilyLamp "Family Room Lamp"
(gLights_ALL, gLights_ON_MORNING, gLights_OFF_DAY, gLights_ON_AFTERNOON, gLights_ON_EVENING, gLights_OFF_NIGHT, gLights_OFF_BED, gLights_ON_WEATHER)
Switch aPorchLight "Front Porch"
(gLights_ALL, gLights_OFF_MORNING, gLights_OFF_DAY, gLights_ON_AFTERNOON, gLights_ON_EVENING, gLights_OFF_NIGHT, gLights_OFF_BED)
Switch aFamilyLamp_Override (gLights_WEATHER_OVERRIDE)
Switch aFrontLamp_Override (gLights_WEATHER_OVERRIDE)
Switch aPorchLight_Override (gLights_WEATHER_OVERRIDE)
There is an ON Group and an OFF Group for each TimeOfDay time period. Lights are added to an ON Group when they are to be turned ON at the start of that time period and added to the OFF Group when they are to be turned off at the start of that time period.
All the Groups are members of gLights_ON or gLights_OFF so we can use Associated Items to get access to the appropriate Group for a given TimeofDay.
val logName = "lights"
// Theory of operation: Turn off the light that are members of gLights_OFF_<TOD> and
// then turn on the lights that are members of gLights_ON_<TOD>. Reset the overrides.
rule "Set lights based on Time of Day"
when
Item vTimeOfDay changed
then
// reset overrides
gLights_WEATHER_OVERRIDE.postUpdate(OFF)
val offGroupName = "gLights_OFF_"+vTimeOfDay.state.toString
val onGroupName = "gLights_ON_"+vTimeOfDay.state.toString
logInfo(logName, "Turning off lights for " + offGroupName)
val GroupItem offItems = gLights_OFF.members.filter[ g | g.name == offGroupName ].head as GroupItem
offItems.members.filter[ l | l.state != OFF ].forEach[ SwitchItem l | l.sendCommand(OFF) ]
logInfo(logName, "Turning on lights for " + onGroupName)
val GroupItem onItems = gLights_ON.members.filter[ g| g.name == onGroupName ].head as GroupItem
onItems.members.filter[ l | l.state != ON].forEach[ SwitchItem l | l.sendCommand(ON) ]
end
// 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" // does not work prior to 2.3 Release
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(logName, "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 = gLights_WEATHER_OVERRIDE.members.findFirst[ o | o.name == overrideName ]
if(override.state != ON && l.state != vIsCloudy.state) l.sendCommand(vIsCloudy.state as OnOffType)
if(override.state == ON) logInfo(logName, l.name + " is overridden")
]
end
// Theory of operation: any change in the relevant lights that occur more than five seconds after
// the change to DAY or after a change caused by cloudy is an override
rule "Watch for overrides"
when
Member of gLights_ON_DAY changed
then
// wait a second before reacting after vTimeOfDay changes, ignore all other times of day
if(vTimeOfDay.state != "DAY" || vTimeOfDay.lastUpdate("mapdb").isAfter(now.minusMinutes(1).millis)) return;
// Assume any change to a light that occurs more than n seconds after time of day or cloudy is a manual override
val n = 5
val causedByClouds = vIsCloudy.lastUpdate("mapdb").isAfter(now.minusSeconds(n).millis)
val causedByTime = vTimeOfDay.lastUpdate("mapdb").isAfter(now.minusSeconds(n).millis)
if(!causedByClouds && !causedByTime) {
logInfo(logName, "Manual light trigger detected, overriding cloudy control for " + triggeringItem.name)
postUpdate(triggeringItem.name+"_Override", "ON")
}
end
Theory of Operation
When vTimeOfDay changes state we reset all of the Overrides. Then we use Associated Items to get the OFF and ON Group for the current TimeOfDay. Then it loops through all the OFF Group’s Items and turns them OFF and loops through all the ON Group’s Items and turns them ON. We don’t just sendCommand to the Group to avoid sending a command to a light that is already in that state.
Next there is a rule that turns on the lights that are a member of gLights_ON_WEATHER on when it is cloudy. The item vIsCloudy is calculated elsewhere and illustrates Design Pattern: Separation of Behaviors. If it is DAY time and it is cloudy, all members of gLights_ON_WEATHER that have not been overridden are turned on.
The Associated Items design pattern is used to check the Override Item associated with a given light. If the light is not overridden it is turned ON or OFF depending on the cloudy state.
The final rule is where the detection between a manual or Rule based change takes place.
In this Rule it checks to see if the current time of day is DAY (since we only worry about the manual detection during the day time). It also checks that vTimeOfDay didn’t just change to DAY because we do not want the change to DAY to override the lights, just changes to vIsCloudy.
Now the Rule checks the timestamp on the last update for vIsCloudy and vTimeOfDay. If vIsCloudy and vTimeOfDay both received an update less than half a second ago we know that the Light was updated because of a Rule. If these Items were last updated longer than half a second ago we assume that the light was updated manually.
Advanages and Disadvantages
Advantages:
- Sometimes an approach like this is the only way to distinguish between manual and Rules based updates to an Item
- Still relies on timing but doesn’t require sleeps
Disadvantages:
- Still relies on timing
Related Design Patterns
Design Pattern | How It’s Used |
---|---|
Design Pattern: Expire Binding Based Timers | Approach 1 uses Expire to reset the dead man’s switch |
[Deprecated] Design Pattern: Time Of Day | The Dead Man’s Switch is an implementation of a simple State Machine, uses the TOD example in Approach 2 |
Design Pattern: Working with Groups in Rules | Approach 2 uses a several Group operations |
Design Pattern: Separation of Behaviors | Approach 2 uses this for calculating whether it is cloudy |
Design Pattern: Associated Items | Used to access the Override switches in Approach 2 and accessing the controlling Items in Approach 3 |
Design Pattern: Proxy Item | The many proxy Items used in all three approaches. |