Skip to content

Zigbee Home Automation (ZHA) + SONOFF Zigbee Dongle-M

Zigbee devices — motion sensors, IKEA TRADFRI bulbs, smart plugs, smoke alarms — are bridged into Home Assistant by its built-in Zigbee Home Automation (ZHA) integration, talking to a SONOFF Zigbee Dongle-M coordinator at 192.168.1.169 over the network. This is a separate radio from the Thread side: Thread and Matter run on the SMHUB Nano MG24 at 192.168.1.168 (see Home Assistant → OTBR), while Zigbee runs on the Dongle-M.

This setup uses ZHA, not Zigbee2MQTT. There is no MQTT broker in the cluster — no Mosquitto, no mqtt: block, nothing publishing to a broker (the Home Assistant config is default_config: only). ZHA talks to the coordinator directly over TCP and stores its settings on Home Assistant's persistent volume, not in configuration.yaml. The Dongle-M's "HA/Zigbee2MQTT connected" status line is just its generic "a client is attached" label — it does not mean Zigbee2MQTT is in use.

Hardware and topology

The Dongle-M is a networked, PoE-powered coordinator — not Wi-Fi, and not a USB stick. It pairs two chips:

Chip Role Firmware track
Silicon Labs EFR32MG24 The Zigbee radio mg24/zigbee
ESP32 Networking — bridges the radio's serial output to TCP over wired Ethernet esp32

ZHA connects to the radio over the LAN as a serial-over-TCP stream — the dongle's own USB port is deliberately unused. The full path for every Zigbee message is:

Zigbee device --radio--> Dongle-M (EFR32MG24) --serial--> ESP32 --TCP over PoE--> ZHA (Home Assistant)

The coordinator's IP is reserved in UniFi (192.168.1.169). ZHA reaches the coordinator by IP, so this matters — but it is already handled by a DHCP reservation on the dongle's MAC address. The dongle log line "The device has been assigned a new IP: 192.168.1.169" after a reboot is just the lease being re-granted to that reserved address; it is not IP drift and not a failure mode here. (If the reservation is ever removed, note that ESP32 firmware before 1.0.10 has a bug where a static IP set outside the DHCP range reverts to automatic assignment after a reboot — another reason to keep it a router-side reservation.)

Connection — ZHA over the network

ZHA is configured through the Home Assistant interface (Settings → Devices & Services → Zigbee Home Automation), so the coordinator definition lives on the persistent volume (under /config/.storage), not in configuration.yaml — which is why it does not appear in this repository. The stored values are:

Radio type:  ezsp  (the EmberZNet Serial Protocol — the Silicon Labs radio stack)
Connection:  socket://192.168.1.169
Channel:     25

The address has no port on the end; ZHA instead records 6638 in a "baudrate" field — a leftover from serial-cable setups. Either way, 6638 is the port the Dongle-M's serial-over-TCP service listens on, so that is the port in use. If the connection ever fails, confirm the port in the dongle's web interface rather than assuming it.

Find the coordinator connection address

ZHA stores the coordinator address in its settings file on the persistent volume. Read it directly:

kubectl exec -n home-assistant deploy/home-assistant -- python3 -c "import json; d=json.load(open('/config/.storage/core.config_entries')); [print('connection:', e['data'].get('device',{}).get('path'), '| radio type:', e['data'].get('radio_type')) for e in d['data']['entries'] if e['domain']=='zha']"

It prints connection: socket://192.168.1.169 | radio type: ezsp. The port is not shown on the address; ZHA stores it (6638) separately, as noted above.

Firmware

The dongle carries two independent firmwares — update both from the firmware page in its web interface.

Track Keep at (stable) Why it matters
esp32 (networking) 1.0.10 1.0.10 "significantly improved UART-over-TCP connection stability", and 1.0.7 improved forwarding efficiency — both help the connection survive and re-establish cleanly after a network or coordinator reboot. 1.0.10 also fixes the IP-reservation bug noted above.
mg24/zigbee (radio) 1.0.0 (stack 7.4.5) The Zigbee radio stack.

