Patterns proposal: item states delay management and items dependencies (cascading)

I was fine tuning the rules that handle audio-video control (switching devices on and off, changing inputs so the entire systems gets ready for TV, music or movie with one single click) and updating the “controller” rule dedicated to the videoprojector when I figured out that, once more, I was implementing the same logic inside a script. So my idea to suggest here 2 patterns.

The patterns rely on virtual items. The logic is to interact with a virtual item that wraps the real actuator / device and that takes care of the transitional states of the hardware device.

First pattern applies to hardware devices that need time to switch from one state to another. Audio video hardware (delay at switch on before they can process an IR command such as video projectors, amplifiers or even TV), computers (wait for Kodi to be ready), blinds, garage door, property gate to name the most common ones.

Applicaton for this first pattern is a video-projector device that accepts ON and OFF commands and has a turn-on/turn-off delays (preheating/cooling steps). This timed behavior is similar to blinds or garage doors would do.
Pattern consists in:

  • A virtual item that wraps the video-projector device:
Number AV_mRDC_Salon_vp	"Projecteur salon [%s]"	<_switch> (AV, mRDC_salon, AV_device) {autoupdate="false"}
  • 2 rules that handle the commands received and updates of the virtual device
var Timer vp_task = null //timer that will take care of device delays
var Integer vp_on_delay = 20 //delay for switching on the device (or getting to the )
var Integer vp_off_delay = 40 // delay for switching off the device
var Integer vp_state_target = null

rule "Projector command (processor)"
when
    Item AV_mRDC_Salon_vp received command
then
//AV_mRDC_Salon_vp states : 0=OFF, 10=Preheating, 90=Shutting-down, 100=0N
//IR_mRDC_Salon_command is the IRTrans item that send IR command to hardware device
    switch(AV_mRDC_Salon_vp.state.toString + ">" + receivedCommand.toString){
        //cases when ON command received
        case "0>100": { 
			vp_state_target = 100 // ON
            AV_mRDC_Salon_vp.postUpdate(10) //Preheating: transitional state
            IR_mRDC_Salon_command.sendCommand("Jvc_vp,poweron") 
            vp_task?.cancel()
            vp_task = createTimer(now.plusSeconds(vp_on_delay)) [|
                AV_mRDC_Salon_vp.postUpdate(100) //ON
                vp_task = null
            ]
        }
        case "90>100": { //Shutting-down>ON: wait for state to change to OFF before sending the ON command
			vp_state_target = 100 // ON
            AV_mRDC_Salon_vp.postUpdate(10) //Preheating: transitional state
        }
        //cases when OFF command received
        case "100>0": { 
			vp_state_target = 0 //OFF
            AV_mRDC_Salon_vp.postUpdate(90) //Shutting-down: transitional state
            IR_mRDC_Salon_command.sendCommand("Jvc_vp,poweroff") 
            vp_task?.cancel()
            vp_task = createTimer(now.plusSeconds(vp_off_delay)) [|
                AV_mRDC_Salon_vp.postUpdate(0) //OFF
                vp_task = null
            ]
        }
        case "10>100": { //Preheating>OFF: wait for state to change to ON before sending the OFF command
			vp_state_target = 0 //OFF
            AV_mRDC_Salon_vp.postUpdate(90) //Shutting-down: transitional state
        }
    }
end

rule "Projector state (reporter)"
when
    Item AV_mRDC_Salon_vp changed
then
    if (AV_mRDC_Salon_vp.state != vp_state_target) { AV_mRDC_Salon_vp.sendCommand(vp_state_target.toString)}
end

The example has 2 states but can be generalized to multistate (for example, having an AV device that accepts commands “OFF”, “HDMI input” and “SPDIF” which wraps the change of source)

Now, What is going on if the projector receives an OFF command while it is heating? Well, mine does not take it into account: you have to wait it is ON before sending an IR command. The wrapping takes care of acting “intelligently” in that case and wait. But if the command is part of a sequence of commands inside a script, then this wait is is an issue as other devices involved should “wait” (or not…) for the projector to be in the expected state to continue the processing. This is where the 2nd pattern comes.

