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.
—
—
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
High
Do not use without reviewing
Process steps in order. Do not skip ahead.
The always-on knowledge — why this is dangerous, the prerequisites, vendor-latest sourcing, and the
guardrails — is rules/firmware-update.md. This skill is the step-by-step procedure; read the rule first.
Hubitat's built-in Device firmware updater (Settings → Z-Wave Details → Maintenance) flashes OTA
over the same radio the device already uses — no driver swap, no self-hosting, and it handles LR +
S2 that the community driver-swap updaters stall on. Its HTTP surface and every field are grounded
in hub_fw_update.py and ../_reference/endpoints.md.
The load-bearing hazard: a failed OR stalled OTA can hang the entire zwaveJS controller — the
hub keeps returning success:true but transmits nothing, freezing every Z-Wave node at once (Zigbee
is unaffected — that asymmetry is the tell). A mid-transfer stall on a weak node did exactly this to a
main/automation hub and staleness-poisoned every lux/temperature-gated rule downstream until a reboot.
Steps 5–6 exist to prevent and recover from that; never fire-and-forget a flash.
Establish the hub (--ip or --hub) and confirm the backend is zwaveJS (zwaveJS:true in
/hub/zwaveDetails/json) — this skill drives the zwaveJS updater. Identify the target device(s) and
whether they are mains or battery/FLiRS: battery/sleepy devices (locks, most sensors) must be
awake for the multi-minute transfer (USB power, fresh batteries, or operate them to wake). Proceed to Step 2.
Read installed versions over HTTP — no UI:
GET /device/fullJson/<deviceId> → device.data.{deviceModel, firmwareVersion, protocolVersion, manufacturer}.GET /hub/zwave/deviceFirmware/details?nodeId=<n> → targets[0].version + firmwareIdHex.Build a model + version + node/device-id map. Proceed to Step 3.
GET /hub/zwave/deviceFirmware/available?nodeId=<n> exists but must not be the source of truth —
it lags (offered ZEN04 2.30 when 2.60 shipped, missing the SDK/S2 fixes) and mis-matches (offered a
Springs shade a bogus downgrade). Get the real latest from the vendor:
getzooz.com/firmware/<MODEL>_V<MM>R<mm>.zip (.gbl 700/800, .otz 500). Read the per-model change log to justify the update..ota on leviton.com/content/dam/leviton/support/. Never cross model files.Hardware revision decides the image. 700-series and 800LR share a model name but need different
files; pick by the installed major version. The wrong image can brick — the hub rejects a mismatch at
/start, but do not rely on that. Unzip to the raw .gbl/.otz. Proceed to Step 4.
Upload once per model — it persists and is reusable across every node of that model:
curl -F "uploadFile=@<MODEL>_V<MM>R<mm>.gbl" http://<hub>:8080/hub/fileManager/upload/firmwareConfirm via GET /hub/zwave/deviceFirmware/files. Proceed to Step 5.
Write a worklist [{"nodeId":N,"fileName":"X.gbl","target":"2.6","name":"…"}, …] and run:
python3 skills/_scripts/hub_fw_update.py --ip <addr> --worklist work.json --canary <devId>:<nodeId>Full argument/flow/hazard contract: the hub_fw_update.py module docstring. It is idempotent (skips
nodes already at target) and carries two required guards plus a floor:
percent stops advancing at any level (a frozen
transfer never emits DONE/FAILED and would hang the radio forever).--canary devId:nodeId, a known-healthy mains node) — after a failed flash (a
verified success already proved the radio transmits), confirms the controller still transmits; if not,
it reboots the hub and re-checks, aborting if it stays hung.--rssi-floor, default −95 dBm) — skips hang-prone floor-signal nodes; override only
when attended with --flash-weak.One batch per radio (a hub's Z-Wave is single-threaded for OTA); use --wait-pid to chain a second
batch after the first, or run different hubs in parallel. Never run two flashes into the same radio at
once. Proceed to Step 6.
The script verifies each node against its target (the hub caches the old version until the post-reboot
re-interview, so /details is polled until it flips). Re-read device.data.firmwareVersion for a
second confirmation. Report per device: updated / skipped (already current) / skipped-weak / failed —
and for any failure, that the device is on old firmware, not bricked (nodeState OK) and retryable.
Do not silently drop the weak/failed nodes; name them and their current version.