"Interrupt" driver loop

As a learning experience, I'm coding a virtual shade driver (based on the existing one(s)) that will simulate a window shade. For example, when it receives an open command, it will set the windowShade attribute to "partially open", then go into a loop that will...

  1. Sleep for one second.
  2. Do a sendEvent to simulate the shade position changing.

The number of iterations is a preference. So if the iterations is 10, it will loop 10 times. Each time, it will update the position by 10%, e.g., 0%, 10%, 20%,...,100%. If iterations is 5, it will show 0%, 20%,...,100%. It also handles the scenario where an open command is simulated on a partially open shade. So if iterations is 10, and the shade is currently at 50%, the position will start at 50% instead of zero, and it will only iterate five times.

The driver does not actually try to control a shade. It is for simulation/testing only.

Open and close are basically working. I'm working on the "stop" command now. My idea is to check to see if there was a stop command issued from the same control device while the driver is in the loop. Is this possible with a HE driver? If so, what's the best design for doing this?

The simulated scenario needs to handle the possibility of multiple buttons each controlling a single shade. So, there may be three shades opening at the same time because three individual buttons were pressed at nearly the same time. If one of those is pressed again while it's shade is still "opening", I want to only stop the shade that the button is controlling.

For testing, I'm using either the driver detail page or the relay button on a ZEN30 double switch. For the latter, I have rules set up so that if the relay button is pressed while the shade is not opening and not closing, an open command is sent. If the shade is opening or closing, a stop command is sent. The rules seem to be working, I can see the open command and the stop command in the log. I just don't know how to make the stop command interrupt the "in progress" open command. I use a button 2x tap to send a close command.

I realize that the real shade drivers don't need to worry about this, e.g., they just send a stop command to the shade and it's done. But my driver is acting as the shade, so it's a different scenario.

You can use a state variable, named something like "stop" and then set that to true. Have to loop check for stop=true and if so end the loop.

Although, I would recommended against using sleep in any driver or app, it can have unintended side effects. It is a better practice to use runIn and call another function to run in X seconds. You can use the same thing with a state variable in this case as well.

2 Likes

I wrote "sleep" as a generic term. I'm actually using the pauseExecution function. Is that the one with unintended side effects?

Time to read up on state variables. I saw them in some of the driver code I looked at, but haven't tried to understand them. My plan is to check for "stop" before every pause (however the pause is implemented).

pauseExecution is equivalent to sleep

2 Likes

They are like global variables that persist on the device page.

I was looking into adding an attribute for "stop" to the two attributes provided for the windowShade class, but I haven't figured out how to do it. Is a state variable a better approach?

I'm having a hard time finding documentation on stuff like this. For this level of detail, is the only way to figure it out is to look at available driver code?

Yes, please do not use an attribute for that. State variable is the correct thing to use.

Check the "flash" functions on my switch driver for a good example" [DRIVER] Zooz ZEN Switches Advanced (and Dimmers)

Driver code starting on line 524: https://github.com/jtp10181/Hubitat/blob/main/Drivers/zooz/zooz-zen-switch.groovy

That actually shows an example of the state variable and using runIn to schedule another function. I am basically doing the same as what you are trying to do. I create a looping flashing cycle with runIn and break it using a state variable. The state variable can be changed via other commands.

You can even remove the state variable when done with it to keep it clean looking on the device page.

1 Like

No, the use of state in drivers (and apps) is documented:

That being said, there aren't a ton of examples there, but most of what you need to know is just how Groovy maps work. Example drivers can certainly help if you are not sure.

1 Like

@jtp10181 I was actually looking at your ZEN dimmer driver. I'm playing around with the ZEN77 800LR dimmer to get a better understanding of the environment.

