Design Pattern: Manual Trigger Detection

Tags: #<Tag:0x00007f61701da8c0>

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.
11 Likes
Fire Rule when groupmember receives command
Design Pattern: Proxy Item
Need help with rule to only execute on manual button press of device
Google Assistant check
Command Source/Client IP
Rule exception for manual override?
Find the device or user which issued the command
Timeout for rule, although Item is updated
Online motion sensor not showing up in log
How to check if my set point item gets updated from rule or device
Official Google Assistant Integration for openHAB
Add sending device to logs
Cloudiness from OpenWeatherMap binding
Roadmap to Happiness - What is missing in the core framework
Insteon Keypad + Phillips Hue
Two items on one thing
Please test the new Expire Binding
IFTTT Alarm integration
What are your top 3 automations
A More Clever System for "Locking" Lights?
Any way to isolate events from UI vs Hardware
How to say if a switch was pressed manually / physically
[SOLVED] KNX Tag on item?
Using autoupdate=false
Little advice with manual trigger detection adaptation
Separating automated switching from user input
Rest API JSON Payload
String Items set using OnOffType ON will always appear to be OFF
Why does a rule with item trigger doesn't process ItemStateEvent
Detecting Usage in HabPanel
OH2 and "group" rules
System Generated Commands vs User Generated Commands (Keeping Track of When User Overrides System)
Proposal: ActivityManager for Consistent Handling of Motion/Triggers and Their Timers
Rule Trigger, when user changed Item, not another rule
[SOLVED] Is event from physical device or openhab
Lightcontrol
Extended Motion Sensor Rule: any Suggestions?
Extended Motion Sensor Rule: any Suggestions?
Collect energy consumption over power-fails / power offs
KNX / LCN Binding

Sorry Rich,
Typos…

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

1 Like

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…

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

.

2 Likes

Hi @gewuerzgurke,

thank you for your solution and your offer to ask a question. I’ve been trying to adapt your item configuration and rule to my setup. However, it does not work.

I receive the following errors in my log:


2019-12-08 00:11:48.118 [ome.event.ItemCommandEvent] - Item 'GF_WardrobeLight_UI' received command ON

2019-12-08 00:11:48.125 [vent.ItemStateChangedEvent] - GF_WardrobeLight_UI changed from OFF to ON

==> /var/log/openhab2/openhab.log <==

2019-12-08 00:11:48.257 [WARN ] [home.model.script.LockingItemTrigger] - source=UI

2019-12-08 00:11:48.265 [WARN ] [home.model.script.LockingItemTrigger] - source=UI

2019-12-08 00:11:48.277 [WARN ] [home.model.script.LockingItemTrigger] - update called on non-Device, ignoring

2019-12-08 00:11:48.289 [WARN ] [home.model.script.LockingItemTrigger] - source=UI, setting LockedUntil, receivedCommand=ON

2019-12-08 00:11:48.300 [WARN ] [home.model.script.LockingItemTrigger] - >> proxy=GF_WardrobeLight_Proxy (Type=SwitchItem, State=OFF, Label=Garderobe Licht, Category=null, Groups=[Licht_Controlled])

==> /var/log/openhab2/events.log <==

2019-12-08 00:11:48.304 [vent.ItemStateChangedEvent] - GF_WardrobeLight_LockedUntil changed from 2019-12-08T08:06:57.470+0100 to 2019-12-08T08:11:48.292+0100

==> /var/log/openhab2/openhab.log <==

2019-12-08 00:11:48.308 [WARN ] [home.model.script.LockingItemTrigger] - >> ui=GF_WardrobeLight_UI (Type=SwitchItem, State=ON, Label=Garderobe Licht, Category=null, Groups=[Licht_Controlled, GF_Wardrobe, gLight])

2019-12-08 00:11:48.319 [WARN ] [home.model.script.LockingItemTrigger] - >> rules=GF_WardrobeLight_Rules (Type=SwitchItem, State=OFF, Label=Garderobe Licht, Category=null, Groups=[Licht_Controlled])

