childDevices - inconsistencies in calling parse, hasAttribute, hasProperties

I have what, I think should be simple code that works with child devices and uses the parse in a component driver, but also needs to use hasAttribute and other functions for the child. I am running into an inconsistency that I can't figure out. Maybe there's a solution.

Here's what I'm doing.

I need to work with a bunch of child devices as well as the root device, so I create a list of them

List<com.hubitat.app.DeviceWrapper> targetDevices = getChildDevices()

I then have a block of code in which I want to check each child device for a property and call its parse, like:

targetDevices.each{
     if (it.hasAttribute("something")) { do something}
     it.parse( [[event:"switch", value:"on"]])  // sample event
}

My actual code is much more complex, but this gives a basic idea.

Now that all works fine. But sometimes I also want perform the same set of test and generate a similar parse in the parent device. To do that, I have a void parse(List<Map> events) method defined in the parent device, so it can perform a "parse" using the same method call that is available in a child component device.

It seems the next step is obvious - I should be able to add the "parent" onto my list of target devices. I've attempted that in two ways:

Method #1

List<com.hubitat.app.DeviceWrapper> targetDevices = getChildDevices()
targetDevices += device
targetDevices.each{
     if (it.hasAttribute("something")) { do something}
     it.parse( [[event:"switch", value:"on"]])  // sample event
}

and

Method #2

List<com.hubitat.app.DeviceWrapper> targetDevices = getChildDevices()
targetDevices += this
targetDevices.each{
     if (it.hasAttribute("something")) { do something}
     it.parse( [[event:"switch", value:"on"]])  // sample event
}

If I add the parent to the targetDevices list , using Method #1, (by adding 'device') then the it.hasAttribute works for the parent device, but the call to it.parse fails for the parent (but both work for the child devices).
If I include the parent using Method #2, (by adding 'this') then the it.hasAttribute fails for the parent device, but the call to it.parse works for the parent (and both work for the child devices).

Logically, it seems to me that a device on the targetDevice list should behave similarly - i.e., calling an it.hasAttribute or one of the device methods through it.methodName() should be logically the same. Is there a way to accomplish what I've described?

An obvious solution would be to handle the parent in code specific to the parent device, but it seems it should be possible to have a list of devices that include the parent and be able to operate on it consistent with what you can do for child devices.

it.getChildDevice(childKey).parse()

Methods #1, #2 in original example, above, now expanded/clarified.

Thanks. I'm not sure if I understand the suggestion (or if my scenario was unclear). In my scenario, 'it' can be a child device or the parent device, so when 'it' is a child device, wouldn't that be getting the child device of the child device? And I'm assuming childKey is the network ID string. How would I fill that in within the 'each'

I.e., are you saying I'd code it like

(childDevices + device).each{  it.getChildDevice(childKey).parse( [[name:"switch", value:"on"]]) }

with something else needed there in that code to fill in childKey.

Any further help is appreciated.
Thanks,

I just threw something together quick and wasn't able to demonstrate the failures you note (nor is it immediatley apparent to me why they would be happening, assuming you did indeed implement parse(List<Map> description) in the parent as described).

It might not be helpful unless you can spot something I'm doing that you're not or of I misunderstood what your actual problem is, but here is that test regardless. :slight_smile:

EDIT: I should add that the custom testParse() command is what I would run to test the potenitally-problematic code--that's what I created it for.

metadata {
   definition (name: "Virtual Parent Test", namespace: "RMoRobert", author: "Robert Morris") {
      capability "Actuator"
      capability "Switch"
      command "testParse"
      //command "parse"  // only needed if use this.device instead of this
   }
       
   preferences {
   }
}

void installed() {
   log.debug "installed()"
   runIn(1,"createChildSwitch")
}

void updated() {
   log.debug "updated()"
}

void createChildSwitch() {
    addChildDevice("hubitat", "Generic Component Switch", "${device.deviceNetworkId}/Switch",
                	[name: "Child Switch TEST"])
}

void off() {
   log.debug "off()"
   sendEvent(name: "switch", value: "off")
}

void on() {
   log.debug "on()"
   sendEvent(name: "switch", value: "on")
}

void testParse() {
   log.debug "testParse()"
   List<com.hubitat.app.DeviceWrapper> devs = getChildDevices()
   devs += this
   devs.each {
        it.parse([[name: "switch", value: "on"]])
    }
}


void parse(List<Map> desc) {
    desc.each {
        if (it.name in ["switch"]) {
            sendEvent(it)
        }
    }
}

void componentOn() {}
void componentOff() {}
void componentRefresh() {}

I was thinking more of a list of devices that had child devices.. but back to your question as long as the left side is a deviceWrapper logically both should work. What happens if you do something like:


List<com.hubitat.app.DeviceWrapper> targetDevices = getChildDevices()
targetDevices.add(this)
…

or maybe this.device

Edit: a third consideration might be to cast it as a deviceWrapper when adding it to the list

