CtrlK
BlogDocsLog inGet started
Tessl Logo

jbaruch/hubitat-dev

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

Quality

93%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

SKILL.mdskills/firmware-update/

name:
firmware-update
description:
Update Z-Wave device firmware on a Hubitat hub via the native zwaveJS updater — discover installed versions, find vendor-latest firmware, stage it, and batch-flash safely with a radio-hang watchdog. Use when the user wants to update/flash device firmware, check which devices are behind on firmware, or fix a device whose issue a firmware update addresses.

Firmware-Update Skill

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.

Step 1 — Frame the job

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.

Step 2 — Discover installed firmware

Read installed versions over HTTP — no UI:

  • Bulk: GET /device/fullJson/<deviceId>device.data.{deviceModel, firmwareVersion, protocolVersion, manufacturer}.
  • Per node (driver-independent): GET /hub/zwave/deviceFirmware/details?nodeId=<n>targets[0].version + firmwareIdHex.

Build a model + version + node/device-id map. Proceed to Step 3.

Step 3 — Find vendor-latest (NOT auto-discovery)

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:

  • Zooz — the OTA files page lists every model/version; direct free download getzooz.com/firmware/<MODEL>_V<MM>R<mm>.zip (.gbl 700/800, .otz 500). Read the per-model change log to justify the update.
  • Leviton — free .ota on leviton.com/content/dam/leviton/support/. Never cross model files.
  • Others vary; some (e.g. Springs shades) publish no downloadable firmware — say so and stop.

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.

Step 4 — Stage the firmware on the hub

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/firmware

Confirm via GET /hub/zwave/deviceFirmware/files. Proceed to Step 5.

Step 5 — Flash with the safeguards

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:

  • No-progress watchdog — aborts a flash if percent stops advancing at any level (a frozen transfer never emits DONE/FAILED and would hang the radio forever).
  • Canary (--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, read per hop count (--rssi-floor) — skips a node whose link the floor does not clear, in two reported buckets: skipped_weak (override when attended with --flash-weak) and skipped_unknown (override when attended with --flash-unmeasured). The two overrides are independent — a flash-everything run passes both. Which bucket a node lands in is the script's decision predicate: rssi_gate() in skills/_scripts/hub_fw_update.py, with the policy it implements in rules/firmware-update.md Guardrails.

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.

Step 6 — Verify and account

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 / skipped-unknown / failed. For any failure, report that the device is on old firmware, not bricked (nodeState OK) and retryable. Do not silently drop the weak/unknown/failed nodes; name them and their current version. A skipped_unknown node was not judged unflashable. Its own link was never measured. Report it that way, never as weak.

README.md

tile.json