2019-12-08 00:11:48.326 [WARN ] [home.model.script.LockingItemTrigger] - >> device=GF_WardrobeLight_Device (Type=SwitchItem, State=OFF, Label=Garderobe Licht Device, Category=null, Groups=[Licht_Controlled, GF_Wardrobe])

2019-12-08 00:11:48.332 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'Light control received command': An error occurred during the script execution: Could not invoke method: org.eclipse.smarthome.model.script.actions.BusEvent.postUpdate(org.eclipse.smarthome.core.items.Item,java.lang.Number) on instance: null

2019-12-08 00:11:48.382 [WARN ] [home.model.script.LockingItemTrigger] - source=LockedUntil

2019-12-08 00:11:48.391 [WARN ] [home.model.script.LockingItemTrigger] - update called on non-Device, ignoring

This is my item configuration:

Group:Switch    Licht_Controlled
Group           Licht_Locks
Switch          GF_WardrobeLight_UI "Garderobe Licht [%d]" (Licht_Controlled, GF_Wardrobe, gLight)
Switch          GF_WardrobeLight_Proxy "Garderobe Licht [%d]" (Licht_Controlled)
Switch          GF_WardrobeLight_Rules "Garderobe Licht [%d]" (Licht_Controlled)
Switch          GF_WardrobeLight_Device "Garderobe Licht Device [%d]" (Licht_Controlled, GF_Wardrobe) { channel="knx:device:bridge:AKS1:EG_Diele_Licht" }
DateTime	    GF_WardrobeLight_LockedUntil  "Garderobe Licht [%d]" (Licht_Controlled, Licht_Locks)

I did not make any changes to the script you posted but I will put it here anyway. Perhaps I made an error here:

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

Can you spot my error? Best regards, Max

EDIT: I placed the _Device item in my sitemap groups because I wanted to check if the conventional UI switch is working. And yes: That is no problem. Works fine. But when I trigger the UI item I get the error message above.

I’m using this pattern, and extending it with _Override to track when the switch was manually triggered (with an expire), AutoShutoff to time when to auto-shutoff after motion detection, and _Occupancy with expire to estimate if the room is currently occupied (triggered by motion).

I wonder about the downside to having a “proliferation of items”. I’m using mySql persistence, and so far have not seen any performance problems. I feel like I’ll eventually need a script to trim old entries out of the tables, and I’d love to have a way to trim out unused items (since deleted or renamed), but other than that it doesn’t seem to be a particular nuisance. Is there a performance or management concern I’m not thinking about?

1 Like

Consider what you need to persist. More Items aren’t a problem there if you’re not persisting them.
Occupancy, override, shutoff, are these sensible to persist? (They might be)

You can write rules to overcome NULL states at system startup, or use the specialist mapdb to restore efficiently.

That’s kinda where I’m going with this. I’m inclined to be lazy and just persist everything (how to I avoid persisting something, anyway?). My little fanless pc seems to be getting the job done just fine, running OH, mySQL, and a couple of “daemon” type apps. I’m fairly early into this journey, and I’m wondering if I need to be worried about my laziness.

I wasn’t even thinking of Persistence when I wrote that. It is more just the fact that the more Items you have the more Items you have to maintain and I suppose Persistence is part of that. In the future, if you change your approach or change how you do something later on, the more Items you have involved in that thing the more work it is to change later.

I strongly suggest not being lazy about persistence though. It’s not so much because of storage of performance of the database as much as it is related to certain databases and certain strategies are better suited for some tasks (e.g. MapDB is better for restoreOnStartup) than others (e.g. an everyMinute strategy might be better for charting but not a great choice if you need to get the previous state in a Rule). See Design Pattern: Group Based Persistence for a way to use Groups to tag Items with a persistence strategy (e.g. one for restoreOnStartup, one for charting, another for history) and then you can configure the strategy in an appropriate database with an appropriate strategy.

But note that if you have an Item that is a member of multiple such Groups, both strategies will apply so you need to be careful in how you approach persistence. It’s easy to start out being lazy but over time you will find subtleties that need to be managed.

