Cancel delayed actions vs Cancel Rule Timers

Hub: C8
Platform Version: 2.4.1.167

I have a question about 'cancel delayed actions' vs 'cancel rule timers' (as referred to in the docs).

In the rule machine docs it says:

"Choosing the Cancelable option allows the delay to be cancelled with the Cancel Delayed Actions action elsewhere in the same rule, but this must be done explicitly."

Then it says:
" The Cancel Rule Timers actions will also cancel delayed actions (and can be called on the rule from any rule, not just the same rule), but it affects all rule timers, not just delays. It will also cancel delays with or without the Cancelable? option selected."

There's two things I don't understand about the above:

  1. If an action is delayed then how can a "cancel delayed actions" action execute elsewhere within the same rule while the delay is underway in the same rule? If the rule is executing the delay action, then it's paused until the delay completes, so how can another action in the same rule ever cancel that delay when it can only execute after the delay completes? Is there some way to have multiple threads of actions executing within the same rule that I'm not yet familiar with? If not, it seems like 'cancel delayed actions' can never do anything? (obviously I must not be understanding the docs)

  2. The 'cancel rule timers' action cancels the delay for me, but it goes one further and seems to cancel the entire rule execution, not just the delay action in progress. In the script below, the ELSE that should occur after IF (Variable armPending = false) never executes when the other script sets armPending to false during the delay. Why?

It's a bit difficult to understand the documentation because the vernacular used in the docs isn't exactly the same as the options available. For example, the phrase 'Cancel Timed Actions' is nowhere to be found...so I'm not sure if doing The Right Thing. :man_shrugging:t3:

Help me comprehend this, please! :grin:

My scripts:

Enable alarm:

Required expression:
Variable alarmArmed(false) is = false(T)  AND 
Variable alarmTriggered(false) is = false(T) [TRUE]
Security(off) turns on
Actions:
IF (Back Door, Front Door all motion is inactive(T) [TRUE]) THEN
	Set alarmState to 'arming'
	Set armPending to true
	Notify iPad, iPhone11ProMax: 'Alarm will arm in 30 seconds'
	Delay 0:00:30 (cancelable)
	IF (Variable armPending(false) is = true(F) [FALSE]) THEN
		IF (External Only(on) is on(T) [TRUE]) THEN
			IF (Back Door, Front Door all motion is inactive(T) [TRUE]) THEN
				Set alarmArmed to true
				Set alarmState to 'armed'
				Set armPending to false
				On: ASTAT-Blue-Ext Only
				Notify iPad, iPhone11ProMax: 'Alarm Enabled Ext Only'
			ELSE
				Set alarmState to 'disarmed'
				Set armPending to false
				Off: Security
				Notify iPad, iPhone11ProMax: 'Check Ext Doors, Arming Cancelled'
			END-IF
		ELSE
			IF (Front Door, Back Door, Apt Motion, Apt Door all motion is inactive(T) [TRUE]) THEN
				Set alarmArmed to true
				Set alarmState to 'armed'
				Set armPending to false
				On: ASTAT-Red-Alarm Armed
				Notify iPad, iPhone11ProMax: 'Alarm Enabled'
			ELSE
				Set alarmState to 'disarmed'
				Set armPending to false
				Off: Security
				Notify iPad, iPhone11ProMax: 'Motion Detected, Arming Cancelled'
			END-IF
		END-IF
	ELSE
		Set alarmState to 'disarmed'
		Notify iPad, iPhone11ProMax: 'Arming Cancelled'
	END-IF
ELSE
	Off: Security
	Notify iPad, iPhone11ProMax: 'Check Ext Doors'
END-IF

Alarm Status - Cancel Arm Pending

Required expression:
Variable armPending(false) is = true(F) [FALSE]
Trigger:
Security(off) turns off
Actions:
Set armPending to false
Cancel Timed Actions: Alarm Status - Enable (New)
Set alarmState to 'disarmed'
Notify iPad, iPhone11ProMax: 'Alarm Cancelled'

Unless you build such logic into your rule, nothing stops it from being re-triggered while it's still "running" through the actions. It is common to have a Cancel Delayed Actions as one of the first actions in the rule in rules where you use these kinds of actions for this reason, or sometimes in the other half of an IF/ELSE if the trigger is some binary state (though there are normally other ways to do this).

If you don't need much control over the cancellation and just want it to happen when the rule re-triggers, a "Wait for event: elapsed time" works that way, so I would normally just suggest that instead.