Stay on the zigbee radio track, not multipan. Multi-protocol would share one radio between Zigbee and Thread, but this homelab runs Zigbee on the Dongle-M and Thread on the separate SMHUB — there is no reason to flash multipan here.

Verify

In the dongle's web interface (Connection State) a healthy coordinator shows TCP Connected, Internet Connected, and HA/Zigbee2MQTT connected (the generic "client attached" label). In Home Assistant, Settings → Devices & Services → Zigbee Home Automation shows the coordinator online with its devices listed. End to end: trigger a motion sensor and confirm its state changes in the HA Logbook and the light turns on in under a second.

Troubleshooting — slow or dead motion-triggered lights after a network reboot

The classic trigger is a network-equipment reboot (UniFi or the PoE switch), not a whole-house power cut. Because the PoE switch reboots, the dongle loses power and the network path at once, while Home Assistant keeps an open TCP connection to it. The mains-powered bulbs and battery sensors keep running, so the Zigbee mesh itself does not lose power. There are two distinct problems to separate — a transient connection one, and a persistent mesh one.

1. Stale ZHA ↔ coordinator connection (transient — fixes the "nothing happens" right after a reboot). When the switch and dongle dropped, ZHA's TCP connection went half-open; ZHA keeps writing into the dead connection until TCP timeouts fire (a few seconds) before it reconnects and re-initialises the radio. Force a clean reconnect:

Settings → Devices & Services → Zigbee Home Automation → (three-dot menu) → Reload (or restart Home Assistant) once the network and coordinator are back.

2. Make recovery automatic. The dependency chain is PoE switch → Dongle-M → ZHA, and nothing guarantees ZHA cleanly re-attaches to a freshly-rebooted coordinator. Add a watchdog: a connectivity check that pings 192.168.1.169, plus an automation that reloads the ZHA integration when the coordinator becomes reachable again (and on Home Assistant start). This turns a manual reload into self-healing after every network blip.

3. Keep the ESP32 firmware at 1.0.10 — its connection-stability fixes make the link more resilient to this drop-and-reconnect cycle.

4. Weak mesh routing (the persistent cause of 5–10 s delays and missed triggers). If lights are reliably late rather than dead, the bottleneck is the mesh, not the connection. A healthy mesh delivers a motion report in well under a second; multi-second delays mean messages are being retried or are triggering route re-discovery on the way to the coordinator.

  • Channel is already clean — don't change it. The coordinator runs on Zigbee channel 25, which an energy scan shows is quiet, so Wi-Fi interference is not the cause here.
  • IKEA TRADFRI bulbs are mediocre routers. Most of this mesh's routers are TRADFRI bulbs, which have small routing tables and drop child devices more readily than purpose-built routers. Add a few solid mains-powered routers (good smart plugs — the Third Reality plugs here are reasonable) near the motion sensors and any dead spots to give battery sensors a dependable parent.
  • Never switch a routing bulb off at the wall. A smart bulb only routes while powered; cutting it at a switch removes that hop and forces its children to rediscover a route — exactly the multi-second delay you see.
  • Re-pair a laggy battery motion sensor in place, next to a strong router, so it binds to a good parent. Re-pair only (via permit-join) — do not factory-reset, or you lose its automations.

Tell the connection apart from the mesh

ZHA's diagnostics (Settings → Devices & Services → Zigbee Home Automation → three-dot menu → Download diagnostics) include counters that separate the two:

  • Serial/host linkASH_OVERFLOW_ERROR, ASH_FRAMING_ERROR, ASH_OVERRUN_ERROR. Near zero means the TCP/serial link to the dongle is clean, so a persistent delay is not the connection.
  • Mesh health — a high MAC_TX_UNICAST_RETRY relative to MAC_TX_UNICAST_SUCCESS, plus non-trivial MAC_TX_UNICAST_FAILED, ROUTE_DISCOVERY_INITIATED, and NEIGHBOR_STALE, points to weak routing — apply the mesh fixes in cause 4.

Quick check from the Logbook: compare timestamps for one event — the motion sensor's state change versus the light. If the sensor state never changes, ZHA is not getting the report → connection (causes 1–3). If the sensor fires but the light lags, it is the mesh or the automation (cause 4).