As for the size and resource usage of the database, I wouldn’t worry. It will be years before you have to worry and at that time you can just log in and delete the old stuff manually using SQL (or use one of the many many UI based database admin tools out there to do it). I wouldn’t even worry about creating scripts to do it. My real concern though is if you persist all Items the same way in the same database you will miss out on or make it very challenging to do some things with persistence you may want to do.

HI, after fighting a bit with this (before reading the pattern) I came to a different solution
In my case I need to know if the light was turned on by a rule or by the user (through the switch)
I have a sonoff basic that when receives a cmnd sends a state, but when the user pushes the switch it only sends the state
When the user pushes the button only the state message is sent while when OH sends a command the command and the state messages are sent (by different actors)
So, I created two items for the same bulb:

  • The first one only receives the state (door_switch)
  • The second one only sends the command (door_bulb)
    (I would copy the items here but I’m using paperUI and there is no way to generate the items, and I haven’t learned yet how to write them)

Then this is my code:

var Boolean DoorCommand=false
var Timer DoorTimer = null

rule "door_Sensor"
when
	Item doorSensor changed
then 
		if (doorSensor.state == "false") { // the door is open, open the light
			if (door_bulb.state != ON )){
				door_bulb.sendCommand(ON)
			}
			if (DoorTimer !== null) DoorTimer.cancel
		}
		else { // the door is closed, open the light and close it after a while.
			if (door_bulb.state != ON )
				door_bulb.sendCommand(ON)
			DoorTimer = createTimer(now.plusSeconds(30), [ |
				door_bulb.sendCommand(OFF)
				DoorTimer = null
			] )
		}
end

rule "diirCommand"
when
	Item door_light received command 
then
	DoorCommand=true
end

rule "swith pulsed"
when
	Item door_switch changed 
then
			if (DoorCommand) {
					DoorCommand=false
			}
			else {		
				if (door_switch.state == ON ) {
					DoorTimer = createTimer(now.plusSeconds(15), [ |
					door_bulb.sendCommand(OFF)
					DoorTimer = null
				] ) 
				} else {
				if (DoorTimer!== null) {
						DoorTimer.cancel;
						DoorTimer=null;
					}
				}
			}
end

Screen shots or an export of the JSON from the REST API is usually adequate. In OH 3 there will be a way to generate text representations of Items and Things and Rules and such for pasting into the forum.

If doorSensor represents a boolean state, why not use a Switch or a Contact (Contact is usually more appropriate for a sensor)?

This line could be shortened to

DoorTimer?.cancel

Since you don’t recreate DoorTimer in this Rule after cancelling it, for consistency you should set it to null like you do everywhere else when the timer is cancelled or has run.

I’m looking at the code I’m not sure about:

  • DoorCommand is never used (NOTE, you’ve defined the variable as doorCommand so should be seeing syntax errors in the second rule)
  • DoorCommand just toggles every time the door_switch changes. It doesn’t actually represent that the light was manually triggered, it only represents that the door_switch has changed state, and even then it only represents that fact half the time.

Let’s walk through a scenario.

First, the purpose of this DP is to have some sort of flag or the like to check in order for other rules to determine whether or not a change in a light (in this case) was caused by a rule or by a person. So let’s assume that DoorCommand is intended to be that flag. It should be true when the light was changed by a person and remain OFF the rest of the time.

With the code above, that’s not happening though. Let’s say door_switch is OFF and DoorCommand is false. A person flips the switch and now door_switch becomes ON and the “swith pulsed” rule triggers.

DoorCommand is false so the else clause runs. door_switch is ON so a timer is created to turn off the door_bulb in 15 seconds.

DoorCommand remains false.

Let’s say now door_light receives a command. BTW, what the heck is the purpose of door_light? You don’t mention it. I guess door_light can only be commanded manually? How does a command to door_light propagate to the door_switch or door_bulb Item?

I think you need to explain a bit better what each of these Items do, how they are linked to the devices, and any missing rules here. I’m just not understanding how these rules can work as written nor can I figure out the ultimate goal. I’m also not yet seeing how this is different from the proxy Item approach above, but that could be because of misunderstanding the code.