Second pattern creates a kind of dependency between layers of items (real or virtual). In a certain way, this pattern can be seen as a generic extension of Group items behavior where the aggregator would be defined by the user and where the items could be of any type. It helps to manage asynchronicity between commands sent to a device and their actual execution by the device.
It behaves in a complementary way of the Associated items pattern.

As an example for the 2nd pattern, I added :

one virtual item that wraps the projector screen to handle the delay to open/close, similar to the projector
one virtual item that handles the audio-video mode for the room: modes are OFF, TV or projector

Number AV_mRDC_Salon_screen         "Ecran cinéma [%s]"                     <_screen>       (AV, mRDC_salon, AV_device, glAV_mRDC_Salon_videomode ) {autoupdate="false"}
Number AV_mRDC_Salon_vp             "Projecteur salon [%s]"                 <_switch>       (AV, mRDC_salon, AV_device, glAV_mRDC_Salon_videomode) {autoupdate="false"}
Number AV_mRDC_Salon_videomode 		"Mode vidéo [%s]"						<_video>		(mRDC_salon, AV) 	{mqtt=">[cheznous:/openHAB/out/AV_mRDC_Salon_videomode/state:state:*:default],<[cheznous:/openHAB/in/AV_mRDC_Salon_videomode/command:command:default]", autoupdate="false"}
Switch AV_mRDC_Salon_tv 			"TV salon [%s]"						    <_switch>		(AV, mRDC_salon, AV_device, glAV_mRDC_Salon_videomode)  {autoupdate="false"}

Two rules create the “glue” between the devices of the underlying layer:

rule "Command video mode (command processor)"
when
	Item AV_mRDC_Salon_videomode received command
then
	val Integer vptotv_fullvisibility_delay=20 //delay when screen is rolling up when the TV set is fully visible
	val Integer vptotv_irvisibility_delay=5 //delay when the IR receiver of the TV is visible and can accept a command
	val Integer tvtovp_playbackvisibility_delay=25 //delay screen roll-down until projector is ON if playback on-going on TV
//Command processor: forwards commands to underlying items "glued" to virtual item AV_mRDC_Salon_videomode
//"Glued" items are those belonging to group gAV_mRDC_Salon_videomode (see av.items)
//States for AV_mRDC_Salon_videomode: 0=OFF, 1=tv, 2=projector, 3=projector>tv, 4=projector>tv, 5=off>projector, 6=projector>off
	videomode_timer?.cancel
	switch(AV_mRDC_Salon_videomode.state.toString + receivedCommand.toString){
		case '10':{ //tv>off
			AV_mRDC_Salon_tv.sendCommand(OFF)
			AV_mRDC_Salon_vp.sendCommand(0) //OFF: in case underlying devices state would be unsynced with this layer 
			AV_mRDC_Salon_screen.sendCommand(0) //OFF: in case underlying devices state would be unsynced with this layer 
		}
		case '20':{ //projector>off
			AV_mRDC_Salon_tv.sendCommand(OFF) //OFF: in case underlying devices state would be unsynced with this layer 
			AV_mRDC_Salon_screen.sendCommand(0) 
			AV_mRDC_Salon_videomode.postUpdate(6)//projector>off: transitional state until the TV set behind the projector is visible again to switch video from projector to tv
			videomode_timer = createTimer(now.plusSeconds(vptotv_fullvisibility_delay)) [|
				AV_mRDC_Salon_videomode.postUpdate(1)//tv is visible again so AV_mRDC_Salon_videomode is temporarily set to tv 
				AV_mRDC_Salon_videomode.sendCommand(0)// to off
				videomode_timer = null
			]
		}
		case '21':{ //projector>tv
			AV_mRDC_Salon_screen.sendCommand(0) //OFF: in case underlying devices state would be unsynced with this layer 
			AV_mRDC_Salon_videomode.postUpdate(3) ///projector>tv: transitional state until the screen is rolled-up high enough so TV set is fully visible
			videomode_timer = createTimer(now.plusSeconds(vptotv_fullvisibility_delay)) [|
				AV_mRDC_Salon_tv.sendCommand(ON) // TV IR receiver should not be hidden at this point
				AV_mRDC_Salon_vp.sendCommand(0) //OFF: switches projector OFF only when TV set is fully visible 
				videomode_timer = null
			]
		}
		// #### OTHER PROCESSOR CASE{} REMOVED FOR READABILITY ###
	}