I believe "Cancel Timed Actions" was changed to "Cancel Rule Timers" in a version change a while back, but they are basically the same thing. It's just that it doesn't affect just quite the actions is the former name suggests, as all schedules, including periodic triggers to name a common example, are affected.

For the rules you shared, do you have questions about them? If so, please provide a plain-language description of what you want them to do and any problems you may be experiencing with them.

Are rules like instances of a class spun off into their own thread every time a rule is triggered? If a trigger is repeated several times, does it start up a new 'instance' of the rule, resulting in several rule threads executing the same set of actions...or does re-triggering a rule stop the rule in its tracks and start it from the beginning again?

Yes - my question was buried in item #2 above:

  1. The 'cancel rule timers' action cancels the delay for me, but it goes one further and seems to cancel the entire rule execution, not just the delay action in progress. In the script below, the ELSE that should occur after IF (Variable armPending = false) never executes when the other script sets armPending to false during the delay. Why?

So to put it more plainly, my question is - how do I get the ELSE to execute when I cancel the delay?

I arm the alarm by clicking the "security" button. The 'enable alarm' script runs and get to the 30 second delay:

Delay 0:00:30 (cancelable)

Then I cancel arming the alarm by pressing the 'security' button again (toggling the switch to off), which runs the 'cancel' rule:

Cancel Timed Actions: Alarm Status - Enable (New)

Back to the 'enable alarm' script/actions. The delay has now been cancelled. So I am expecting the next action to run:

	IF (Variable armPending(false) is = true(F) [FALSE]) THEN

That should then fall through to the ELSE (the 2nd to last ELSE in the actions):

	ELSE
		Set alarmState to 'disarmed'
		Notify iPad, iPhone11ProMax: 'Arming Cancelled'
	END-IF

But that ELSE clause never executes. Why?

Another way to ask the question:

Does 'Cancel Rule Timers' cancel only the DELAY action, or does it mean 'Cancel the delay action and exit the rule'?

It should be noted that rules -- or Hubitat apps in general -- are very rarely "running" but rather wake in response to something (an event, a schedule, etc.), do their thing, and go back to sleep. Then, something else may wake them up in the future.

Rules are the same. A "Delay" action will make the rule sleep, then it will wake when that scheduled event is up -- or when something else, like another trigger event, wakes it up before that. Rarely, this can result in simultaneous execution, which can be problematic (and can usually be avoided with careful trigger selection); more commonly, it simply starts another instance of the actions from the top, which may or may not "cancel" anything previously set in place by the actions, depending on how you wrote the rule. For example, as you may have seen in the docs, waits are automatically cancelled. Delays are not unless you do something.

It erases all scheduled jobs you may have created anywhere in the rule, including delays, waits for elapsed time, periodic triggers, and anything else that may have done so. It cancels more than just "Delay" actions. It doesn't "exit" the rule per se (i.e., if it's in the same rule, additional actions will continue on in the list like normal), though if the cancel is the last action in the rule, there's really no difference. And if it's a delay (or wait) that was "cancelled" (no matter how), cancellation means that nothing after the delay is going to run either, as that is what cancellation entails for a delay.

Enabling all logging for a rule is generally a good troubleshooting step and should give you a clearer picture of what is happening.

Maybe you want a timeout on a wait instead and can then check your desired state after that?

1 Like

I believe I don't want that? The goal is a delay before arming an alarm, giving the user an opportunity to cancel arming. Timeout logic would be like: Arm the alarm, now affirm you really want to arm the alarm within 30 seconds or the alarm doesn't arm. I'm doing: Arm the alarm, you have 30 seconds to cancel or the alarm is armed - which is traditional alarm-arming behavior.

Ok. I'll accept that as a fact, but...I do find that quite unintuitive. It means the names of these actions (Cancel Delays, Cancel Rule Timers) are quite imprecisely named. I would think that Cancel Delays (with a delay being checked as cancellable) means exactly that: the delay will be cancelled. Not the actions that follow the delay. Likewise for Cancel Rule Timers. Why not name Cancel Rule Timers "Cancel Rule" if that's effectively what it does? Or perhaps more accurately: "Cancel Rule Timers and All Subsequent Actions"...

Curious: what would the purpose of canceling a delay be if the actions after it ran anyway?

If you want to proceed based on a specific event or a specific amount of time, then a "Wait for event with a timeout would do that. You can test for %device% being "timeout" after the wait to see if the actions continued because of the timeout or (if not equal to this) because of the actual event, as one way you might respond if this difference is important for your automation. I'm not sure what this event is supposed to be in your case, but such an automation should be possible to write.

