Calling method in parent driver from child

Looking for some pointers with a multi-endpoint Zigbee device...

Working example:
Parent driver:

def on(){
     log.debug "parent on() called"
     return "he cmd 0xAE83 0x0B 0x0006 1 {}"
}

Child driver:

def on(){
     parent.on()
}

Non-working example:
Parent driver:

def on(){
     log.debug "parent on() called"
     return "he cmd 0xAE83 0x0B 0x0006 1 {}"
}

def childOn(dev) {
/*normally, do stuff to determine endpoint
  hardcoded commands for example*/
     on()
}

Child driver:

def on(){
     parent.childOn(this)
}

Running the command from the parent device UI sends the command and the device turns on. Running the command from the child, the log line shows up, but the command does not work. I'm guessing there are some sandbox related differences in how HE does this vs. SmartThings. What is the correct way to send a Zigbee command in response to a method call from a child device?

Edit: does the childOn() command need to be declared in the "commands" section for that to work? Is there any way to have the command not show up on the UI?

Is the switch capability defined for the child as well. What happens when you call the "on" method on the child? The top one should work. But this might be a bad example. Why would you want on on the parent and on on the child to do the same thing?

For the method commandOn to work? No. You can have methods that aren't defines as commands that work correctly. For the method to be available outside of the driver itself (i.e. call from an app)? Yes.

Yes, the Switch capability is defined on both. Bottom example, childOn() is called on the parent, which calls on(), normally with some parameters. I can see that on() gets called from the logs.

I defined the childOn command in 'commands' and it magically started working... I'd like to not have what should be private method calls showing up in the UI.

Everything works in the method except issuing the Zigbee commands.

I'm sorry, I still don't understand what you are trying to do. If you are calling the method from the child, just do this:

Parent driver:

def on(){
     log.debug "parent on() called"
     return "he cmd 0xAE83 0x0B 0x0006 1 {}"
}

Child driver:

def on(){
     parent.on()
}

Edit: I just realized is in your first post.... I don't understand why that won't work given the little snippit of code you've put up.

When you say that the command isn't sent...the call to the parent method isn't run at all, correct?

Is childOn declared as a command in that driver?

so,

is logged in your non-working example?

Correct. the childOn() method is a helper method that converts some values and passes them along to the on() method (or setLevel, setHue, etc. I chose this one for the example as it was the simplest.) I stripped it down to this to try to eliminate possible reasons for it failing. There would be more to it than this normally.

What values are you passing up the parent for the on method? Are you trying to pass the method up to the parent or down to the child?

You aren't passing anything with parent.childOn() But in the parent you are expecting something?

You're not passing dev from the child. So, I don't see how it is even logging in the example you gave. That's why i am confused now.

Wouldn't that be done in the child? Since it knows who it is?

Child calls parent.childOn() passing a device reference. childOn() is determining the endpoint based on the device object and passing along to the on() method.

Sorry. I've updated the original post.

I don't see how the logging activity can take place but not the z-wave command.

That doesn't make any sense. And there are no errors thrown? it just ignores the z-wave command part? That really doesn't make any sense at all. Add logging in after the command is sent to see if that logging is captured.

That's typically done using a return statement, so no logging after that. In the non-hard-coded version, I even confirmed that the command string is formed correctly on the line before the return. The log line before the return shows that it should be all good. Sandbox/security restriction in play? Scope issue? It seems correct from a Java/Groovy standpoint.

Wait...what? Are you using this as a virtual device? How would you know if the command was sent or not? It won't send it to a device unless the driver is paired to a device.

No, it's a physical device with two endpoints. Maybe similar to the Hampton Bay fan controller? I can verify that if called from the parent driver, the command is sent and the device reacts accordingly. If sent via the child driver UI, no dice.

However, explicitly declaring the childOn command in the metadata section lets it work as expected, but now I'm exposing internal methods in the UI.

I asked you that an hour ago. If you want to call the command it has to be exposed in the commands section. Otherwise it won't work.

That's why typically, if you have multiple children using the parent to just send data, you would determine what the command is in the child, and send it up through the parrent using the same method for all commands.

The administrative UI, yeah. But that's all the Hubitat GUI is supposed to be. The admin interface.

I assumed that this would be the case for calling from apps, but from a child device driver? I would have thought that declaring it would be the equivalent of public whereas default would be package level access. That's an odd design decision. Ok then. I need to rethink the design a bit. Thanks for hashing it out with me.

2 Likes

Looks like in order to make it work the way I was originally going for, I needed to wrap the commands in HubAction/HubMultiAction and use the sendHubCommand() method instead of just returning the ArrayList of commands as I was used to.

Switched from using a custom child driver to the built-in generics. They were using helper methods as well, so was running into the same issues. No declaring commands in the metadata necessary using this method, so I get my clean UI page :+1:

Just piling on this thread for documentation sake.

I have a parent / child driver I am working on for a SmartenitZBMLC30. It is a 220V motor controller that I use for the pool pumps.

There is a composite driver for SmartThings and I am porting it to Parent / Child.

The key think I was missing was the command directive in the parent is as @Ryan780 states:

I had to add the command "turn_on", ["Integer"] along with the turn_off in the parent, in order for the child to be able to activate the dynamically called switch. The switch1_on/off and switch2_on/off are from the original driver.

    command "switch1_on"
    command "switch1_off"
    
    command "switch2_on"
    command "switch2_off"
    
    command "turn_on", ["Integer"]
    command "turn_off", ["Integer"]

Prior to that change it was behaving as @mitchp indicated. Messages were being logged, but the zigbee.on() / zigbee.off() command wouldn't do anything.

I had assumed there was a scoping issue in the following function.

def turn_on(Integer output = 0) {
	//if (dbgEnable)
		log.debug "${device.label} Parent Is turning output ${output} on."
    
    if (output == 2){
        log.info "turn_on(${output}) with cmds"
        def cmds = []
        cmds << "st cmd 0x${device.deviceNetworkId} ${Endpoint2} ${OnOffCluster} ${OnCommand} {}"
	    cmd
    }
    else if (output == 1){
        log.info "turn_on(${output}) with zigbee.on()"
	    zigbee.on()
    }
} 

The log output was like this, but the zigbee action was not firing:

dev:662020-03-07 11:51:38.661 info turn_on(1) with zigbee.on()
dev:662020-03-07 11:51:38.658 debug Pond Control Parent Is turning output 1 on.
dev:662020-03-07 11:51:38.650 info switch1_on() via turn_on(1)

So thanks to both of you for helping me get one more piece of the puzzle.