"Stays away from" and "Stays any of" triggers unexpected behavior


I can suggest a failure mechanism:

The stays doesn’t know the door is locked initially because the piston is still processing an event saying it is unlocked and that is always checked first.

The stays returns false and also schedules a wake up because that is how it works. The door locked event that comes along never reaches the stays trigger so it doesn’t cancel that wake up. It wakes up, returns true and sends the e-mail.

The problem I have is why your fix works. If you simply added a long wait after the lock with TCP left alone I’d understand it as the locked event would cause it to be cancelled and the stays would never be reached. However with TCP set to Never Cancel I don’t know.


Maybe this falls into the observations that @WCmore has made : stays unchanged


Yes, the lock event seems to take 3-4 seconds to process so when the “stays” trigger is first encountered it’s always still unlocked, but by the time 30 seconds have expired and the subscribed event is resolved the lock has long since reported the “locked” event. At that point the “stays unlocked” condition should fail, or so I would think. Do the stay conditions not process any events that occur during the period from when the trigger starts to when it is resolved? That seems not a little counterintuitive, and if so it makes me question what utility “stays” triggers have.

The reason the wait workaround works, as I understand it, is because by the time the wait event expires the lock has already reported back that it’s locked, so both when the “stays” trigger starts and when it ends the condition on the stays trigger evaluates to false.


Is this a known (if undocumented) issue then? I’m very new to webcore and tried to search for a breakdown of how the different triggers and conditions are evaluated, but didn’t come up with anything other than a broad description of how triggers are different from conditions. While informative, that doesn’t help me determine how the inputs to different types of triggers are processed or when the time period starts/ends. Perhaps them being half broken is why the documentation is a bit lacking…


Consider a simple condition like Door contact is open. If you run a piston containing that condition using the Test button it will look at the current value of the Door contact attribute to see if it is open. If, however, the piston has fired because of a Door contact event, the piston will look at the event value instead. The values can be different.

The stays condition is an unusual one. It is classed as a trigger and that means the piston subscribes to the device attribute it uses. The actual comparisons are like conditions though. A trigger condition can normally only evaluate to true if the piston is currently processing the trigger event, but stays doesn’t work like that.

Considering stays away from locked for 30 seconds, the piston starts by looking to see if the lock status is locked. If it is (bearing in mind the value may come from the event) the comparison returns false, job done.

If the lock status is not locked, the piston sets a timer for 30 seconds time but also immediately returns false and continues.

After 30 seconds the piston wakes up, and unless it has been cancelled, continues where it left off, returning true. This is why it sometimes gets called a timed trigger.

In order to stop the piston returning true, an event has to occur in those thirty seconds to make it run again, which in your case is the locked event. The first ‘if’ group will now return false and cancel any scheduled events set up last time around when it was true. However it would appear that the stays timer doesn’t count as a scheduled task and that the stays trigger actually needs to be evaluated again for it to see the lock status has changed.

That’s how I see things but there are bits where I am really fuzzy about the details. For instance a long wait, implemented by an exit and a wake up, doesn’t seem to turn the piston’s idea of the current event into a timer, it seems to leave it alone.


So if I’m understanding you correctly the stays trigger only works properly if the event it’s waiting for is able to invoke a new instance of the piston and get to the point in the code where the stays trigger is located, otherwise the value it’s watching will always be evaluated the same as it was when the trigger timer was started?


I think you probably are. I’m not saying I’m correct but that is what I think happens. It is similar in principle to the TCP cancelling scheduled tasks. It is all down to the piston executing a new instance and conditions giving different results to previously.


I’ve just made the simplest test I can think of to check this, and it seems you’ve nailed it. I made a test that will respond to the door lock being unlocked by immediately locking it then waiting for 30 seconds to see if the lock status is changed to locked. If it fails to lock, it then turns off a light as an indicator. The following works as expected, where the light stays on if the lock locks and turns off if it fails to lock for any reason:

The following does not, and the only difference is that now the initial trigger does not include the locked state, which makes more sense on its face but prevents instances of the piston started due to “locked” events from progressing to the “stays” condition. No matter what happens, this one will always turn off the lights:

So the only way the “stays” triggers will work as expected is if you ensure that all potential events that can occur during the wait period will initiate a new instance of the piston, while also meeting all conditions to progress to the “stays” statement, otherwise the events are never “seen” by the original piston instance. This seems to severely limit the utility of the “stays” statement, such that it is difficult to incorporate properly unless it’s the very first condition statement hit in a piston.

This being the case, I don’t see any way to do what I want outside of defining a global variable and having a separate piston with the global variable acting as a restriction which just locks the door then has the “stays” trigger within there, resetting the global variable afterward. Does that sound about right, or might there be a more straightforward approach to watching for whether a lock successfully locks?




If you have an HE system, I have fixes for “stays*” that I’m looking for a tester.

It passes my initial tests.


I guess I forgot to mention that, but yes it is an HE system. I’m willing to give it a shot, where can I go about getting the changes?


The HE repo is updated with these changes.

If you use HPM on HE, you will need to do a repair, as I have not updated the HPM config file (ie I’m not forcing it as an advertised update to everyone.)


I updated it from github directly since I’m not using HPM, but the behavior doesn’t seem to have changed. I ran the following as a test:

It still always turns off the lights regardless of whether the lock action is successful or not. Am I misunderstanding what was changed? Here is the log:

1/22/2021, 12:23:41 PM +962ms
+4ms 	╔Received event [Redacted's House].time = 1611347021523 with a delay of 439ms, canQueue: true, calledMyself: false
+24ms 	║RunTime initialize > 23 LockT > 1ms > rtDT > 2ms > pistonT > 1ms (first state access 20 6 17)
+26ms 	║Runtime (5860 bytes) successfully initialized in 2ms (v0.3.110.20210122_HE)
+30ms 	║╔Execution stage started
+36ms 	║║Cancelling condition #6's schedules...
+37ms 	║║Condition #6 evaluated true (1ms)
+38ms 	║║Cancelling condition #5's schedules...
+40ms 	║║Condition group #5 evaluated true (state changed) (5ms)
+41ms 	║║Cancelling statement #7's schedules...
+64ms 	║║Executed physical command [Living Room Lights].setLevel([0], ) (16ms)
+65ms 	║║Executed [Living Room Lights].setLevel (19ms)
+71ms 	║╚Execution stage complete. (41ms)
+74ms 	╚Event processed successfully (72ms)
1/22/2021, 12:23:13 PM +851ms
+2ms 	╔Received event [Front Door Lock].lock = locked with a delay of 42ms, canQueue: true, calledMyself: false
+29ms 	║RunTime initialize > 28 LockT > 1ms > rtDT > 1ms > pistonT > 0ms (first state access 26 4 24)
+32ms 	║Runtime (5846 bytes) successfully initialized in 1ms (v0.3.110.20210122_HE)
+33ms 	║╔Execution stage started
+43ms 	║║Comparison (enum) locked is (string) unlocked = false (2ms)
+45ms 	║║Cancelling condition #2's schedules...
+46ms 	║║Condition #2 evaluated false (10ms)
+48ms 	║║Cancelling condition #1's schedules...
+49ms 	║║Condition group #1 evaluated false (state changed) (13ms)
+54ms 	║╚Execution stage complete. (22ms)
+146ms 	║Setting up scheduled job for Fri, Jan 22 2021 @ 12:23:41 PM PST (in 28s)
+148ms 	╚Event processed successfully (147ms)
1/22/2021, 12:23:11 PM +314ms
+9ms 	╔Received event [Front Door Lock].lock = unlocked with a delay of 72ms, canQueue: true, calledMyself: false
+25ms 	║RunTime initialize > 23 LockT > 2ms > rtDT > 2ms > pistonT > 1ms (first state access 19 11 12)
+28ms 	║Runtime (5797 bytes) successfully initialized in 2ms (v0.3.110.20210122_HE)
+29ms 	║╔Execution stage started
+38ms 	║║Comparison (enum) unlocked is (string) unlocked = true (1ms)
+40ms 	║║Cancelling condition #2's schedules...
+47ms 	║║Condition #2 evaluated true (15ms)
+49ms 	║║Cancelling condition #1's schedules...
+51ms 	║║Condition group #1 evaluated true (state changed) (18ms)
+52ms 	║║Cancelling statement #3's schedules...
+83ms 	║║Executed physical command [Front Door Lock].lock() (27ms)
+85ms 	║║Executed [Front Door Lock].lock (29ms)
+205ms 	║║Comparison (enum) unlocked stays_away_from (string) locked = false (112ms)
+209ms 	║║Adding a timed trigger schedule for condition 6
+211ms 	║║Cancelling condition #6's schedules...
+213ms 	║║Condition #6 evaluated false (124ms)
+215ms 	║║Cancelling condition #5's schedules...
+216ms 	║║Condition group #5 evaluated false (state changed) (128ms)
+223ms 	║╚Execution stage complete. (193ms)
+257ms 	║Setting up scheduled job for Fri, Jan 22 2021 @ 12:23:41 PM PST (in 30s)
+260ms 	╚Event processed successfully (258ms)


So things look right to me.

You nested the check for ‘stays away from’. I get the impression you don’t want it nested.

In general nesting triggers is a bad idea, as the code for the trigger may not run due to the nesting.

I otherwise see the right scheduling, and execution.


Oh ok, I just misunderstood then. What was it that these changes fix then?


I suspect you might be better off not using stays away from locked in your case. You might find you can replace it with

   if Lock 1 is unlocked
        wait 30 seconds
        <take whatever action you want>

Then if the locked event does arrive the cancellation of scheduled tasks will cascade down and stop the action task happening.


I had already considered that, and it handles the cases where the lock is completely unresponsive just fine. However, in cases where the lock is physically obstructed the status changes to “unknown” and the wait is canceled all the same. Is there a method for continuing execution at some point after the wait event when the wait is canceled, or does it just exit the piston completely in all cases when TCP is set to condition change? I do have a piston that works appropriately with both the stays event and using the wait as you suggest (plus a bit more to handle unknown cases), it’s just a bit more convoluted than I had expected for so simple a task.


It may be that you need to handle ‘unknown’ as a special case and that what seems convoluted is the best solution.


Just as reference, I added more of a writeup on

‘did not change’

vs. various forms of ‘stays’



That’s very helpful actually, I just wish this kind of stuff made it into the official documentation. With the number of caveats to the scheduled triggers there should be big flashing letters warning you about them in the UI itself, but I couldn’t really find a comprehensive breakdown of its operation even when I was looking for it. I still don’t understand the utility of it executing either conditional branch every single time the condition is hit while it’s still waiting, but knowing that it does helps.