feat(zigbee2mqtt): Zigbee2MQTT 2.14.1 on esh-docker-vm, replacing HA's ZHA

Requested by ha-dev; approved by Prime in this session. Radio: SLZB-MR1U chip 0
(EFR32MG21, EmberZNet 8.0.2) at tcp://10.0.90.10:6638, adapter ember. A fresh
network was formed on channel 25, PAN 0xCFF4. The broker is reached as mosquitto
user zigbee2mqtt, with HA discovery on homeassistant/. The frontend on :8099 is
token-protected.

State and the network key stay host-only in /opt/docker/data/zigbee2mqtt
(root 0700, restic). The repo carries only compose, .env.example and the README.
configuration.yaml refers to the secrets as !secret.yaml, and those references
survived Z2M's v4->v5 settings migration.
This commit is contained in:
vh
2026-09-27 12:43:49 -07:00
parent 47cad33dd1
commit c7b32418e1
5 changed files with 133 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
# zigbee2mqtt — copy to /opt/docker/compose/zigbee2mqtt/.env on esh-docker-vm.
# No secrets here: they live in /opt/docker/data/zigbee2mqtt/secret.yaml (root 0600) and the vault.
IMAGE=koenkk/zigbee2mqtt:2.14.1@sha256:fef0de769dcd04c27b3a6d277b61046eb96284bdd4198dcb1687c3a01b3020f3
FRONTEND_PORT=8099
+67
View File
@@ -0,0 +1,67 @@
# zigbee2mqtt
The **house Zigbee stack** on esh-docker-vm, replacing Home Assistant's ZHA. It was
requested by ha-dev and approved by Prime on 2026-09-27. It started greenfield: the
Zigbee network had no devices, so nothing was migrated.
| | |
|---|---|
| **Frontend** | `http://10.0.50.45:8099`, protected by an auth token (vault `esh-docker-vm/zigbee2mqtt-frontend-token`) |
| **Image** | `koenkk/zigbee2mqtt:2.14.1@sha256:fef0de76…` (`.env` `IMAGE`, digest-pinned) |
| **Radio** | SLZB-MR1U `10.0.90.10` (VLAN 90), chip 0 = EFR32MG21 / EmberZNet 8.0.2, `tcp://10.0.90.10:6638`, adapter `ember`. Chip 1 (CC2652P7, :6639) is unused. |
| **Network** | channel 25, PAN ID **0xCFF4** (53236), extended PAN ID `0xd0cbe1735919b497`, coordinator IEEE `0x187a3efffe99a487`. Formed fresh on 2026-09-27 (the EFR32 left the abandoned ZHA network: channel 25, PAN 0xF09F). |
| **MQTT** | `mqtt://mosquitto:1883` over `traefik-net`, as broker user `zigbee2mqtt` (vault `esh-docker-vm/zigbee2mqtt-mqtt-password`). Base topic `zigbee2mqtt`, HA discovery on `homeassistant/`. |
| **State** | `/opt/docker/data/zigbee2mqtt` (root 0700): `configuration.yaml`, `secret.yaml`, `coordinator_backup.json`, `database.db`. Backed up by the host's restic (`/opt/docker`). |
## ⚠ The network key
`secret.yaml` holds the Zigbee **network key**, which encrypts the whole mesh. The same
key is in the vault as `esh-docker-vm/zigbee2mqtt-network-key`. If it is lost, every
device must be re-paired. It must never be in git. That is why this repo holds only
`compose.yaml`, `.env.example` and this README; the data dir is host-only and
`deploy-stack.sh` never touches it. `configuration.yaml` refers to the secrets as
`'!secret.yaml <key>'`.
## Notes
- **One client per radio socket.** HA's ZHA must stay disabled or deleted while Z2M
owns 6638.
- **Configured by IP.** Containers here cannot resolve `*.internal`. The SLZB's mDNS
TXT record advertises `radio_type znp` for 6638, which is wrong: 6638 speaks EZSP/Ember.
- **Pairing is timed.** Z2M 2.x has no `permit_join` setting. Joining is opened from
the frontend or over MQTT, and closes on a timer.
- The broker user was added with `mosquitto_passwd` + `SIGHUP`; the password file's
owner and mode are unchanged (1883, 0600). The broker still has
`allow_anonymous true`, a pre-existing setting that is not this stack's to change.
## Acceptance (2026-09-27, first start at 1240)
- Z2M 2.14.1 on EmberZNet 8.0.2 (EZSP 14). herdsman reported "Adapter network does not match
config. Leaving network...", found no backup, and formed a new network on channel 25:
PAN 0xCFF4. It wrote `coordinator_backup.json`.
- `zigbee2mqtt/bridge/state` = `{"state":"online"}`, and the bridge's 8 HA discovery configs are
retained under `homeassistant/+/1221051039810110150109113116116_0x187a3efffe99a487/…`. Whether
the device shows up in HA is ha-dev's to confirm.
- On startup Z2M migrated the settings to `version: 5`. All three `!secret.yaml` references
survived the rewrite, so no key is in plaintext in `configuration.yaml`. The pre-migration file
is kept as `configuration_backup_v4.yaml`.
- Frontend: HTTP 200. The websocket accepts the vault token, and closes with `4401 Unauthorized`
on a wrong token or no token.
- Broker: the new user is accepted, a wrong password is refused, and the reload disconnected no
one. Afterwards 3 clients were connected (HA, the `denspots` Tasmota, Z2M). The Tasmotas
`consoletree`, `fireplacetree` and `mantlelights` show LWT `Offline`, but they have not connected
at any point in the broker's current log (back to 2026-09-09), so that predates this change.
## Deploy
```bash
scripts/deploy-stack.sh esh-docker-vm zigbee2mqtt --compose
# on the host, first time only:
# /opt/docker/compose/zigbee2mqtt is lkraven:docker 2755, so infra-ops cannot `cp` into it
cd /opt/docker/compose/zigbee2mqtt && sudo -n install -o lkraven -g docker -m 644 .env.example .env \
&& docker compose config -q && docker compose up -d
```
To upgrade, change `IMAGE` in the host `.env` to a new `tag@digest`, then run
`docker compose up -d`. Read the release notes first: an adapter or firmware change can
require re-pairing.
+48
View File
@@ -0,0 +1,48 @@
# zigbee2mqtt: the house Zigbee stack on esh-docker-vm, replacing Home Assistant's ZHA.
# Requested by ha-dev, operator-approved (Prime, 2026-09-27). Greenfield: no devices migrated.
#
# Radio: SLZB-MR1U at 10.0.90.10 (VLAN 90), chip 0 = EFR32MG21 / EmberZNet 8.0.2 on TCP 6638,
# so adapter `ember`. Chip 1 (CC2652P7, :6639) is unused. By IP on purpose: containers here
# cannot resolve *.internal, and the SLZB's mDNS TXT wrongly advertises radio_type znp for 6638.
# ONE client per radio socket: HA's ZHA entry must stay disabled/deleted while this runs.
#
# ⚠ STATE, and the Zigbee NETWORK KEY, live in /opt/docker/data/zigbee2mqtt on the host (root
# 0700): configuration.yaml, secret.yaml (network key, MQTT password, frontend token),
# coordinator_backup.json, database.db. Covered by the host's restic (/opt/docker). NEVER in
# git: this repo holds only this file, .env.example and the README. Losing the network key
# means re-pairing every device, so the vault holds a copy (see README).
#
# .env (tunables): IMAGE, FRONTEND_PORT.
name: zigbee2mqtt
services:
zigbee2mqtt:
image: ${IMAGE:?set IMAGE}
container_name: zigbee2mqtt
restart: unless-stopped
environment:
TZ: America/Los_Angeles
volumes:
- /opt/docker/data/zigbee2mqtt:/app/data
ports:
- "${FRONTEND_PORT:-8099}:8080"
networks:
- tnet # reaches the broker as mqtt://mosquitto:1883
labels:
- homepage.group=Apps
- homepage.name=Zigbee2MQTT
- homepage.icon=si-zigbee2mqtt
- homepage.description=Zigbee stack (SLZB-MR1U, channel 25)
- homepage.href=http://10.0.50.45:${FRONTEND_PORT:-8099}
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
networks:
tnet:
name: traefik-net
external: true