end

rule "State video mode (aggregator)"
//custom state aggregator 
//  1- monitor updates on items in group glAV_mRDC_Salon_videomode (those are "glued" with item AV_mRDC_Salon_videomode)
//  2- calculates and updates the resulting state of the overlying virtual item AV_mRDC_Salon_videomode
when
	Member of glAV_mRDC_Salon_videomode received update
then
    logDebug("rules.av_modes.video_state", "Triggerred by " + triggeringItem.name + ", AV_mRDC_Salon_screen=" + AV_mRDC_Salon_screen.state.toString + ", AV_mRDC_Salon_vp=" + AV_mRDC_Salon_vp.state.toString + ", AV_mRDC_Salon_tv=" + AV_mRDC_Salon_tv.state.toString )
    logDebug("rules.av_modes.video_state", AV_mRDC_Salon_screen.state.toString + "#" + AV_mRDC_Salon_vp.state.toString + "#" + AV_mRDC_Salon_tv.state.toString )
    logDebug("rules.av_modes.video_state", (AV_mRDC_Salon_videomode.state != 0).toString )
	if((triggeringItem.name == 'AV_mRDC_Salon_vp') || (triggeringItem.name == 'AV_mRDC_Salon_tv')){
		//handle state changes due to tv or projector items
		switch (AV_mRDC_Salon_screen.state.toString + "#" + AV_mRDC_Salon_vp.state.toString + "#" + AV_mRDC_Salon_tv.state.toString){
			case '0#0#OFF':{ if(AV_mRDC_Salon_videomode.state as Number != 0){ AV_mRDC_Salon_videomode.postUpdate(0) }} // OFF
			case '100#100#OFF':{ if(AV_mRDC_Salon_videomode.state as Number != 2){ AV_mRDC_Salon_videomode.postUpdate(2) }} // projector
			case '100#100#ON':{ if(AV_mRDC_Salon_videomode.state as Number != 2){ AV_mRDC_Salon_videomode.postUpdate(2) }} // projector
			case '0#0#ON':{ if(AV_mRDC_Salon_videomode.state as Number != 1){ AV_mRDC_Salon_videomode.postUpdate(1) }} // tv
		}
	}
	else if(triggeringItem.name == 'AV_mRDC_Salon_screen'){
		//handle transitional states related to screen rollling up / down
		switch (AV_mRDC_Salon_screen.state.toString + "#" + AV_mRDC_Salon_videomode.state.toString){//vp>tv
			case '0#3':{ if(AV_mRDC_Salon_videomode.state as Number != 1){ AV_mRDC_Salon_videomode.postUpdate(1) }}// off#vp_tv: state becomes tv
			case '100#4':{ if(AV_mRDC_Salon_videomode.state as Number != 2){ AV_mRDC_Salon_videomode.postUpdate(2) }}// on#tv_vp: state becomes projector
			case '100#5':{ if(AV_mRDC_Salon_videomode.state as Number != 2){ AV_mRDC_Salon_videomode.postUpdate(2) }}// on#off_vp: state becomes projector
			case '0#6':{  if(AV_mRDC_Salon_videomode.state as Number != 0){ AV_mRDC_Salon_videomode.postUpdate(0) }}////off#vp_off: state becomes off
		}
	}
end

One may think this is much coding to do barely what a group could do.

But when things involve more devices, these 2 patterns are really helpful in layering the processing and making code easier to maintain. For AV, In my real setup, there are 4 layers:

  • first layer wraps each device to handle the on/off and source switching delays (with first pattern)
  • 2nd layer has 2 virtual items: one is dedicated to switch audio mode (2.0, 5.1, zone 2 as my audio setup is quite complicated with several amplifiers and source switching) and “glues” audio devices. The other virtual item switches video mode between (TV or projector) and “glues” the video devices.
  • 3rd layer handles the global AV mode of the room and glues the 2 items of the 2nd layer + Kodi virtual item: when AV_mode is set to TV, audio mode is set to zone 2 and video mode. When AV_mode is set to movie, audio mode is set to multichannel and video mode is set to projector. When AV_mode is set to audiophile, audio_mode is set to 2.0 (using different amplifier), video mode is set to OFF
  • 4th layer handles the entire movie scenery and has 4 states : OFF, ready for movie and movie playing: it glues the AV_mode, the blinds of the room, the lights in the room, the presence mode of the house and the notification subsystem (speech OH notifications are redirected to OSD).

