Context for developing and debugging Hubitat Elevation apps, drivers, and hub environment — sandbox constraints, lifecycle idioms, capability contracts, plus grounded deploy/log-tail/lint mechanisms.
74
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
A Hubitat device can own other devices. The owner is a parent, the owned are children, and the set is one physical thing (or one cloud account) projected as several hub devices — a power strip's outlets, a Hue bridge's bulbs, a dual switch's two loads.
Sources: Hubitat's Parent/Child Drivers
doc and its genericComponentParentDemo.groovy / genericComponentDimmer.groovy examples, plus the
/hub2/devicesList tree verified live on 2.5.1.128 (C-8 Pro) against a hub of 151 top-level
devices and one parent owning 5 children.
rules/device-lifecycle.md). These are harder to see: the device is not indented anywhere,
and its only tell is a "Parent app" row in the Device Details table on the device page's
Device Info tab (/device/fullJson/<id> exposes it as parentApp — skills/_reference/endpoints.md).Both make the device a child. Only the reporting differs.
GET /hub2/devicesListReturns {suggestBackup, devices:[...]}, and devices is a tree, not a flat list. Each entry
is {key, data, children, parent, child}:
key — DEV-<id>parent — bool: this device is a parent. It is not a pointer to one.child — bool: this device is a child.children[] — the owned entries, same shape, nested.Children appear only nested — never at the top level. The grounded hub returns 151 top-level
entries and 5 children reachable only by walking children[] (156 devices in all). Iterating
devices[] without recursing silently misses every child device.
Verified child entry: parent:false, child:true, driver type Zooz Power Strip Outlet Component,
DNI 1A-CH1…1A-CH5 under parent DNI 1A. Deriving a child DNI from the parent's is the common
pattern, not a requirement — the docs state authors may use any convention, and Hubitat's own
example keys on the parent's device.id instead ("${device.id}-${type}").
data.source is System | User | Linked. Linked marks a hub-mesh device owned by another
hub — orthogonal to parent/child (2 on the grounded hub).
rules/zwave-zigbee-mesh.md)./device/fullJson/<id> lists child devices, so
the removal blast radius includes them (rules/device-lifecycle.md).skills/device-migration/SKILL.md.state.endPoints > 0 is not proof the children existOn a multi-endpoint Z-Wave device, state.endPoints records what the device reported, not what
the hub created. Check fullJson.hasChildren / childDevices for existence, never
state.endPoints — the same "children are invisible to the flat list" family as the tree read
above.
A platform update can start surfacing that gap as a stream of device errors on a device that has
been fine for a month. Grounded 2026-08-12: a Zooz ZEN14 outdoor double plug began erroring
No device for endpoint (1)/(2). Press Configure to create child devices. on the first refresh
after the hub updated to 2.5.1.152. Every error forwarded to notifications, so it read as a
device fault. It was not — the device had carried state.endPoints = 2 for a month while
refresh() sent a per-endpoint switchBinaryGet every 6 h with no errors, and the parent's
on()/off() targets endpoint 0 and drives both outlets, which is what Alexa, HomeKit and Maker
API all use. Nothing was broken before or after.
The errors fire only on an inbound report carrying a non-zero endpoint, so the endpoint-encapsulated replies had previously not been reaching the driver with their endpoint. (The dispatch mechanism is inference from a tight correlation across the restart, not measured.)
The trap for anyone diagnosing it. Reading the driver, you would conclude Configure cannot
help: createChildDevices() is called only from the MultiChannelEndPointReport handler, and the
probe that requests that report fires only when state.endPoints == null — which it isn't.
Configure works anyway, because configure() → clearVariables() → state.clear() nulls
endPoints, so the probe re-fires. That indirection is the whole answer and it is invisible from
the two obvious call sites.
Two notes for the repair:
fullJson.settings[].defaultValue against
device.data.configVals first — on the grounded device all eight matched, so the resync was a
no-op.device.name (the driver type), not the device label, so they land
as <Driver Type> - Outlet N and need relabelling.A scan for other exposed devices only sees drivers that track endpoints in state, so it is not
conclusive for built-in drivers.
addChildDevice(namespace, typeName, deviceNetworkId, properties).
Hubitat's example: addChildDevice("hubitat", "Generic Component Switch", "${device.id}-Switch", [name: "...", isComponent: true]). Built-in component drivers use namespace hubitat; a
custom child driver uses its own.Xyz() calls componentXyz(cd) on the parent,
passing the child device as the first parameter (componentOn(cd), componentSetLevel(cd, level, transitionTime)). The parent does all the Z-Wave/Zigbee/Matter/LAN talking.parse() on the child with a List of Event
maps — getChildDevice(cd.deviceNetworkId).parse([[name:"switch", value:"on", descriptionText:"..."]]). Component drivers turn those into events. This is the convention, not
sendEvent on the parent (rules/state-vs-attributes.md).getChildDevice(dni) / getChildDevices().input (rules/app-lifecycle.md).