[2.4.0.125] Add support for the choice of components in parent/child drivers

When adding a generic child device driver, developers currently have two choices:

addChildDevice("hubitat", "Generic Component Dimmer", "data": [label: "Dimmer", isComponent: true])

addChildDevice("hubitat", "Generic Component Dimmer", "data": [label: "Dimmer", isComponent: false])

When isComponent: true, then the child device can't be changed by the user.

When isComponent: false, then the child device can changed by the user but the user can select any device .

I'd like to add a third option that I've named uses:

addChildDevice("hubitat", "Generic Component Dimmer", "data": [label: "Dimmer", uses: "parent.componentSetLevel"])
addChildDevice("hubitat", "Generic Component Dimmer", "data": [label: "Switch", uses: "parent.componentOn"])

This could be a string or a list of strings.

addChildDevice("hubitat", "Generic Component Dimmer", "data": [label: "Dimmer", uses: ["parent.componentOn", "parent.componentSetLevel"])

This option would allow the user to chose a child device but with a restriction that the child device declares that it uses a specific API. In the last case, I would be able to select any device that declares the use of 'parent.componentOn' or 'parent.componentSetLevel'.

Some context about the use case(s), I have several Zen16, Zen17 relays and recently purchased the Fibaro Smart Implant.

My first issue was controlling a magnetic lock using a Zen16 relay:

This appears as a 'switch' and not a lock. Since 'isComponent: true', I can't modify the device to a 'generic lock'.

Let's take example of the code from @jtp10181 for the Zen17:

/Handle Sensor Child Devices
	if (inputType != null) {
		epName = "Sensor ${endPoint}"
		// properties.type = "S"
		switch (inputType) {
			case 4: deviceType.typeName = "Generic Component Water Sensor"; break
			case 5: deviceType.typeName = "Generic Component Switch"; break
			case 6: deviceType.typeName = "Generic Component Motion Sensor"; break
			case 7: deviceType.typeName = "Generic Component Contact Sensor"; break
			case 8: deviceType.typeName = "Generic Component Carbon Monoxide Detector"; break
			case 9: deviceType.typeName = "Generic Component Carbon Dioxide Detector"; break
			case 10: deviceType.typeName = "Generic Component Switch"; break
			case 11: deviceType.typeName = "Generic Component Contact Sensor"; break
			default: deviceType.typeName = "Generic Component Switch"
		}
	}

	properties.name = "${device.name} - ${epName}"
	logDebug "Creating '${epName}' Child Device"

	def childDev
	try {
		childDev = addChildDevice(deviceType.namespace, deviceType.typeName, dni, properties)
	}

If this code was changed to:

//Handle Sensor Child Devices
	if (inputType != null) {
		epName = "Sensor ${endPoint}"
		// properties.type = "S"
		switch (inputType) {
			case 4: deviceType.typeName = "Generic Component Water Sensor"; break
			case 5: deviceType.typeName = "Generic Component Switch"; 
                                      properties.uses = 'parent.componentOn'
                                      break
			case 6: deviceType.typeName = "Generic Component Motion Sensor"; break
			case 7: deviceType.typeName = "Generic Component Contact Sensor"; break
			case 8: deviceType.typeName = "Generic Component Carbon Monoxide Detector"; break
			case 9: deviceType.typeName = "Generic Component Carbon Dioxide Detector"; break
			case 10: deviceType.typeName = "Generic Component Switch"; 
                                        properties.uses = 'parent.componentOn'
                                    break
			case 11: deviceType.typeName = "Generic Component Contact Sensor"; break
			default: deviceType.typeName = "Generic Component Switch"
                                        properties.uses = 'parent.componentOn'
		}
	}

	properties.name = "${device.name} - ${epName}"
	logDebug "Creating '${epName}' Child Device"

	def childDev
	try {
		childDev = addChildDevice(deviceType.namespace, deviceType.typeName, dni, properties)
	}

I could then select any child device that uses a 'parent.componentOn', most surely the Generic Component Lock, yay!

All generic components could use an extra declaration:

https://github.com/hubitat/HubitatPublic/blob/master/examples/drivers/genericComponentDimmer.groovy

    definition(name: "Generic Component Dimmer", namespace: "hubitat", author: "mike maxwell", component: true) {
        capability "Light"
        capability "Switch"
        capability "Switch Level"
        capability "ChangeLevel"
        capability "Refresh"
        capability "Actuator"
        uses 'parent.componentOn'
        uses 'parent.componentSetLevel'
    }

The meaning of 'uses' = "parent.componentOn" is this driver is expected to call componentOn() for the switch on/off capabilities . "parent.componentSetLevel" the componentLevel() functions. There's no compile-time checks at all, it acts like a tag/category.

I think it should be fairly easy to implement and help simplify parent/child issues.

Right now, I have this wonky setup where every "Generic Component Switch" has a lock capability. I think it was a suggestion in the forums and has grown into a mess.

My drivers set the iscomponent to false…. So you can change the driver to anything you want if the child was created by my driver. I figure you are already using my advanced driver so if you want to change child drivers around go for it but it may not work the same.

That being said, not sure if the generic lock driver will work or not as a switch but you can sure give it a try.

Another option would be to use the switch bindings app or a rule to bind the child device with a separate virtual lock device.

I understand there are workarounds but this should be easy to change by design. Another example:

I had to modify the Fibaro Smart Implant driver of @christi999 to isComponent: false to use my own Voltage sensor which can be calibrated to interpret PH:


metadata {
	definition (name: "Generic Voltage PH Sensor", namespace: "gde", author: "Jonathan Bond") {
		capability "Refresh"
		capability "VoltageMeasurement"
        capability "pHMeasurement"
		capability "Sensor"
		
		attribute "rawVoltage", "number"
        
		command "setTemperature", [
			[name:"temperature", type:"NUMBER", description:"Set the temperature used for calibrations."]]

		command "setVoltageOffet", [
			[name:"offset", type:"NUMBER", description:"Set an adjustement to the voltage (useful if different from multimeter)."]]

		command "updatePH", [
			[name:"voltage", type:"NUMBER", description:"Update PH based on measured voltage from sensor (useful when testing a PH sensor)"]]

        // https://atlas-scientific.com/blog/how-to-calibrate-ph-meter/
        // https://www.hannainst.com/hubfs/006-finished-content/pH_Guides/calculating-a-ph-slope-percentage--hanna-instruments.pdf
		command "calibrate", [
            [name:"PH", type:"NUMBER", description:"The PH of the buffer solution used to calibrate the sensor", type: "ENUM", constraints: ["4", "7", "9"]]]

        input "debugEnable", "bool", title: "Enable Debug Logging?", defaultValue: true
	}
}

void setVoltageOffet(offset) {
    state.voltageOffset = offset
}

void setTemperature(celcius) {
    state.temperature = celcius

    // Update PH of calibration buffers + re-compute slope
    // state.ph.low = 4,
    // state.ph.mid = 7,
    //  state.ph.high = 9,
    //updateSlope()
}

void updatePH(voltage) {
    onVoltage(voltage, "physical")
}
            
void calibrate(buffer) {
    refresh()
    state.calibrations[buffer] = state.rawVoltage
    updateSlope()
}

void onVoltage(voltage, eventType) {
    if(!state.calibrations)
        setDefaultCalibrations()

	state.rawVoltage = voltage 

    voltageVal = getVoltageAdjustement(voltage)
	sendEvent(name: "voltage", value: voltageVal, unit: "v", type: eventType, descriptionText:"voltage is ${voltageVal}v")

    if(!voltageVal) {
        logWarn("No voltage from sensor, PH value not updated.")
        return
    }

    val = getPH(voltageVal).setScale(2, BigDecimal.ROUND_HALF_EVEN)
    sendEvent(name: "pH", value: val, unit: "ph", type: eventType, descriptionText:"PH is ${val}ph") 
}

BigDecimal getPH(voltage) {
    return state.slope * voltage + state.y
}

void updateSlope() {
    state.slope = (state.ph.low - state.ph.mid) / (state.calibrations.low - state.calibrations.mid)
    state.y = state.slope * -state.calibrations.mid + state.ph.mid;
    checkSlope()
}

void checkSlope() {
    phUnitsDelta = state.ph.mid - state.ph.low
    phVoltageDelta = state.calibrations.mid - state.calibrations.low

    slopePercent = ((phVoltageDelta - phUnitsDelta) / 59.16 * 100)
  
    if(slopePercent < 85 || slopePercent > 105) {
        logWarn("PH Slope at ${slopePercent}, check calibrations and PH sensor.")
    }
}

void installed() {
    setDefaultCalibrations()
}

void refresh() {
	parent.componentRefresh(device)
}

void updated() {

}

void parse(List<Map> data) {
    data.each {
        if (it.name in ["voltage"]) {
            onVoltage(it.value, "digital")
		}
    }
}

BigDecimal getVoltageAdjustement(rawVoltage) {
    // TODO: compute max/min voltage based on calibrations 
    // ph 0-14
    adjVoltage = rawVoltage
    
    if(state.voltageOffset) {
        adjVoltage += state.voltageOffset;
    }
    
    return adjVoltage
}

private setDefaultCalibrations() {
    // TODO: adjust for temperature
    state.temperature = 25
    state.ph = ['low': 4, 'mid': 7, 'high': 9]
    state.calibrations = ['low': 2.038, 'mid': 1.542, 'high': 1.161]
    state.voltageOffset = 0
    updateSlope()
}

private getNameFromId() {
	parts = device.deviceNetworkId.split('-')
	return parts[1]
}

void logWarn(String msg) {
	log.warn "${device.displayName}: ${msg}"
}

void logInfo(String msg) {
	if (txtEnable) log.info "${device.displayName}: ${msg}"
}

void logDebug(String msg) {
	if (debugEnable) log.debug "${device.displayName}: ${msg}"
}

One way to deal with it is @christi999 adds this code:

properties.uses = 'parse.voltage'
addChildDevice(deviceType.namespace, deviceType.typeName, dni, properties)

I add this to my driver:

metadata {
	definition (name: "Generic Voltage PH Sensor", namespace: "gde", author: "Jonathan Bond") {
	capability "Refresh"
	capability "VoltageMeasurement"
        capability "pHMeasurement"
        capability "Sensor"
        uses 'parse.voltage'
    }
}

Boom, it's ~compatible with the parent driver and can be selected in the UI.

Another use case here:

The strict "isComponent: true" makes it hard to create drivers which are extendable. A lot of projects on Hubitat could benefit from more collaboration.

I'd like to use the virtual keyboard and extend it and not have to re-write the parent/child code.

@mike.maxwell Any thoughts on something like this? I don't care much about the naming but would like to have your thoughts on the general concept.

Just to clarify on naming, replace "uses" with '"tags" and it represents the same concept.

addChildDevice("hubitat", "Generic Component Dimmer", "data": [label: "Dimmer", tags: "parent.componentSetLevel"])
addChildDevice("hubitat", "Generic Component Dimmer", "data": [label: "Switch", tags: "parent.componentOn"])
    definition(name: "Generic Component Dimmer", namespace: "hubitat", author: "mike maxwell", component: true) {
        capability "Light"
        capability "Switch"
        capability "Switch Level"
        capability "ChangeLevel"
        capability "Refresh"
        capability "Actuator"
        tags 'parent.componentOn,parent.componentSetLevel'
    }

The inbuilt drivers all set isComponent: true as there is a tight integration between the parent and child device messaging. Allowing changes to the child drivers is likely to introduce errors between the parent and child.
When we develop drivers where the parent device is capable of multiple preference driven functions that involve children, any child that is contrary to the preference is removed and a new child matching the preference is then added.

Given that, I'm really not following the ask.

1 Like

That's fine, it the parent driver creates child devices 'isComponent: true':

addChildDevice("hubitat", "Generic Component Dimmer", "data": [label: "Dimmer", isComponent: true])

Everything will work fine and the driver can't be changed. If you want to design a parent driver to be extendable by the community (the ask), your only option is:

addChildDevice("hubitat", "Generic Component Dimmer", "data": [label: "Dimmer", isComponent: false])

That's quite chaotic and broken as the whole list of drivers is shown.

So the idea here is to allow selecting a list of "highly likely compatible" child devices. Via the "uses/tags" declaration.

addChildDevice("hubitat", "Generic Component Dimmer", "data": [label: "Dimmer", isComponent: false, "uses": "parent.componentSetLevel"])

or

addChildDevice("hubitat", "Generic Component Dimmer", "data": [label: "Dimmer", isComponent: false, "filter": "parent.componentSetLevel"])

or

addChildDevice("hubitat", "Generic Component Dimmer", "data": [label: "Dimmer", isComponent: false, "allow_changing_to_devices_with_tag": "parent.componentSetLevel"])

...

I wouldn't consider this broken or chaotic.
The author of the original driver should be in charge of determining the correct child type, in any event this is such an edge case and honestly not that big of a deal to pick a driver that I can't currently see the payback in development effort.

1 Like

I tend to agree. It would be a cool thing to have but maybe not worth the effort to implement.

Fair enough, don't think it's an edge case at all though.

e.g. You could design a Camera parent and have several child devices done by the community:

Hikvision etc... It's inconceivable for a single developer to write say a 'camera' driver with all subcomponents/child drivers.

sure, and if you know how to do that, then as a developer it's no big deal to pick the correct child device with or without filtering before or after the fact...

2 Likes

I think you're missing the point, there's no way for me to extend:

parent Camera Device (by design an extendable driver)

  • Child 1 VideoStream
  • Child 2 AudioStream
  • Child 3 MotionEvents

Anyways, I'm part of Hikvision's partner program so ya I'll just re-write it. But ok sure, I'll just use isComponent: false

Not sure if it still works with the new UI, but there used to be a trick where if you went to the device page and then saved where you change the name and driver, it would actually temporarily unlock the name and driver options, and then they could be changed and saved again.

So I think I have a better half-baked idea I can try out without any changes from the hubitat staff. Instead of below:

addChildDevice("hubitat", "Generic Component Dimmer", "data": [label: "Dimmer", isComponent: true])

I'm going to use a 'registry pattern' to specify /override what child device I want to use:

compatibleChildDeviceUse("dni-id", "hubitat:Generic Component Switch",  "hubitat:Generic Component Lock")
compatibleChildDeviceUse("dni-id2", "hubitat:Generic Component Switch",  "mystuff:Generic Component Switch")

When the device does configure(), it will use compatibleChildDeviceAdd instead of addChildDevice

compatibleChildDeviceAdd("hubitat", "Generic Component Switch", ...)
compatibleChildDeviceAdd("hubitat", "Generic Component Switch", ...)

Will result in in the follow addChildDevice commands:


#for dni-id
addChildDevice("hubitat", "Generic Component Lock", "data": [isComponent: true])

# for dni-id2
addChildDevice("mystuff", "Generic Component Dimmer", "data": [isComponent: true])

May seem complicated but I've used this sort of pattern if the past.

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