Only virtual items of layers 3 (av_mode) and 4 (start movie scene) are exposed in the UIs so a command takes the entire system to the correct state, whatever the current state was (even a transitional state). And the cascading and delaying makes all the magic happen in a smart way.

This example focused on audio-video but applies to blinds, gate opening when light or other actuators are involved, even virtual (alarm, presence).

Example is in DSL-rule style and I am sorry for that as I know it will be deprecated in the future but the logic of the pattern remains the same whatever the language and my purpose if focused on the architecture of that layering more than the syntax itself (would be nice to store the turn-on/off delay in metadata for example).
Can we imagine a type of “brick” in the NextGen script designer to “wrap” a hardware device with the logic of the first pattern and then “glue” together several devices which states depend on each other? I wish this can happen. Gluing is indeed a bit more complicated than my example as it could precedence management between items with criteria such as "when all / at least one / exactly one item changed to state “AAA”. I believe that such generic patterns can help in simplifying scripting for beginners and provide brick to obtain behaviors which are not many and quite standard at the end regarding home automation.

One could argue that this should be dealt at binding level. For sure, but we are not all capable or willing to write a binding just to drive the ON/OFF state.

Feel free to give your feedback on this, especially if any feature I miss in OH permits obtaining the same result :flushed:

2 Likes

If I understand it correctly (I didn’t fully review all the code) I think a small modification to Design Pattern: Gate Keeper would make layer 1 a little more generic and simpler. The example in the DP has a fixed time between commands but the Python version lets you pass in a different time with each command. It would be a simple matter to add the time to delay before the next command to make it variable to the Rules DSL version.

This would eliminate needing to maintain that switch statement and vp_task in your first Rules.

You should look at 3 different methods to use scenes with Google Home & openHAB for some alternative ways to approach your second layer which might make it a little more generic and maintainable. If you move to Scripted Automation, Design Pattern: Using Item Metadata as an Alternative to Several DPs can come into play which can let you define the scenes and Group membership to define the scenes instead of hard coding them into Rules. See https://github.com/openhab-scripters/openhab-helper-libraries/tree/master/Community/Mode%20(Time%20of%20Day)/automation for an implementation similar to this implemented using Item metadata.

I think you would find that most of us would vehemently assert that something like this should absolutely not be implemented as a binding.

Independent of the comments above, you might look to see if there is anything you can apply from Design Pattern: How to Structure a Rule which might give you some simplification ideas for your second sets of Rules.

Thanks for posting!

The “real” interest of my DP proposal is the 2nd design pattern. It handles precedence of execution among several items whatever the processing time for each item is. For a limited number of items that update instantly or with a fixed delay, the execution is predictible so the Gate Keeper pattern is the right pattern to model this timelined execution.

But when these delays are unknown or variable (affected by other subsystems), or if you have multiple items involved with no control over their states, you cannot model it with the Gate Keeper as you don’t have control over delays. Proposed cascading DP acts :

  • In top-down direction: : when item B receives a command, it triggers commands on dependant items A1...An acccording to the logic of the processor rule
  • In down-up direction: when items A1... An updated, the aggregator rule updates the state of item B
    With this daisy chain, we don’t care about the delays needed to complete the commands of items A1... An and item B will “wait” until conditions defined in the aggregator are met to run.

The first pattern is a placeholder to trigger a state change event after a certain time when the device is not capable of reporting its state change in real time. With this “wrapping”, it can be included in the cascading pattern . But as you pointed out and as I explained above, the Gate Keeper is a more straight-forward solution if the delays are fixed and state changes predictable. My original example was purposely “simplified” which makes it addressable with Gate Keeper. But now consider that I add a HTPC computer that takes a variable time to start to my screen/projector example, then the Gate Keeper DP is not capable to address the startup of the video system.

I suggest you do something I’ve done in most of my DP tutorials. Break it down into simpler parts with an example or examples that are just complex enough to illustrate the concept. Often these examples are not all the practical but they are easy to understand and illustrate the overall approach without a lot of extra details getting in the way.

Then provide a comprehensive example showing an application of the DP to a real world problem. In the comprehensive example use comments to point out those lines/rules/etc that implement the DP.

