Zigbee Routing: Why a Sleepy Sensor Went Stale and How a Router Plug Fixed It
A Zigbee end device keeps its parent until it rejoins. How my bedroom sensor went silent under a thermostat, moved to a router plug, and how I detect staleness with last_seen.
HomeLab, Smart home. Updated . 6 min read.
My bedroom temperature sensor went silent for five hours while its radio link looked fine. No leave event, no low battery, nothing in the logs. The sensor was healthy. Its parent had quietly forgotten it existed.
That sensor feeds a night-time AC automation, so a stale reading isn't cosmetic. Fixing it meant learning how Zigbee end devices actually pick a parent, and the answer is less automatic than I assumed.
TL;DR: a battery sensor talks through exactly one parent and keeps it until it rejoins. My parent was a Tuya thermostat that drops child entries. I added a mains-powered SONOFF plug as a router, made the sensor rejoin through it, and confirmed the parent in a fresh network map. For staleness, I watch Zigbee2MQTT's last_seen, not the entity's last_reported.
The stack, briefly
The coordinator is a Sonoff Dongle Plus V2 on the NUC. I started on ZHA, but the TYBAC-006 thermostat I wanted is supported in Zigbee2MQTT and ZHA created no entities for it. So I flashed the dongle in place from 6.10.3 to EmberZNet 8.0.3 with universal_silabs_flasher inside the Home Assistant container, and moved to Zigbee2MQTT 2.13 with the ember adapter:
serial:
adapter: ember
rtscts: false
advanced:
channel: 25
homeassistant:
enabled: trueThen I re-paired everything, deleted the ZHA integration, and removed zigbee.db. The network is small: the thermostat, the bedroom sensor (a SONOFF SNZB-02D), an entrance button, an entrance light switch, and later a plug.
How a sleepy sensor picks a parent
Zigbee has three roles. The coordinator starts the network. Routers are mains powered, keep their radio on, and relay traffic. End devices are usually battery powered. They sleep, wake up, and talk only to one parent.
The parent matters more than it looks. The end device does not route. When it joins, it attaches to one router or the coordinator, and that parent keeps it in a child table. The sensor sends its reports to that parent, and the parent holds messages for it while it sleeps. If the parent drops the child entry, the sensor is effectively cut off, even with a perfect signal.
The trap: a sleepy end device keeps its old parent until it rejoins. Adding a better router next to it changes nothing on its own.
Why the bedroom sensor went stale
When I paired the thermostat, the bedroom sensor ended up as its child. The thermostat's neighbor table put the link at LQI 24 to 51, which is weak. The thermostat has an in-wall antenna. Weak alone would be survivable. The real problem showed up later: the sensor went silent for hours with good LQI and no leave event. The thermostat is a Tuya TS0601 device, and it dropped child entries.
One more tell from that device: Zigbee2MQTT logs Failed to execute routing table for it. That's a firmware gap in the Tuya TS0601 family, not a network fault, so I ignore it. But it's a hint that I shouldn't lean on this device as the one router a sleepy sensor depends on.
A plug as a router
The fix was a mains router on the bedroom side. I bought a SONOFF S60ZBTPG plug, put it on the TV wall next to the sensor, and called it TV Wall Plug. Before trusting it, I verified it's actually routing:
type: RouterandrxOnWhenIdle 1in its device record- in the neighbor tables of both the coordinator (LQI 117/195) and the thermostat (90/63)
Then I moved the sensor on purpose. I open joining on the plug only, not on the whole network, so the sensor can't pick another parent, and joining stays open for 254 seconds. Hold the sensor's pairing button for five seconds. Then request a fresh map and read the parent. I do this over MQTT, no frontend needed. The scan takes about 60 seconds:
mosquitto_sub -u <user> -P <password> -t zigbee2mqtt/bridge/response/networkmap \
-C 1 -W 150 > /tmp/nm.json &
sleep 2
mosquitto_pub -u <user> -P <password> -t zigbee2mqtt/bridge/request/networkmap \
-m '{"type":"raw","routes":true}'The first move, on 2026-08-30, gave Bedroom Sensor -> TV Wall Plug rel child at LQI 82-99, and the thermostat had no children left. Reports came back after a 5-hour silence (12:00 to 17:16).
It didn't stay put. On 2026-09-07 the sensor had drifted back to the thermostat at LQI 48. I paired it through the plug again at 22:29, and the fresh map showed Bedroom Sensor -> TV Wall Plug, child relationship, LQI 68. Reports resumed right away: 25.5 C, 62.3% humidity, battery 100%. I closed joining at 22:30. The path is now NUC to TV Wall Plug to sensor.
Gaps are not outages
After the first move, I looked at a full night: 111 reports between 00:00 and 10:35, median gap 2.2 minutes, max gap 41.9 minutes, radio LQI 192-208.
That 41.9-minute gap looks bad until you check the values around it. The temperature didn't move and the humidity changed by 1%. The SNZB-02D reports on change, not on a timer. A long gap on a stable night is the sensor doing its job.
So I judge sensor health by two things: LQI and the parent in a fresh map. Never by gap length alone.
last_seen, not last_reported
The AC automation needs to know whether the bedroom reading is fresh. My first version checked the temperature entity's last_reported time. It raised false alarms.
The reason: Home Assistant's MQTT integration skips the state write when the value hasn't changed. A sensor that reports 25.5 C three times in a row updates the entity once. So on Sep 6 at 09:53, the check decided the sensor was stale and fell back to a default setpoint, while the last Zigbee message was only 67 seconds old.
Since 2026-09-07, Zigbee2MQTT publishes last_seen in ISO 8601 and runs availability checks:
advanced:
last_seen: ISO_8601
availability:
enabled: truelast_seen updates on every message the device sends, changed or not, so it measures contact, not change. Availability uses the defaults: 10 minutes for active devices, 25 hours for battery devices. The automation uses sensor.bedroom_sensor_last_seen with its own tighter limits, 20 minutes while cooling and 60 minutes at rest. Missing contact means a safe fallback setpoint and a phone notification.
OTA and power_on_behavior
The plug arrived on firmware 1.0.2. I OTA'd it to 2.0.3 on 2026-08-30. It took 12 minutes and 2957 chunks. There's no changelog: the zigbee-OTA index carries no release notes for it. No new exposes appeared after the update.
The trap here is the reboot. The OTA ends with the plug restarting, and the default power_on_behavior is off. On a plug that feeds a real load, that means the load is dead after the update. Set power_on_behavior: previous before any OTA on a plug, not after.
When the coordinator hangs
On the night of 2026-09-04 the whole network went down twice. The dongle stopped acknowledging serial frames at 00:43 and again at 01:07:
ASH_NCP_FATAL_ERROR
ERROR_EXCEEDED_MAXIMUM_ACK_TIMEOUT_COUNT (on SEND_UNICAST)
No USB event in the kernel log, and the port stayed open. The firmware on the stick hung. Zigbee2MQTT exits with code 2 on an adapter fault, and the container's restart policy was no. So the thermostat, the bedroom sensor, and the plugs all stayed down until I restarted the container by hand at 00:55 and 02:11.
It was the first time in the 25 Zigbee2MQTT logs I keep. It was also the first night after an upgrade to 7.3.2 (kernel 6.18) and a swap of the boot flash drive, which now sits on the same USB 2 hub as the dongle. It wasn't load: no job runs at that hour, mains stayed at 225-236 V, and there was no OOM, thermal, or xhci line.
The fix I shipped the same day is the boring one: restart policy unless-stopped, applied live with docker update and added to the container template so it survives a recreate. Exit code 2 on an adapter fault is exactly the case a restart policy is for. It doesn't explain the hang, it just stops one hung stick from taking the house down until morning. Moving the dongle to another port and a one-night kernel downgrade stay on the list if it repeats. There's a related quirk too: after a reboot, the dongle sometimes fails to enumerate with error -71. A sysfs port cycle didn't help. A physical replug did.
Questions
- Why does a Zigbee sensor go silent while its LQI looks fine?
- A battery sensor is a sleepy end device. It talks only through one parent router. If that parent drops it from its child table, the sensor can go quiet for hours with no leave event and a good radio signal. Check the parent in a fresh network map, not only the LQI.
- Does adding a Zigbee router near a sensor move the sensor to it?
- No. A sleepy end device keeps its old parent until it rejoins. Open joining on the new router only, hold the sensor's pairing button, and confirm the new parent in a fresh network map.
- How should I detect a stale Zigbee sensor in Home Assistant?
- Use the Zigbee2MQTT last_seen value, not the temperature entity's last_reported time. The MQTT integration skips writes for unchanged values, so a healthy sensor that reports the same temperature looks stale.
- What should I set before a Zigbee plug firmware OTA?
- Set power_on_behavior to previous. The OTA ends in a reboot, and with the default off the plug comes back with its load switched off.