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
Radio-mesh diagnosis is a different axis from code debugging. The data comes from three
undocumented JSON endpoints (skills/_reference/endpoints.md); skills/_scripts/hub_mesh.py fetches and flags,
the mesh-health skill interprets. Hubitat publishes no numeric "bad" thresholds — flag
unambiguous signals and rank the rest; never assert an invented cutoff.
A clean radio is not a working device. Every metric here describes the radio path. Commands
reach a device over a separate command path (app → hub mesh → owning hub → radio), and a broken
command path leaves every radio metric green. Radio evidence can never conclude "the mesh is
healthy, so the devices are fine" — see The command path.
nodeState FAILED — the node is unreachable. Split it by deviceId (hub_mesh.py tags failure_kind): FAILED with a bound deviceId is a real device currently unreachable — may be transient (recover it, do NOT delete); FAILED with no deviceId is an orphan ghost (a pairing that never bound a device) — safe to remove. Never force-remove a real device thinking it is a ghost.lastTime — when the hub last heard the node; absent on a node never heard at all (zwave.never_heard[]). Such a node is reported nodeState:OK and passes every radio check, so no FAILED-keyed check sees it. A never-heard node with no deviceId is a ghost by the split above; with a deviceId it is a real device that has never spoken — judge it, don't assume either way.per — cumulative packet-error count ("accumulation of packet errors for a node"), not a percentage. Lower is better; 0 is ideal. Nonzero means errors are occurring; judge severity relative to the node's msgCount and to peers.averageRtt — round-trip time in ms; lower is better. No spec cutoff — rank, don't threshold.listening — the always-on indicator. true identifies a mains-powered classic-mesh node that
can repeat. false identifies a sleepy node.listening instead of guessing repeater capability from device type.beaming means the node requires beam wake-up. It does not mean the node sends beams or repeats.beaming field was false even for sampled FLiRS locks on 2.5.1.132.beaming key as repeater proof.routeChanges — classic-mesh stability indicator; frequent changes mean the Last Working Route keeps failing. Reported by the legacy backend; N/A on the zwaveJS backend.neighbors — how many nodes a classic-mesh node hears. 0 for Long Range nodes (a star has none — see below), not a backend artifact.route — hexadecimal node ids with hub (01) first, the destination itself last, and repeaters
only between them. 01 -> 57 reaches node 0x57 directly. 01 -> 1B -> 57 uses repeater
0x1B. hub_mesh.py reads only intermediate hops into zwave.route_fan_in.shared_device_count, and read the same way: blast radius, not a fault. zwave.route_fan_in.repeaters[] ranks every repeater by dependent_count; each node also carries its own dependent_count.summary counter, and needs no remedy. Hubitat publishes no "too many dependents" cutoff — rank, never threshold.zwave.route_fan_in.load_bearing_concerns[] is that cross: a repeater that is never_heard, FAILED, weak, or carrying packet errors, ranked by how many nodes sit behind it. A never-heard leaf is one silent device; a never-heard repeater carrying 12 is 12 nodes whose path runs through something the hub has no evidence is alive. Both sit in never_heard[] and only dependent_count tells them apart.never_heard. Quote its dependent_count as the scope of that one warning — never add it up as extra faults.dependent_count: null — the question does not apply, which is not the same as zero. route_fan_in.anomalies[] holds routes that do not run hub → node (stale or malformed records) and unknown_node marks a hop absent from nodes[]; both are data gaps, not mesh faults.lastTime proves the radio works. An actuator (shade, outlet, lamp) transmits only after being commanded, so its lastTime is when a command last landed — silence is unknown, not broken.zwave.stalest / zigbee.stalest this way. hub_mesh.py ranks staleness and never flags it — the classification is yours.hub_mesh.problems[] is critical: a peer that cannot carry commands is as dead as a failed radio. peer_unreachable / peer_identity_mismatch come from probing the recorded address; peer_offline / peer_inactive / peer_warning are the hub's own claims.active:true, offline:false, warning:null with lastActive ticking. Never read those three as an all-clear; only the probe is evidence. The peer table is asymmetric — each hub keeps its own record, and one side can be right while the other is stale.shared_device_count is its blast radius: every shared device is a link an app can bind to, and removing the peer unbinds them. Quote the count before advising a re-add, and prefer editing the address over remove-and-re-add. A hub whose address drifts re-breaks the record — a DHCP reservation on the hub's current IP is the durable fix.lastTime is stamped differently per backend: legacy emits an explicit +0000 (true UTC); zwaveJS emits a naive stamp in the hub's local zone (timeZone in /hub/details/json). Reading naive as UTC ages every zwaveJS node by the hub's UTC offset. Zigbee's lastActivity carries +0000 on both.lwrRssi is reported on two different scales depending on the Z-Wave backend. Read zwaveJS first.zwaveJS:true — absolute dBm (negative, e.g. -78db); closer to 0 is stronger. Silicon Labs receiver sensitivity: −97 dBm (700-series, 100 kbps GFSK) and −110 dBm (800-series LR channel, 100 kbps O-QPSK); the classic GFSK floor on 800 is a few dB higher. A route near the floor is genuinely weak.zwaveJS:false (legacy) — dB above the noise floor (positive, e.g. 27dB); ≤ 0 is at/below noise.routeChanges is also N/A under zwaveJS — a backend limit, independent of topology.hub_mesh.py tags each node's topology.neighbors:0 and a direct route (01 -> <node>) are inherent to LR, not faults. LR uses per-transmission dynamic power control over a long link budget, so a distant LR node sitting at −85…−93 dBm can be normal.neighbors, multi-hop route, routeChanges, repeaters, and Z-Wave repair apply.GET /hub/zwaveRepair2, skills/_reference/endpoints.md), and its route rebuilds on its next wake, sitting in the rebuild's Pending list and completing async. For a marginal battery node the durable fix is RF/topology — a repeater near it — not a repair click.zigbeeDetails snapshot exposes no per-device LQI or RSSI — from it, judge liveness, not signal.GET /hub/zigbee/getChildAndRouteInfo (the neighbor/route table, text — LQI:<n> per router). Per-frame LQI+RSSI come from the live zigbeeLogsocket. Don't rank Zigbee signal off the snapshot; use one of those.active is not liveness. Long-dead devices can remain active:true.active value as a freshness verdict.lastActivity and rank its age.lastActivity timestamp is unknown, not fresh.name:"Device" of type:"Device" is a join that never initialized — the Zigbee ghost.networkState ≠ ONLINE, healthy:false, or weakChannel:true (interference on the current channel). Zigbee uses 2.4 GHz channels 11–26; weakChannel overlaps busy Wi-Fi.ws://<ip>/zwaveLogsocket and ws://<ip>/zigbeeLogsocket stream per-frame decoded traffic — distinct from the driver /logsocket. Tail via skills/_scripts/hub_radiolog.py; the snapshot says who is weak, the log shows it happening.lastHopLqi/lastHopRssi, Z-Wave per-frame RSSI: -NN dBm), sequence gaps (a soft missed-frame hint, not a hard drop count — the counter is shared across the device's traffic), and which cluster/command a device uses.lastHopLqi/lastHopRssi are the last hop into the hub — for a routed device that is the repeater→hub link, not the end device's own radio. ZCL cluster names for the common clusters; 0xFC00–0xFFFE is manufacturer-specific, 0xE000–0xEFFF is reserved space vendors (Tuya) use off-spec.hub_radiolog --summary → transmit_report) gives the noise floor and SNR at both ends. An elevated or spiky hub noise floor with hub_snr well below dest_snr means the hub's own receiver is the bottleneck, from its RF environment rather than the device or distance. Common culprits are co-located 900 MHz radios, USB3, and gear clusters. The fix gets the hub's receiver out of that noise by relocating the hub, fitting an external antenna, or separating co-located hubs. Never the device.skills/_reference/zwave-lifecycle.md).RemoveFailedNode) applies only to a FAILED orphan ghost, never a recoverable real device — groundable via POST /hub/zwave/nodeRemove. A full Z-Wave network rebuild is groundable too, via GET /hub/zwaveRepair2 — both in skills/_reference/endpoints.md.skills/_reference/endpoints.md).