@bertabcd1234 I had seen the doc on "Storing data (state)", but it's really high level with very few details. Can state variables only be strings? What methods are associated with them? For example, the ZEN dimmer driver uses state.remove("flashNext"). How should I have known (or found out) that there is a remove method? Are there other methods? Is the "remove" method documented? It's pretty easy to glean what the "remove" does and what the parameters are (unless there's more parms than the variable name), but are all methods that self-documenting?

Are state variables "HE things" or "Groovy things"?

1 Like

The key must be a string (I think, or at least I wouldn't try anything else...). I would stick to common Groovy objects for values, like strings, numbers, lists or maps or these types, etc., though I believe they made some changes a while back that allow for some more unusual choices to be stored as well.

The state object itself is specific to the Hubitat environment. The docs do provide a brief overview of Groovy (see the Developer Overview) but are not intended to be a comprehensive resource for Groovy itself. Maps are a Groovy/Java thing.

2 Likes

No you can also save lists and maps there, and then use them just like a map/list. I have one I am working on where I save a Maps of lists.

It is basically just a special single Map variable "state" and whatever maps you create in there become state variables. So it is mostly just a groovy thing, but specialized for HE.

state.stop = true
is like doing (I think?)
state += [stop:true]

or more complex state variables

state.list = ["a", "b", "c"]
state.map = [1:"a", 2:"b", 3:"c"]

Pardon any syntax errors I am just typing this out from memory.

Still missing something (or it's a DPE bug)...

Open command sets request type to open and state.stopRequested to "false", then calls the funtion that does the looping. Stop command sets the request type to "stop", sets state.stopRequested to "true", then returns. Here's what happens. Note that the log messages with the "**" in them are from the stop command function. All the others are from the looping function when it was called from the open command...

Stop shades log

So when the shade is "40% open" (as shown on the device details page), I pressed the stop button. That command drove the stop function. It got the current shade state from the object attribute, the "stop state before stop" from state.stopRequested, logged them, then set state.stopRequested to "true". The next log message from the loop shows that state.stopRequested is "false", so it looks like it doesn't "see" that the stop command set state.stopRequested to "true".

I played with replacing pauseExecution() with runIn(), but it will be a pretty big change because runIn is an async function.

Maybe I don't understand the scope and/or lifetime of the device state object. If I have five independent shades, do I have five state objects? Is each one automatically created when the device is defined and last until the device is deleted? Does the driver need to explicitly do anything to create/initialize the object?

I suffered from concurrent execution as my app (and driver) was asynchronously triggered by rapid, almost instantaneous MQTT payload changes, with the corresponding device update events. I had great difficulty with state variables remaining consistent or they became null, i.e. not as expected and of course you can’t use atomicState in drivers.

In the end I implemented callbacks (via events) into my main app to check what was executing and restart if necessary. The app uses atomicState but I also found I had to use some @field vars too. This has worked for be although repeatedly launching my app before it completed needed handling better. I feel sure I should have got this coded better but I’m a novice programmer and this does appear to work fine now so I didn’t revisit it.

Yes, each device using a driver has its own copy. (Note that if you take the field annotation approach above, this is not true--those are shared at the driver level. This is not an insurmountable problem, one approach being a Map that you "index into" using the installed device ID. But it may not be one you need to bother with at all.)

You do not need to do anything to create or initialize state. It's just there.

What I suspect you're running into is that you have two separate instances of the driver running, one from your code doing that is looping, and one from your commands. The contents of state are not written until your driver (or app) finishes execution, and pauseExecution() does not count as such. This is one reason I'd recommend the runIn() approach others have suggested. Then you can write the new state, exit, and either your next execution of this "loop" (not really anymore; now your callback method) or something else that causes the driver code to run (e.g., your "Stop" command/ethod) will have the current values.

In an app, this would also be solvable using atomicState, which is written right away. However, this option is not available in drivers.

2 Likes

I think the problem just sunk in. I read that the above in the doc, but didn't realize what it was saying. So it sounds like while the driver is in the pause loop, the change of state.stopRequested to "true" by the stop command isn't available to the pause loop until the next time the driver gets a command. Is that correct?

I'm not sure what you mean and it's hard to say without really seeing your code, but I don't think this is exactly what I said. What I meant is that anything you write to state while in your loop, including while it's waiting for a pauseExecution(), will not be readable by your command until that first piece of code (the one including your loop, in this case) exits. So if you call the command in the middle of that, it will still see the old state, which again, only gets written on exit. You'll see the new state if you run the command after that.

1 Like

I think we are saying the same thing, but let me make a more general scenario...

Button 1 is pressed, which initiates a command. A snapshot of the state variables is taken at that instant and that is what the driver sees for this "thread" while executing code for button 1. Button 1 changes state.var1.

While the button 1 thread is running, button 2 is pressed. A snapshot of the state variables is taken at that instant and that is what the driver sees for the button 2 thread. Since the thread for Button 1 is still executing, its changes to state.var1 haven't been written out, so button 2 does not see the change button 1 made to state.var1.

Button 2 changes state.var2 and terminates before the button 1 thread terminates. Button 2's changes are "written out" but the current button 1 thread will never see those changes.

I made some quick changes to my driver to see if the above is how it works, and it seems to show that it does.

Yes, that sounds correct. (Note also, in case it wasn't clear, that button 2 changes will effectively be lost once button 1 changes are actually written.)

Understood. I envisioned this to be something like a device having its own process in Linux and each command being a thread running in the device's process. In that environment, all the commands would have access to any memory/variables in the process space, and changes by one thread would be instantly available to other threads in the device process. Looks like the atomicState you mentioned is logically similar to this.

Looks like I need to change the driver to use RunIn().

Update: Changing the driver from a "loop with pause" design to an "async runIn()" design was a bigger change than I expected, but it's now correctly simulating the stop command while it is in the middle of simulating an open command. The shade state is set to "partially open" and the shade position shows the expected value.

Thanks for all the help.