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 driver is a single Groovy file with a metadata { definition(...); preferences {...} } block plus top-level methods. It is the layer apps and users talk to a device through.
capability "X" is a promise to implement every command X requires, as a Groovy method with the matching name and parameters. capability "Switch" obliges on() and off(); capability "SwitchLevel" obliges setLevel(level, duration).skills/_reference/capabilities.json. A declared capability with a missing command method is a real defect the lint-review skill flags.Actuator, Sensor) carry no commands — they only classify a device as controllable vs. reporting.command/attribute declarations go inside definition. Attributes surface as Current States; update them with sendEvent (see rules/state-vs-attributes.md).GET /device/fullJson/<id> → commands[] is the authoritative per-device command list — each {name, parameters:[{type, defaultValue}], relatedAttribute, capability:<bool>}, with capability:false marking a driver custom command. Read it instead of inferring commands from the declared capability, which misses every custom command (skills/_reference/endpoints.md).installed() — device created. updated() — user clicks Save Preferences. uninstalled() — cleanup.initialize() — on hub startup, only if the driver declares capability "Initialize". Use it to re-establish telnet/websocket/socket connections.configure() — required by capability "Configuration". refresh() — required by capability "Refresh". poll() — required by capability "Polling".capability "Configuration" declares that a configure() method exists — never what it configures. Whether it reaches Zigbee attribute reporting is not discoverable from the declaration.preferences, readable as settings in fullJson. A built-in driver is frequently narrower than the hardware — a sensor advertising a 1–240-minute reporting interval may expose only logEnable / txtEnable. Read settings before designing around any vendor-advertised configurable (skills/_reference/endpoints.md).parse(String description) receives raw inbound device data. Decode by source: Zigbee → zigbee.parseDescriptionAsMap(description); Z-Wave → zwave.parse(description, cmdVersions()) (legacy backend; returns null for unsupported command classes — always null-check; zwaveJS differs, see the backend split below); LAN → parseLanMessage(description); MQTT → interfaces.mqtt.parseMessage(description).zwaveEvent(...) overloads with a hubitat.zwave.Command catch-all placed last.parse()'s description is a decoded JSON value payload, not the classic zw device: … string. Same backend split as the RSSI/routing one in rules/zwave-zigbee-mesh.md, reaching the driver layer.zwave.parse() — it is richer (per-attribute prevValue, the device's own enum, already-decoded values, fields with no typed-class equivalent) and immune to typed-class defects. Payload shape and examples: skills/_reference/endpoints.md.zwave.parse() on that JSON is lossy and throws a GroovyCastException on some command classes, and the throw escapes parse(), killing every attribute downstream in that execution (rules/groovy-gotchas.md).description?.trim()?.startsWith("{") → parse the JSON; else zwave.parse(description, cmdVersions()) for the legacy backend.…Get() against an already-interviewed node produces no frame at all, independent of how it would be decoded.zwaveSecureEncap(...).parse().skills/_reference/parent-child-devices.md).parse() while developing: if (logEnable) log.debug "parse: ${description}" — see rules/logging-conventions.md.lock from DoorLockCCOperationReport's current mode, the commanded state.bolt status, latch status and door status.mode: Secured with the bolt retracted, and the hub publishes lock = locked.refresh() does not help. The truthful field arrives over the wire and is discarded on arrival.lock alone. Announcing "locked" from lock announces what the lock was asked to do.latchStatus or doorStatus. Both read open on a definitively shut Ultraloq door.doorCondition does reach a driver's parse() on the zwaveJS backend (2.5.1.135): bit0 door (1 = closed), bit1 bolt (1 = unlocked), bit2 latch (1 = closed).boltStatus is readable from another hub (rules/multi-hub-topology.md).hubitat Virtual Motion Sensor is unsafe for app-driven latched state: its active() auto-reverts to inactive on a hardcoded ~15s timer. An app that drives a virtual motion device to hold an aggregate state, such as a zone controller, cannot keep it active with the built-in.active() from the device page emitted inactive exactly 15s later, and an app-created child device renders no autoInactive preference on its edit page to disable it. A dead-consistent ~15s active duration in the event history is the signature.capability "MotionSensor" plus active/inactive commands that only sendEvent, no timer.skills/_reference/playwright-ui.md gotcha 25).