Seeking Better Certainty of Device States

Yeah. Thatā€™s good. I forgot about Initialize capability. No rule needed.

@bravenel this is awesome. It is going to take me some time to digest and absorb this properly because I have minimal familiarity with Groovy and driver structure, but I'm generally understanding the code. Most of my lack of comprehension comes from which words are Hubitat keywords/reserved words, which are general conventions for variables/methods in Hubitat drivers, and which must be defined in the driver. I suspect that you have defined everthing here that you need to though.

Particularly "onCheck", I haven't seen that, is that a predefined State in Hubitat drivers?

Somehow I was thinking Initialize was a method that was called when the device was powered up, and I saw your suggestion about hub offline/reboot as addressing when the hub rebooted. Does Initialize get called in both situations? Are you suggesting using an attribute instead of a state because attributes are preserved through a hub reboot and states are not?

If the added commands onV() and offV() were left in the driver, then cancelOffCheck() and cancelOnCheck() would need to remain in those methods as well as be added to on() and off() I presume.

Thanks again.

No, state objects are just inventions of the coder.

The initialize method is called when the device is created. The initialize capability causes that method to be called upon hub startup, which is a different kettle of fish. This is used for things such as reconnecting to telnet for a driver that uses telnet, etc. So it could serve the purpose of restarting the checking after a reboot if it was happening before.

No, I did that to expose it to RM, but if initialize is used, it doesn't need to be an attribute, and state would work fine. Both state and attributes are preserved across reboots.

No, because in each case the corresponding method is called. onV() calls on(). No need to cancel checking twice. But there is a need to cancel it in on() and off() in case those are commanded instead of onV() or offV().

runIn() is a hub service method that schedules something to happen in the future. device.currentValue() is a built-in method that returns the current value of a device attribute. on(), onV(), refresh(), etc. are all names of methods in the driver. Certain methods are mandatory depending on the Capabilities defined. For example, Switch Capability mandates that the driver must implement on() and off(). onV() and offV() were declared to be commands of the driver in its metadata section:

        command "onV"
        command "offV"

That causes those methods to show up on the device page. exposing them to the world.

Yes, I had seen those metadata command declarations in a custom driver for an Enerwave dual relay that I've been using as a learning tool. I haven't seen them in the Hubitat tutorials on custom drivers though. That clears things up for me.

I would like to try to include your code in driver code for devices I use and try to get it to work. Do you know, are you able to make public the built-in drivers for the Zooz Zen17, Zen04, or Aeotec SmartSwitch7?

Start with @jtp10181's Zooz driver. Get one to work.

The best way to learn Groovy, imo, is to modify existing code, and get the modifications to work as expected. You can freely sprinkle in log.debug to see things as you go, and then take them out later. This particular code is pretty simple and straightforward, so is a good place to start.

1 Like

I don't have s full driver for the ZEN17 but I do for the ZEN04: [DRIVER] Zooz Smart Plugs Advanced (ZEN04 / ZEN05 / ZEN14 / ZEN15)

2 Likes

OK, I can do that with the Zen04.

One more thing, if onCheck is the invention of the coder, was that pre-existing in the driver you were modifying?

Yes, thanks, I had installed and was experimenting with your Zen17 driver last night to see if Supervision parameters were available, and I was looking at the code for the Zen04 just to try to understand the general makeup of Hubitat drivers and Groovy.

No, I just made it up for this code. A state variable that has not been assigned a value starts out as null. That's assumed in this code.

BTW, the code to add to initialize() (once Capability "Initialize" is added) is this:

    if(state.onCheck) redoOn() 
    else if(state.offCheck) redoOff()

@bravenel does onCheck need to be defined anywhere?

No, state objects are defined just by using them in the code.

2 Likes

The way I see it, there is a heirarchy like this:

Rule Machine --> Device Driver --> Device

The device can be unresponsive but the device driver fine.

Are there ever situations when RM is fine, but the driver is unresponsive?

Also, regarding the device state that RM shows for a switch device (On or Off), is that value stored on the hub from the last event the driver reports or is it a driver state variable or a driver attribute that RM accesses when it needs it?

In general, drivers are always "fine". So are apps (unless there is some bug). Devices go south for any number of possible reasons.

Not really. The driver is simply providing an interface to the device. It would almost always be the device that is unresponsive.

Device state pretty much always comes from an attribute. Apps don't have access to the internal state of a driver, only to its attributes and commands (except in the case of child devices, where the app could call a driver method to get something other than an attribute back). The driver sets the attribute by sending an event. An app may be subscribed to those events for it to be triggered. If the app is just going to inspect the state, it uses something like device.currentValue(attribute) (actually, more likely to use in the case of a switch device, device.currentSwitch, which means the same thing as device.currentValue("switch")).

1 Like

@bravenel

(1) If the device is non-responsive, for example unplugged, and there is a command from RM to Refresh or turn On or Off, what is device.currentValue within the driver at the moment it begins processing the command? The attribute that RM sees for the device state will be the last known switch state of course.