You will notice how I often have a “theory of operation” section in all of my DPs that explain in prose a narrative the sequence of events and changes that would occur. Perhaps that would make it more clear.

Thanks for your answer!

Let me explain a bit the situation. I have two front doors at home, one after the other. The first one is a big wood door that we close at night while the other one is a “normal” front door. The big door has the open/closed sensor. The space between both doors has a light that we turn on at night while the big door is open (I have some rules to turn it on/off when dark). If we arrive at home at night and the big door is closed when we open the space between doors is dark. So, we want the system to turn the light on; when we close the door we want the light to remain on for a while so we can open the other door.

But sometimes we may want to turn on/off the light manually, so we have a switch (well is the switch that we used before having OH :wink: ) that opens the light for a while (now the rule has some seconds, because is better for testing) or cancels the timer and turns it off.

So the problem is that we need a rule to detect that the user pushed the switch and it was not the system who did that.
First let me apologize for the syntactic errors (I translated the names of the rules I have running on my system to make them clear, and I introduced the typo, which I corrected now)
Second about the doorSensor.state as "false" . I have a sonoff zigbee sensor that sends a json from which I extract the string. I don’t know how to convert it to a switch (I’m still learning, step by step)
The door switch /light is defined as follows (it is in catalan, so door -> porta ; light -> llum ) from the rest…

{
  "editable": true,
  "label": "Porta",
  "bridgeUID": "mqtt:broker:MQTTBroker",
  "configuration": {},
  "properties": {},
  "UID": "mqtt:topic:Porta",
  "thingTypeUID": "mqtt:topic",
  "channels": [
    {
      "linkedItems": [
        "Porta_LLum"
      ],
      "uid": "mqtt:topic:Porta:button",
      "id": "button",
      "channelTypeUID": "mqtt:switch",
      "itemType": "Switch",
      "kind": "STATE",
      "defaultTags": [],
      "properties": {},
      "configuration": {
        "commandTopic": "cmnd/Porta1/POWER",
        "stateTopic": ""
      }
    },
    {
      "linkedItems": [
        "Porta_interr"
      ],
      "uid": "mqtt:topic:Porta:Llum",
      "id": "Llum",
      "channelTypeUID": "mqtt:switch",
      "itemType": "Switch",
      "kind": "STATE",
      "label": "Llum",
      "defaultTags": [],
      "properties": {},
      "configuration": {
        "commandTopic": "",
        "stateTopic": "stat/Porta1/POWER"
      }
    }
  ],
  "location": "Ent"
}

The llum has only command topic while switch has only state topic

How it works:
first lets analyze how Tasmota interacts witht MQTT:
When the user pushes the button; the light is turned on/off by the sonoff that only sends the state message (and the result one ,that we don’t look at )
When from Openhab we want to change the light state: OH will send a MQTT message with the cmnd and get the state message from the sonoff.
About the rules
the rule

rule "portaCommand"
when
	Item Porta_LLum received command 
then
	portaCommand=true
end

It’s only fired when the MQTT cmnd is sent (that is, when the order to change the light status is generated from openhab and not pressing the wall switch)
When the MQTT “cmnd” is sent the device reads the command does the action and sends a new state message that fires the rule triggered by state change

rule "LLum_Interr_porta"
when
	Item Porta_interr changed 
