Design Pattern: Manual Trigger Detection

designpattern
Tags: #<Tag:0x00007f0e9e4c9498>

(Rich Koshak) #1

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

image

There are multiple ways to implement this design pattern.

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

Item

String DeadMansSwitch { expire="30s,state=MANUAL" } // if something goes wrong, make sure the switch falls back to a default state
Group:Switch gWatchItems
// add Items to watch to gWatchItems, change the Group's Type if appropriate

Rule

rule "System Started"
when
    System started
then
    DeadMansSwitch.sendCommand("STARTUP")
end

rule "Rule that changes a gWatchItem"
when
    // some trigger
then
    DeadMansSwitch.sendCommand("RULE")
    Thread::sleep(50) // give it time to populate tto the Item registry, may not be needed, may need to be longer
    // do stuff
    Thread::sleep(200) // give stuff time to complete, may not be needed, may need to be longer
    DeadMansSwitch.sendCommand("MANUAL")
end

// more rules which trigger members of gWatchItems

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 OH starts up the deadman’s switch gets set to “STARTUP”. After 30 seconds the deadman’s switch will get set to “MANUAL” by the Expire binding.

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.

Advanages 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.

Disadvantages:

  • Relies on timing which can be unreliable.
  • If there are a lot of events occuring simultaneously the wrong Items may be detected as manually triggered.

Approach 2: Timestamp

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

Approach 3: 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

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
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.

Fire Rule when groupmember receives command
Design Pattern: Proxy Item
Need help with rule to only execute on manual button press of device
Rule exception for manual override?
Command Source/Client IP
Two items on one thing
How to say if a switch was pressed manually / physically
Any way to isolate events from UI vs Hardware
KNX Tag on item?
A More Clever System for "Locking" Lights?
What are your top 3 automations
IFTTT Alarm integration
Please test the new Expire Binding
Insteon Keypad + Phillips Hue
(Vincent Regaud) #2

Sorry Rich,
Typos…


(Rich Koshak) #3

Thanks. My spell checker isn’t working on my machine right now and I’m a terrible eidtor.


(Vincent Regaud) #4

Me too and my typing skills are terrible. I sometimes have to edit a post several times before I get it right event with reading it 10 times…


(Alexander Mueller) #5

Hi guys,

Thanks a lot for the nice explanation on this topic. I’ve used the given Proxy-Item-Approach and adopted it to my needs:

  • I want to be able to have lights/dimmers/rollershutters as part of some rules which are fired event or time-based
  • As soon as someone manually makes use of some control I want a lock on this very control (for n-hours)
  • To give you an example: Based on a time-of-the-day rule I switch my garden lights ON/OFF. If I sit on the balcony and want the garden lights to be ON I could control this manually but the light rule would override this state (=poor WAF). With this addition I lock the control for 8h
Group:Switch 	Licht_Controlled
Group		Licht_Locks
Switch		Garten_Licht1_UI	"Garten_Licht1 [%d]"			(Licht_Controlled,Licht_Garten)
Switch 		Garten_Licht1_Proxy 	"Garten_Licht1 [%d]"			(Licht_Controlled)									
Switch 		Garten_Licht1_Rules 	"Garten_Licht1 [%d]"			(Licht_Controlled)									
Switch 		Garten_Licht1_Device 	"Garten_Licht1 [%d]"			(Licht_Controlled)								{ channel="homematic:HM-LC-Sw4-SM:3c988ba3:OEQ0215754:1#STATE" }
DateTime	Garten_Licht1_LockedUntil					(Licht_Controlled,Licht_Locks)

Licht_Controlled is the group that can be used for this pattern. Licht_Locks is the group to hold all LockedUntil-states. Licht_Garten is just something to collect all controls for displaying, can be ignored.

rule "Light control received command"
when
    Member of Licht_Controlled received command or
	Member of Licht_Controlled received update
then

    // Names must follow this convention: <Place>_<Control>_<LockingVar> where <Place> could be Garden/EG/OG, <Control> could be Light and for LockingVar there must be Proxy,UI,Rules,Device and LockedUntil
    val lightName = triggeringItem.name.split("_").get(0) + "_" + triggeringItem.name.split("_").get(1)
    val source = triggeringItem.name.split("_").get(2)
	
    // Get Items
    val proxy = Licht_Controlled.members.findFirst[ l | l.name == lightName + "_Proxy" ]
    val device = Licht_Controlled.members.findFirst[ l | l.name == lightName + "_Device" ]
    val ui = Licht_Controlled.members.findFirst[ l | l.name == lightName + "_UI" ]
    val rules = Licht_Controlled.members.findFirst[ l | l.name == lightName + "_Rules" ]
    val locked = Licht_Controlled.members.findFirst[ l | l.name == lightName + "_LockedUntil" ]
	// Log Source
    logWarn("LockingItemTrigger","source="+source)

	if((receivedCommand == null || receivedCommand === NULL) && source != "Device") {
		logWarn("LockingItemTrigger","update called on non-Device, ignoring")
		return;
	}
	
    // The Proxy Item should never receive a command and we ignore commands for LockedUntil
    if(source == "Proxy" || source == "LockedUntil") {
        logWarn("LockingItemTrigger", "Received command on " + triggeringItem.name + ", ignoring.")
        return;
    }
	
	// We want to send receivedCommand to all states, in case we won't accept the input we can override it
    val postUpdateCommand = receivedCommand
	
    // 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.
	// For HM bridge it seems that only updates are performaned while manually pressing switches/etc. 
    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(device.state == proxy.state) {
			logWarn("LockingItemTrigger","proxy state equal to device state, finish")
			return;
		}else{
			// Ensure that we set all other representations to manual triggered value
			postUpdateCommand = device.state
			logWarn("LockingItemTrigger","postUpdateCommand="+device.state)
		}
    }

    // Set 8h lock if pressed manually or via UI
    if(source == "Device" || source == "UI") {
		logWarn("LockingItemTrigger","source="+source+", setting LockedUntil, receivedCommand="+receivedCommand)
        // lock control for 8h regarding automatic rule execution
        locked.postUpdate(now.plusHours(8).toString)
    } else {
        if(locked.state !== NULL && now.isBefore((locked.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli) ) {
			// We'll reset all representations to old state, as the item is locked
			postUpdateCommand = proxy.state 
			logWarn("LockingItemTrigger","" + lightName + " locked until " + locked.state.toString + " - ignoring sendCommand request")
		}
    }

    logWarn("LockingItemTrigger",">> proxy=" + proxy)
    logWarn("LockingItemTrigger",">> ui=" + ui)
    logWarn("LockingItemTrigger",">> rules=" + rules)
    logWarn("LockingItemTrigger",">> device=" + device)

    // 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 != postUpdateCommand) proxy.postUpdate(postUpdateCommand)
    if(ui.state != postUpdateCommand) ui.postUpdate(postUpdateCommand)
    if(rules.state != postUpdateCommand) rules.postUpdate(postUpdateCommand)
    if(device.state != postUpdateCommand) device.sendCommand(postUpdateCommand)
end

For some reason a manual trigger for Homematic-controls can only be observed by “received update” method.

Let me know if you have any questions on this. (Code quality is somehow still in a PoC mode :slight_smile: )

Best Regards
Alex

.