(2) When on() or off() is called in the driver, has the driver already attempted to turn the switch on/off or does it occur after the call, and if after the call, does the code within the method get processed before or after the attempt to turn the switch on/off? This may answer part or all of my remaining questions.

(3) I will need to handle repeat ON commands or repeat OFF commands (duplicate commands). If the device driver is in an onCheck state which hasn't resolved yet, and gets a new command from RM to turn ON, I'll want the driver to ignore the new command, because it will be counting the number of retries, and I don't want that counter variable reset to 0. I may be reading the code wrong, but I'm not sure that possibility is accounted for. My inclination would be to leave onV() as a custom command and do the test there. Something like:

def onV() {
if(state.onCheck = false) {
cancelOffCheck()
state.onCheck = true
runIn(10, refOn)
on()
}
}

I would then remove state.onCheck = false in the other methods and I would add an else statement after

where state.onCheck would be set to false there

(4) In onV() and offV() I don't understand the need for a delay before the call to refOn(). Is it to allow cancelOffCheck to complete?

Thanks!

The current value of the attribute.

The on() or off() method is what causes the Z-Wave commands to be sent to the device. The code I provided uses those methods to accomplish that.

This is all up to what you do in the code, how you keep track of things. I didn't put in retry counting in the code I provided. You're talking about basic algorithmic processing. Up to you -- up to the code.

You don't want anything after the calls to refresh(), on() or off(). These methods return the Z-Wave commands that are then sent by the hub to the device. Groovy returns whatever comes last in a method. The code I gave would have been clearer had it said

return refresh()

The return in implicit in the code I provided. Here is the last line of the refresh method from the driver I used, so you can see what I mean:

return zwave.switchMultilevelV1.switchMultilevelGet().format()

This is to provide time for the device to respond, possibly through a slow mesh. Issuing any command to the device requires some time for it to respond, asynchronously. Refresh asks the device to report, and that takes some amount of time as well. Typically, a driver will exit after issuing a command to a device. Device responses to commands are asynchronous to the command being issued, and is part of the reason for 'fire and forget' drivers. In an event driven hub processing lots of events, you don't want code waiting for things to happen. So, the code I offered sends on() to the device, then waits 10 seconds. If the device took the command and responded as expected to that command, refOn sees that in the currentValue of the attribute, and just exits without doing anything beyond making state.checkOn false. If the attribute has not been updated, then it sends a Refresh, and waits another 10 seconds before checking again. This is a pretty lazy way to do this, but one that won't burden the mesh with too much traffic, slowing down other devices in the mesh. For a healthy device, sending the initial command is sufficient, and there is nothing more to do.

1 Like

Perhaps it would help if you understood the basic operation of a driver relative to a device. There are two sides to a driver, its commands and its parse method. The commands are visible on the device page as tiles. The parse method is what handles message to the hub from the device.

When you command a device from its device page, the driver sends the appropriate message to the device and immediately exits -- its job complete. At some point in time after that, a healthy device having completed the command sends a message to the hub reporting the completion of the command. The parse method processes that message, and sends an event to the hub's event stream. Events update the appropriate attribute, and trigger apps that are subscribed to them (in that order).

So, in the provided code, 10 seconds after the on or off command was issued (through onV or offV) presumably the device has completed the command, sent the corresponding message to the hub, the corresponding event has happened, and the attribute has been updated.

So because of the timer, this test is likely to be a test of the state of the device after the event from the device has been received back, but there isn't a requirement that the driver does in fact receive the event from the device. If your answer to my first question above is true, if the event isn't received back within the runIn interval, the conditional test for device state would then be using the device's last known state.

This would lead me to believe that the testing you did on your Tree light worked because the tree was Off when you unplugged it. If it was On when you unplugged it, and then you gave an On command from the device page, the driver code you wrote would use the previous state of the tree for its test, would consider the On command successful without an event being received, and would have completed with a debug log entry of a successful outcome while the tree was unplugged.

Yes, but what do you expect should happen in this case? The objective of commanding the device is to put it into a particular state. So if it is in that state to begin with, the driver might as well exit immediately. If you want to determine that the device is non-responsive, that's a different objective.

This could be accomplished by looking at the time of the last event, and checking it when refOn is called. The time when the last on() command was sent could be stored in state, and that compared with the last event time. If the last event time is before the last command time, then you'd know the device is non-responsive, and could do whatever accordingly.

Ultimately you would need to add an attribute that reflects the responsiveness of the device, code in the driver that sets that attribute, and apps that respond to those events. This is all non-trivial, but doable.

What my RM rules are designed to do is to rely on an event that occurs after the command, not an expression that could be from an earlier event and be incorrect because something caused the device to be in a different state than the driver currently thinks it's in. So my expectation, one way or another, is to issue an On or Off command, and only be satisfied that the device is in that state when an event is then received saying that it is.