Design Pattern: Manual Trigger Detection

Edit: Updates for OH 4

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. This DP will show a couple.

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

The second approach creates 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 to something besides the default value and 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.

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 consolidated state of the device. Then there is a Rule that gets triggered when any of those Items change and based on what Item received the command or changes we know the source of the command.

Items

  • Group: holds all the Items so we can trigger the rule with member of triggers
  • Proxy: holds the consolidated state, this Item is used in the UI
  • Device: linked to the Channel to control the device and get updates
  • Rules: used by rules to command the device, rules should use Proxy to determine the state of the device though

Be sure to disable autoupdate on at least the Proxy and Rules Items. If your device reports changes, disable autoupdate on that Item too. This prevents the Item from changing to a predicted state in response to the commands.

Group:Switch LightControls
Switch PorchLight_Proxy (LightControls) { autoupdate="false" } // represents the synchronized state, use on UIs
Switch PorchLight_Device (LightControls) { binding config, autoupdate="false" } // controls the device, updates from device
Switch PorchLight_Rules (LightControls) // control from Rules

Rules

Trigger the rule using members of LightControls received command and another trigger using members of LightControls changed. In the rule we can tell where a command or change came from based on the Item that triggered the rule.

Item Event What happened
Proxy Command Indicates the device was commanded from the UI
Proxy Change Can be ignored, changes should only come from this rule
Device Command Can be ignored, commands to the device should only come from this rule
Device Change Indicates the device has changed state outside of OH (e.g. physically flipped the switch)
Rules Command Indicates a rule commanded the device
Rules Update Should never happen so can be ignored

The rule can therefore determine the source of the event based on the type of the event and the Item that triggered the rule. The rule can then take what ever action necessary based on the source of the event which will usually involve updating the Proxy with the changes from the Device and commanding the Device with commands sent to the Proxy or Rules.

Blockly

First we use Design Pattern: Associated Items to determine the names of all the Items we need and we determine what type of event triggered the rule. If it was a:

  • command to the proxy then command the device
  • update from the device then update the proxy
  • command from the rules then command the device, the proxy will be updated when the device reports it’s changed and the rule runs again

This shows the bare minimum that needs to be done by the rule to keep everything in sync. But you wouldn’t be reading this if you didn’t want to do something different depending on where the event comes from. We will add to this rule a calculation on whether the rule was manually triggered or triggered by automation.

JS Scripting

This is basically a JS implementation of the second version of the Blockly version above.

var basename = event.itemName.split('_')[0];
var proxy = basename+'_Proxy';
var device = basename+'_Device';
var rules = basename+'_Rules';
var isCommand = event.type == 'ItemCommandEvent';

// Calculate whether it's a manual event or a result of automation
var isManual = (( event.itemName == proxy && isCommand ) 
               || ( event.itemName == device && !isCommand ));

// Sync the Item states
if( event.itemName == proxy && isCommand ) {
  log.info('Item commanded from the UI');
  items[device].sendCommand(event.itemCommand.toString());
}
else if( event.itemName == device && !isCommand ) {
  log.info('Item updated from the device');
  items[proxy].postUpdate(event.itemState.toString());
}
else if( event.itemName = rules && isCommand) {
  log.info('Item commanded from a rule');
  items[device].sendCommand(event.itemCommand.toString());
}
else {
  log.info('Rule triggered with an event we can ignore.');
}

if(isManual) {
  // manually triggered
}
else {
  // automation triggered
}

Advanages and Disadvantages

Advantages:

  • Does not depend on timing of events.

Disadvantages:

  • Requires a proliferation of new Items

Approach 2: 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. AUTO) 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.

In the implementations below, the shared cache is used to store the dead man’s switch. All three blow could be implemented using a boolean flag. I show using a String to show that one could determine which rule commanded the Item by setting the String to something specific to the rule and testing for that in the manual detection rule.

Blockly

We use the cache to store the deadman’s switch. Sending a command to an Item from a rule will look like the following where we store in the cache “AUTO” to show the command came from a rule:

Then in the manual detection rule that is triggered by commands to the Item we check the cache to determine how the Item was commanded.

JS Scripting (ECMAScript 11)

We will use the cache to store the deadman’s switch. Sending a command to an Item from a rule will be as follows:

cache.shared.put(itemName, 'AUTO');
items.itemName.sendCommand(command);

The manual detection rule gets triggered by itemName received command. This rule implements the extra steps to take for manual or auto detection and updates the dead man’s switch back to ‘MANUAL’.

if(cache.shared.get(itemName, () => 'MANUAL')) {
  // code for manual commands
}
else {
  cache.shared.put(itemName, 'MANUAL'); // set the flag back to MANUAL
  // code for AUTO commands
}

Any rule that needs to know if an Item was manually commanded or commanded from another rule can check the cache. The function passed in that first get initializes the cache if this is the first time the Item has received a command. If the value in the cache is anything but 'MANUAL' we know that the Item was commanded from a rule. All other commands will be detected as a manual command.

Rules DSL

Same implementation as above using the shared cache. When commanding the Item put “AUTO” into the cache.

sharedCache.put(itemName, "AUTO");
itemName.sendCommand(command);

In the rule that triggers by commands to the Item and care if it was manually triggered:

if(sharedCache.get(itemName, [ | "MANUAL" ]) {
    // manual command
}
else {
    sharedCache.put(itemName, "MANUAL")
    // automated command
}

Advantages and Disadvantages

Advantages:

  • Simple to implement
  • Doesn’t require extra Items
  • Has no timing dependencies

Disadvantages:

  • Does not differentiate between manually interacting with the device and commanding from the UI, it can only distinguish between rules commanding the Item and anything else.

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

Sorry Rich,
Typos…

1 Like

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.

5 Likes

Hi,

I know the post (and the thread) is a little old, but this looks like a very promising solution.
But it seems like 200_JythonExtensionProvider.py is not included in the OH3 Helper libraries anymore, is there still a way to get this working?

Thanks!

For OH 3 you’ll need GitHub - CrazyIvan359/openhab-helper-libraries: JSR223-Jython scripts and modules for use with openHAB. Also pay attention to the imports. Everything in org.eclipse.smarthome has moved to org.openhab in OH 3.

Note that Jython’s existence in OH is likely not going to remain for too much longer. The problem is it’s stuck at version 2.7 which is a couple years past end of life and the upgrade to Python 3 in the upstream project is not progressing. At some point Jython will break and OH will be powerless to make it work.

If you’re going to spend a lot of time on this, it’d probably be worth while to rewrite it in JS Scripting or jRuby or one of the other supported languages.

2 Likes

Hey, thanks for the quick reply.
Yeah these are the helper libraries I use, but they don’t include the mentioned file.

Oh, that’s quite bad news for someone who uses jython for most of his rules :frowning:
Guess I need to have a look at the other options to keep this future proof. Since I have don’t have any exprience in these, I probably shouldn’t start with such complex topic. Thanks for the hint through, guess I’ll try the proxy solution for now :slight_smile:

Pay special attention to the URL there.

Old URL: GitHub - openhab-scripters/openhab-helper-libraries: Scripts and modules for use with openHAB
New URL: GitHub - CrazyIvan359/openhab-helper-libraries: JSR223-Jython scripts and modules for use with openHAB

Make sure you are pulling from CrazyIvan359’s repo, not openhab-scripters’ repo. Also make sure you follow the instructions for installation and configuration of the Jython add-on. There’s no Jython support by default.

It’s also possible that there are other changes made when upgrading the helper library to OH 3 that caused what was in that file to be moved. I couldn’t say.

Another vote for jRuby here, went from DSL to jRuby and loving it