@thebearmay @bertabcd1234
Thanks for the suggestions. I'll write up some test code to better demonstrate and will post that tonight or tomorrow for further thoughts.

@thebearmay @bertabcd1234
Here's a simple driver to show the problem:


/////////////////


metadata {
	definition (name: "Simple Parse Test",namespace: "jvm", author: "jvm") {
		command "createChildren"
		command "deleteChildren"
        command "test1"
        command "test2"
		command "test3"
		command "test4"
    }
}

void createChildren(){
	addChildDevice("hubitat", "Generic Component Switch", device.deviceNetworkId +".child1-ep000", [name:"child1", isComponent: false])
	addChildDevice("hubitat", "Generic Component Switch", device.deviceNetworkId +".child2-ep000", [name:"child2", isComponent: false])
	addChildDevice("hubitat", "Generic Component Switch", device.deviceNetworkId +".child3-ep000", [name:"child3", isComponent: false])
}

void deleteChildren()  {
	childDevices.each{ deleteChildDevice(it.deviceNetworkId) }
}


void componentRefresh(cd) {
}

void test1() {    
	(childDevices + this).each{log.debug "Device ${it.displayName} has attribute switch: ${it.hasAttribute("switch")}"}
}

void test2() {    
	(childDevices + device).each{log.debug "Device ${it.displayName} has attribute switch: ${it.hasAttribute("switch")}"}
}

void test3() {    
	(childDevices + this).each{it.parse([[name:"switch", value:"on", descriptionTest:"Received event at device ${it.displayName}"]])}
}

void test4() {    
	(childDevices + device).each{it.parse([[name:"switch", value:"on", descriptionTest:"Received event at device ${it.displayName}"]])}
}

void parse(List<Map> description) {
    description.each {
        if (it.name in ["switch","level"]) {
            log.info it.descriptionText
            sendEvent(it)
        }
    }
}

Install as a virtual driver.

  1. Click on the createChildren button and it will create 3 "Generic Component Switch" children
  2. Open a log window.
  3. Click on "test1" . This test creates a list of devices by joining the child and the root. For the root I use the 'this" variable (childDevices +this).each and prints whether each has the attribute "switch". test1 works for the 3 child devices, but fails for the 'this' device. No surprise so far.
  4. Click on "test2". This test creates a list of child devices by joining the child devices and root. for root I use 'device" - i.e., (childDevices + device).each
    Test 2 succeeds and prints out what each of the child devices and root has the "switch" attribute and the root does not. This is expected.
  5. So, now we know that you can add getChildDevices() + root. So far, so good.
  6. Now click test3. This is similar to test1 in that it groups the devices with (childDevices + this), and calls parse of the devices using .each{ it.parse([[]] }. I'd expect this to fail on the 'this' device, as with test1, but it works.
  7. Now click test4. This is similar to test3, but joins the devices with (childDevices + device) and calls parse using .ech{ it.parse(...) }. Again, as with #3, this is the one I would expect to succeed, but it now fails.

So,
(childDevices + device) works for (childDevices + device).each{ it.hasAttribute (...)}, but fails for (childDevices + device).each{it.parse(...) }

And
(childDevices + this) fails for (childDevices + this).each{ it.hasAttribute(...)", but succeeds for ().each{ it.parse(...) }

Doesn't seem to make sense.

I believe the differences are explained by the fact that this appears to refer to the instance of the class created behind the scenes for the user driver (not documented, as far as I know), whereas device (or this.device) is the documented DeviceWrapper object. There might be some automatic "boxing" going on for this in some cases (probably not the right term, just an analogy, and I am of course not sure of any internal implementation details), but this doesn't appear to be one regardless. You can do a bit of prodding if .class isn't blocked by the sandbox (swore it was, but it worked for me...), or you can call a "dummy" method name and deduce the class name from the resulting error (e.g., asdsdf(this.device)).

So: hasAttribute() won't work with this since I believe that only comes from DeviceWrapper, but parse() works on this since that is defined in your driver (a Groovy script that ultimately becomes a class if what I'd guess about their implementation details is correct). But parse() won't work on the DeviceWrapper (the problem in test4) because it's not declared as a command; that much is easily fixed by adding command "parse" to the driver (something I noted in my sample above but didn't really dig into that much).

Does that account for everything? :smiley:

I think so. Thanks for the explanation.

Thanks for all the insight / suggestions.
It seems the only "real" workable solution for now is that I simply handle the parent in its own code and not try to treat its DeviceWrapper as if it were equivalent to the child ones.

You could still do that if you wanted--you just need to actually make it equivalent to the child ones by adding command "parse" to the parent driver (like they have). But I understand if you don't want to do that--adds a confusing button to the UI for people who might not know what to expect from it, which you aren't the driver (and are instead a human) is basically nothing. :slight_smile:

Yep, I don't want the extra button, but I'll keep your suggestion in mind in case I come across a similar problem in the future and can use what you suggested. Thanks!

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.