HSM itself also has a delay option built in if you don't need custom logic for this. You could also create some other automation that disarms HSM (which should cancel arming if that's still what's happening...I think?) in response to whatever event. That might save you from needing to write a complicated rule to manage this logic yourself. And in any case, breaking a large rule into multiple smaller rules generally makes things easier to write and troubleshoot, as another suggestion that may be helpful if the above isn't what you're looking for.

Good luck!

You click the "Security" button to arm the alarm
The 'Alarm Enable' rule begins
You get a notification that the alarm will arm in 30 seconds
var armPending = true, and the 30 second delay begins
You realize you don't have your car keys (actually you do, they're in your pocket) :sweat_smile:
So you click "Security" button again to cancel arming the alarm within the 30 seconds
var armPending = false (because clicking "Security" while armPending = true fired the rule that sets armPending to false and then executed the 'cancel rule timers' action thus canceling the delay)
The delay having now been canceled, as I've shown above repeatedly, the if armPending = true evals to false, thus the ELSE clause executes, which sets alarmState to disarmed and sends a notification.

That's the purpose.

If there's a better way to do it, I'm all ears.

Ok, I understand that logic, but I don't know how to do it...

I think you're saying:

User toggles Security switch on
Alarm Enable starts
alarmState = 'arming'
armPending = true
wait for event: security switch = off timeout 30 seconds

Then you said: " You can test for %device% being "timeout" after the wait"

The logic from there would be straightforward, if timeout proceed with arming the alarm, else disarm and notify.

But precisely how do I do that test %device% being "timeout" that you refer to? Are you saying that a wait action sets a 'timeout' parameter state to true on any device to 'timeout' when a wait times out? I'm searching the docs and not finding how to do this. :frowning:

You can do that, requires defining a local variable, setting it to %device% and then testing that variable against a static "timeout" string. See this section of the documentation. In your case, that probably isn't necessary, you can just test the switch state after the wait.

Here is an example for both ways to do it (of course you would pick one):

With switch staying on beyond timeout:

With switch turning off before timeout expires:

2 Likes

Aaaah!!! :sweat_smile:
Oooohh! :grin:
Ok, now I get it. That's really cool.
THANK YOU!

1 Like

Reading those docs...

%device%: name of the device that generated the most recent matching trigger event, except:
set to "timeout" after a "Wait..." action if a timeout is specified on a wait and the next action is reached because the timeout expires
not necessarily set to any value if the triggering event does not have a device (e.g., location events or periodic triggers)
%value%: the value of the event (e.g., could be "off" for a switch)

I understand that...but...man that's a strange implementation. If I'm reading it correctly, %device% = "name of the device that generated the most recent matching trigger event" (I'm not sure what 'matching' means in that sentence...matching to what?)

But I find it quite unintuitive that %device% gets set to "timeout" when there's also a %value% parameter. Shouldn't the behavior be that %value% gets set to "timeout", and %device% is always the name of the device? Then %device% would be the name of the switch I'm testing with the wait command, and %value% would be timeout (vs on/off)...

Are you one of the devs at Hubitat Inc?

EDIT...hmm, thinking about it more, maybe it was done that way because %value% can be different types (ie, bool string etc) so it was just easier to make %device% = 'timeout' because that's always a string?

No, just a frequent user of the app, familiar with that pattern, and happen to find the RM documentation useful.

You could argue it's equally unintuitive to set %value% when there was no device event to set it from... It doesn't really make any practical difference in the end.

Seems like a good guess to me.

2 Likes

The device that caused the rule to trigger.

If this seems unnecessary, keep in mind that rules can have multiple triggers, including different devices (and sometimes triggers that aren't a device at all). Different users have found varying uses for these features, such as including the device name in some string (e.g., notification text).

Actions can also be run via some means other than triggers, such as "Run Rule Actions" (manually in the UI) or from other rules. It follows from the documentation that these will not set the value of %device%, as there was no trigger event. (Some users expect it to also just be the name of their only triggering device if they write such a rule -- before the rule has actually triggered; it should be apparent that this also won't happen.)

4 Likes

I sincerely appreciate the insights and your knowledge contributions. When something doesn't work the way I expect it to work, even as I'm reading the docs, it's twice as hard for me to figure out. In phase one, I have to let go of my assumptions, and then in phase two, I accept the way it is. :grin:

Really appreciate it. You got me over the hump for sure. I'll incorporate 'wait' going forward.

2 Likes