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
Hubitat has two distinct persistence mechanisms. Choosing the wrong one is a design bug that surfaces later as "my automation never fires" or "my data vanished".
sendEvent(name: "switch", value: "on", descriptionText: "${device.displayName} switch is on").isStateChange: true. type is "physical" (user acted on the device) or "digital" (hub commanded it).createEvent(...) builds an event map without sending — used when returning events from parse().currentState(attr).date dates the last change to the value, never the last report from the source. A healthy source reporting a steady value emits nothing and is indistinguishable from a dead one.temperature, humidity, thermostatOperatingState). Its staleness measures the world's volatility, not the path's health. Raising the threshold trades detection window against false alerts; no threshold makes the wrong signal right.thermostatTime, runtimeUpdated, sensorsUpdated). Strongest form: stamp your own clock on each verified read, monotonic by construction.lastPoll whose value is a constant string like Succeeded is change-filtered like any value, and goes arbitrarily stale on a healthy integration.isStateChange: true is the writer-side counterpart: set it when a consumer must see every report, not only changes.GET /device/eventsJson/<deviceId> (skills/_reference/endpoints.md) is how you measure an attribute's real gap distribution before trusting it.GET /device/fullJson/<id> and are easy to read backwards: Groovy state is top-level deviceState, attributes are nested device.currentStates (skills/_reference/endpoints.md).currentStates[attr].date in that payload is this trap made concrete — it sits beside value, reads like a freshness stamp, and is the last change, not the last report.lastActivityTime and a healthy sibling attribute prove the radio is alive. Neither proves a given attribute is still being reported.skills/_scripts/hub_device_events.py --hub <name> --device <id> --expect-attribute <name> (contract in its module docstring).maxEvents window and a dead one look identical there (rules/data-collection.md).rules/self-reported-vs-measured.md).An attribute may carry a warning code in the same slot as its measurement. Z-Wave's Battery command class encodes "battery low" as the reserved level 0xFF, and drivers flatten it into the same battery attribute the percentage uses.
The signature is a pair of events inside one wakeup — a plausible level, then an implausible one a few hundred milliseconds later.
Read the pair, never either value alone. Two disagreeing values that close together are one report.
An implausible value there is neither a percentage nor noise to filter out. It is the device asserting a condition.
Confirm such a value; never dismiss it as a decoding glitch.
Confirm against the raw frame rather than the attribute: force a wakeup with the device's Z-Wave button and read [BatteryCCReport] level: N via skills/_scripts/hub_radiolog.py --ip <addr> --radio zwave.
A sleepy sensor reports battery only on a wakeup. Tamper does not trigger one.
The hub issues a BatteryCCGet on wake and the report lands within ~200 ms.
A flag present on some wakeups and absent on others is diagnostic, not noise.
Never treat a resting multimeter reading as refuting a low-battery flag on a lithium primary.
Swap in a known-good cell to test the flag instead.
Describe the symptom, never one encoding.
An attribute may report commanded mode rather than a measurement. A lock's lock is the worked case (rules/driver-lifecycle.md).
state is a Map-like store for the app/driver's own data between wakes, serialized to/from JSON. state.foo = "bar".DeviceWrapper, a closure, or other live objects in state breaks — keep device references out of state.state writes just before the instance sleeps. atomicState commits immediately — use it only when overlapping executions can race, and prefer singleThreaded: true in definition as the cheaper alternative.state is serialized every execution; don't store large blobs there.sendEvent.state.