then
			if (portaCommand) {
					logInfo("Interr_porta-changed","Porta_Command previ!")
					portaCommand=false
			}
			else {		

It has a limitation:

  • It works only with a switch attached to the same sonoff that has the relay on it.

Best
Martí

You can chain transforms so use JSONPATH to extract the “false” and then the MQTT binding supports an “on value” and “off value” field where you can put “true” and “false” which will convert those to ON and OFF. Or if using a contact type Channel OPEN and CLOSED.

Alternatively, in tasmota I think you can configure it to just report ON/OFF or OPEN/CLOSED for the sensor value instead of the JSON. I don’t have any tasmota devices any more so can’t confirm that though.

OK, I think that makes it more clear what is going on.

Hi all,
My OH2 rules also need to detect a manual trigger. I was trying to implement it using the approach presented in that pattern. In majority of cases it worked fine. However, I still experienced conditions that the manual trigger wasn’t detected as it suppose to. Additionally, I didn’t like the idea of creating many virtual or proxy items and syncing the states as it overcomlicated my rules.

Just FYI, I’m using Jython scripts.

So, I did a step back and started looking at OH2 internals, how the events are generated etc. I found that the event object already have optional field called ‘source’ that I could use to provide value such as ‘Auto’ and in my rules can simply detect it. However, I found out that the ‘events’ object exposed to the scripts in fact exposes sendCommand and postUpdate methods but without the source argument.

Based on that I implemented that support for python scripts. So I think I’ll share with you my approach and the code. Maybe someone would like to try it and see how it will work for your cases.

Prerequisites

In automation/lib/python/personal folder, place jython_event_bus.py file (Keep in mind it implements only sendCommand at the moment, but it’s easy to add postUpdate method too)

__all__ = ["register_jython_bus"]

from core import osgi
from core.log import logging, LOG_PREFIX
from java.lang import Class

from org.eclipse.smarthome.core.types import TypeParser
from org.eclipse.smarthome.core.items.events import ItemEventFactory

LOG = logging.getLogger("{}.personal.JythonEventsBus".format(LOG_PREFIX))

EventPublisher = osgi.get_service("org.eclipse.smarthome.core.events.EventPublisher")
ItemRegistry = osgi.get_service("org.eclipse.smarthome.core.items.ItemRegistry")

class JythonEventsBus():
    def __init__(self):
        self._ir = ItemRegistry
        self._event_publisher = EventPublisher
        LOG.debug("Created JythonEventsBus")

    def init_item(self, item_or_item_name):
        if isinstance(item_or_item_name, basestring):
            item_name = item_or_item_name
            item = self._ir.getItem(item_or_item_name)
        else:
            item_name = item_or_item_name.name
            item = item_or_item_name
        return item_name, item

    def sendCommand(self, item_or_item_name, command_string, source):
        if self._event_publisher and self._ir:
            try:
                item, item_name = self.init_item(item_or_item_name)
                command = TypeParser.parseCommand(item.getAcceptedCommandTypes(), str(command_string))
                self._event_publisher.post(ItemEventFactory.createCommandEvent(item_name, command, source))
            except:
                import traceback
                LOG.warn(traceback.format_exc())


def register_jython_bus(extension_provider):
    extension_provider.addValue('bus', JythonEventsBus())
    extension_provider.addPreset("default", ['bus'], is_default=True)

Then modify automation/jsr223/core/components/200_JythonExtensionProvider.py component and update scriptLoaded() method as follows

def scriptLoaded(script):
    if core.JythonExtensionProvider is not None: 
        # Register new jython event bus
        from personal.jython_event_bus import register_jython_bus
        register_jython_bus(core.JythonExtensionProvider)

        scriptExtension.addScriptExtensionProvider(core.JythonExtensionProvider)
        logging.getLogger("{}.core.JythonExtensionProvider.scriptLoaded".format(LOG_PREFIX)).debug("Added JythonExtensionProvider")
  • Restart your OH2

Now, you can use new event bus in your rules available via bus object (instead of events) and you can provide the source parameter as follows:

bus.sendCommand(item, command, 'Auto')

You can test it with a simple configuration as below
Assuming following items are defined

Group gLights
Group gMotionSensors

Dimmer Light (gLights)

Switch MotionSensor (gMotionSensors)

Python rules to test it out


@rule("Motion detected")
@when("Member of gMotionSensors changed to ON")
def motion_detected(event):
    bus.sendCommand("Light", 50, "Auto")

@rule("Detect manual trigger")
@when("Member of gLights received command")
def detect_manual(event):
	
	# If Manual/Physical trigger
	if event.source == "Auto":
	    # Send command to physical device using standard event bus, so no source will be set
		events.sendCommand(event.itemName, event.itemCommand)
	else: # If manual trigger
		# Do something if triggered manually, e.g. start timer blocking the motion sensor, etc.

Hope it will help someone solve issues. When I have more time later this week, I’ll try to share with you a more complete solution that works for me.

4 Likes