Ultimately the DP is trying to show a way to issue a command, wait for an event, then issue some more commands. So maybe come up with a toy example that illustrates the concept really well, then show your full implementation without trying to dumb it down to show it in use in a real world situation.

NOTE: I can think of some very elegant ways to implement this in Scripted Automation that might make it worth pursuing. For example, using Item Metatada you can define metadata that defines which Items to wait until they reach which states before commanding another Item.

Switch firstCommand { waitfor="status"[{command=ON, states={ Foo=OFF, Bar=ON }, secondCommand=ON}, {command=OFF, states={Foo=ON,Bar=OFF}, thirdCommand=OFF}] }

I’m totally guessing at some of the syntax above but it’s something like that. The above would tell the Rule that on an ON command to wait for Foo to turn OFF and Bar to turn ON and when that occurs sendCommand ON to secondCommand. When firstCommand receives OFF as a command wait for Foo to turn ON and Bar to turn OFF and issue an OFF command to thirdCommand Item.

The Python Rule would look through all the Items with the waitfor metadata and it can dynamically generate triggers for a Rule to trigger when it )in this case firstCommand receives a command. This Rule will dynamically create two Rules to trigger when Foo receives the right command and Bar receives the right command and a counter that counts up as the Rules get triggered. When the count gets to the number of states being waited on trigger the command to the new Item.

With an approach like that you don’t have to include implementation specifics in the part of the Rule that handles the waiting for the states. And you can string it all together using Item metadata.

I don’t have time to look up all the right syntax right now but I think JSR223 Jython Replacement for Expire Binding has everything from a syntax perspective that’s needed for examples.

Another approach, and perhaps a simpler approach could be accomplished with a simple while loop. Scripted Automation doesn’t have the same limitations as Rules DSL so having long running Rules isn’t so big of a deal. A simple “waitForState” module could be used that loops until the passed in Item reaches the passed in state (returns immediately if it is already in that state, returns with an error if it exceeds the passed in timeout).

Then you don’t need quite so much redirection. One Item receives a command that triggers a Rule. The Rule sends some commands, waits for Items to reach the right state, then continues on running. This would probably be the simplest approach of them all I suspect. I could see this being pretty useful overall. It would make implementing state machines a little easier in some cases.

I’ve already implemented something like this in JavaScript for the work I’ve done on HestiaPi.

  context.waitForUpdateState = function(item, state, maxSecs) {
    var count = 0;
    while(count < maxSecs && (items[item] != state)){
      count++;
      java.lang.Thread.sleep(1000);
    }
  };

This would be the worst thing imaginable in Rules DSL but it is a pretty elegant solution in Scripted Automation. Were I to make it more generic I’d probably sleep in 100 msec increments but the concept would remain the same.

JSR223 is finally running so give me some days (or weeks) before I get all potential benefits from it and start implemeting :crazy_face:

  • [+] it is much faster (but slower than DSL would have been difficult)

  • [+] tags on items and rules

  • [+] more control over rules. I am thinking about a useful item.SendCommandWhen(command, Item received command/changed/updated to, triggeringStrategy) which could be volatile (trigger once only) or persistent until a RemoveCommandWhen() call

  • [-] rules running synchroneously. Is this a joke? So everything will have to be put into threads. See my reply here

Just to be clear, the only limitation is that only one instance of any individual rule can run at a time. You can have as many other different rules running at the same time as you have the memory/processing power to handle. And frankly, when a rule triggers that often where one would have more than one active at a time it is usually a sign that a different approach is requires; it’s a code smell.

When you do have situations where you need to wait for later to continue processing or may want to exit the rule and continue performing some long running task Timers are the recommended approach. Creating your own threads, while possible, would not be the recommended approach.

As for your reply on the other thread, given this is home automation, the ease of amateur and non-developers to create usable rules is way more important than making rules run super efficiently.

I clearly admit that I have always preferred spending far more time to write the code that looks beautiful intelligent (well, that I believe is intelligent) and generic, expandable. Probably because as a hobbyist programmer, I have never been constrained by planning timeline of a “real” project.
I waste terrible time in architecting things (sometimes totally erroneously) instead of writing the straight forward ugly function that just works :upside_down_face: