Sync A Group Of Dimmer Switches


#1

I FINALLY have a working, simple, and generic piston that will sync the switch state and levels of a specified group of dimmer switches without causing the ‘ping-pong’ event looping:

Almost every other variation that I tried caused event loops. I attempted to use locks, waits, global timestamps, etc. with little luck and much frustration. I put this together as an extremely simple example of how to reproduce my problem and it actually ended up working.

However, I would still like to know if there is a way to explicitly trigger a device event programmatically without causing any pistons listening for that event to trigger. Simply put, I’d like to be able to specify in a piston whether the event triggered should or should not cascade.

I suspect the reason that this example works is because I’m triggering the events as a group rather than looping through each device individually as I have done in other variations. I can’t be sure, and don’t say that authoritatively.


#2

temp

Someone please correct me if I am wrong, but every conversation I have seen about Physical / Programmatic interaction, concludes that it is unreliable in it’s current state. (blame the devices though, not webCoRE)

Sooo, as a thinking outside the box kind of guy, I would be tempted to use a simulated switch as a sort of a “global variable”. My logic is: simulated switches happen nearly instantaneously, versus the globals which have a delay before being written.

There are a few ways we can approach this, but the underlying concept is: when we want a command to go thru without interference, the piston first flips the simulated switch on (let’s call it SS)… then it sends the actual command… then pauses a few seconds… then flips the SS back off.

The only other requirements would be to edit any pistons that are monitoring that device, and add one line of code so they only trigger when the SS is off.

I hope a better way comes to light, but I believe my technique would work in the meantime.

I do this in a round about way on many of my pistons so certain sections of code only run under certain conditions (state of simulated switches).


#3

First of all, I want to make it clear that I’m not necessarily talking about the difference between physical triggers and programmatic triggers. Sometimes you want the latter to propagate events too, so distinguishing between the two is not sufficient.

@WCmore Thanks for donating your brain. If I understand your reasoning, the idea is to use a virtual switch as a type of semaphore? If the virtual switch is in a particular state, certain events will be ignored? A wait() command would be used to flip the switch back so that events could be triggered again. If I understand the infrastructure correctly, you’d need at least 2 pistons to accomplish this. If the wait() block was declared in the same piston as the event triggers, the concurrent events would simply sit idle during the wait() operation while waiting for the piston’s semaphore, then execute afterward with a significant delay.

One of my many iterations of this piston took a similar approach and involved 3 global variables: last_event_device, last_event_datetime, and EVENT_THRESHOLD. Here was the idea:

EVENT_THRESHOLD = 5 (constant: seconds)

1. $device triggers an event
2. IF $device == last_event_device OR $currentEventDate > last_event_datetime + 
EVENT_THRESHOLD
    3. last_event_device = $currentEventDevice
    4. last_event_datetime = $currentEventDate
    5. programmatically trigger events on other devices

This worked to a degree, but it could break when there were long pauses because of the piston-level semaphores or wonky device handlers (see: https://community.smartthings.com/t/zwp-wd100-dimmer-device-handler/125834)

I was also trying to understand the nature of piston-level variables. Do asynchronous triggers of the same piston share piston-level variables, or does each one have its own scope?


#4

Just to clarify:
This technique has the Simulated Switch in the default OFF position 99.9% of the time.
It only turns ON for 10 seconds to let commands pass thru unadulterated.

Yes, my logic is to use the simulated switch as a flag so to speak. (Only do X if SS is on) You could get around the piston in limbo by either running that one block asynchronous, or, perhaps the way I would choose, is just turn on the SS… continue running the piston like normal, and make your last command either a small delay then turn off, or maybe just turn off… (Depending on how long the rest of the piston has run for) I would err on the side of caution though and add a few extra seconds… This way, even if there is a delay in your network, all still works as expected.

I don’t know if I am just being overly cautious, but I make sure that I never have two asynch blocks trying to communicate with or control the same device, or to write to the same variable. While I have not heard of issues from this, it just sounds like a good coding habit to have. (Although I would imagine that reading from the same variable simultaneously might be ok)


#6

@WCmore

Just to clarify:
This technique has the Simulated Switch in the default OFF position 99.9% of the time.
It only turns ON for 10 seconds to let commands pass thru unadulterated.

Hmm… I think I would have to see an example. My sense is that it would be the exact opposite (triggers are free to propagate only when there’s not a propagation already occurring). Did you have a specific scenario in mind?


#7

Sorry, I am not at my PC now, but something like this…

For the piston that will send commands that you want other pistons to ignore:

If trigger happens
Then DO 
Turn on SimSwitch
Turn on important bulb
Bunch of other stuff
Wait 7 seconds
Turn off SimSwitch

Then, in your old piston that normally monitors that bulb, you only have to add one line:

If important bulb changes
AND
If SimSwitch is off    <-- The only line you are adding
Then DO
Your regular commands go here

This method means that all day every day your pistons run like they do now -EXCEPT- for the 10 seconds when SimSwitch is on. It also means only a single line of code is needed for any piston that you occasionally want to be ignored.


#8

@WCmore
What would be the difference between using a virtual switch and a global boolan variable?


#9

As far as I understand, global variables are not written until the piston finishes all of it’s tasks, so my suggestion above would not work using globals. This is why we are going around that limitation a bit, and using the simulated switch as if it were a global. (so we get snappy responses, and can toggle on and off in the same piston)


#10

Here is a ‘working’ example of what I was describing. With device handlers that work as expected, this seems to be giving me good results:

Now, I’m not entirely sure how bullet-proof this is. There are a few questions:

  1. Can someone verify that global variables are only assigned values at the end of a piston execution? This may cause problems if (see #2)

  2. How does the asynchronous handling work with a single piston handling multiple events? It was my understanding that subsequent events firing while a piston is executing will wait for a semaphore until the previous execution is finished. Is this correct? Can a subsequent piston trigger begin executing before a previous trigger of the same piston has finished executing?

  3. How does piston scope work? Does my @OFCL_DATETIME variable even need to be global? Will a local variable work the same assuming we’re only wanting to stop the same piston from triggering before the backoff?


#11

I am not sure how helpful this will be, but here goes…

(1) I believe so, but have not tested this firsthand. Once I learned about the delays on globals, I have intentionally kept a short delay before querying them once written. (I prefer reliability over speed any day)

(2) To be honest, I am not sure. I can say that I have repeatedly witnessed commands getting ‘lost’ when too many blocks are trying to communicate at once, so now I strategically plan my pistons to not step on each other’s toes. (It is not always possible, but it is a good practice for code running thru the ST hub)

(3) Local variables write immediately. If line 28 Sets a local variable, line 29 can read (and act upon) the new data stored.


#12

I’ve been using this one for a few months - no issues with the loops or light level shows I got with previous iterations:


Treating a Group of Lights/Switches/Dimmers as One
#13

@WCmore

(3) Local variables write immediately. If line 28 Sets a local variable, line 29 can read (and act upon) the new data stored.

WRT (3), My question is about scope. Let’s say we have a piston, Foo, with a local variable, fubar, initialized to 0. Piston Foo triggers because of a status change on switch A. Variable fubar’s value is set to 2. For the sake of argument, let’s say that the status of switch A changes again before the piston is finished executing. The piston’s initial execution is temporarily interrupted by the second trigger, which sets variable fubar to 3. The first piston trigger interrupts the second and finishes executing. Before it finishes, it references variable fubar. Will it be 2 or 3? If the piston executes a 3rd time several minutes later, will the initial value of fubar be 0, 2, or 3? To reiterate, is a local variable’s scope tied only to a single execution/trigger of a particular piston sequence, or will the value of a local variable persist until the piston is paused and restarted?


#14

The way your piston is written, it runs completely through before triggering again. It will not be interrupted, started over, or run a second instance in parallel.

Your local variables are available for update immediately. You increase a variable by +1 in one line, read/use it in the next line of your piston, and the +1 value will be loaded.

Global variables are frozen in time when a piston starts executing (the piston reads them and loads the values, then runs through its whole execution), so updates to global variable values are not available to a piston until its next execution. However, if piston A updates a global variable and then piston B reads it, the new/updated global variable value would be available to piston B because it was set before piston B began executing.


#15

@michicago

However, if piston A updates a global variable and then piston B reads it, the new/updated global variable value would be available to piston B because it was set before piston B began executing.

Thanks. So you’re saying that the value of a local variable will persist across multiple piston runs as long as that piston is not paused and restarted? If the first run sets variable fubar from 0 to 1, the second piston run will read that variable’s value as a 1?

If what you said in your last post is all true, my datetime variable that sets the last known event timestamp wouldn’t have to be global at all?


#16

Variables persist indefinitely unless one of two things happen:

  1. You set a default value in your ‘define variable settings’
  2. You write a new value to them

Variables never automatically revert to zero, regardless of how many times a piston is paused, restarted, stopped, edited, etc.

You should never use a global variable unless you need to pass a value from one piston to another. That’s the value they bring above local variables. Otherwise, globals are slower to read/write and as mentioned above, stagnant for the entire execution of your piston.