16 KiB
Reading a SONOFF SNZB-02P on a Raspberry Pi 5
The simplest setup is:
SONOFF sensor -> Zigbee USB dongle -> Zigbee2MQTT -> MQTT -> terminal/application
You do not need to open the sensor until Zigbee2MQTT is ready.
1. Connect and identify the dongle
Attach the dongle's antenna before powering it. Connect it to the Raspberry Pi, preferably through a short USB extension cable to reduce USB 3 radio interference.
Run:
lsusb
ls -l /dev/serial/by-id/
You should see a persistent device path similar to:
/dev/serial/by-id/usb-ITead_Sonoff_Zigbee_3.0_USB_Dongle_Plus_2281293efa9aef1190f1b69061ce3355-if00-port0 -> ../../ttyUSB0
Use this /dev/serial/by-id/... path instead of /dev/ttyUSB0 or
/dev/ttyACM0, because those shorter names can change after a reboot.
Check the label on the dongle to determine its adapter type:
| Dongle model | Zigbee2MQTT adapter type |
|---|---|
ZBDongle-P |
zstack |
The two dongles have similar names but contain different radio chips.
References:
2. Install the Mosquitto MQTT broker
On Raspberry Pi OS, run:
sudo apt update
sudo apt install -y mosquitto mosquitto-clients
sudo systemctl enable --now mosquitto
systemctl status mosquitto --no-pager
sudo tee /etc/mosquitto/conf.d/zigbee2mqtt.conf >/dev/null <<'EOF'
listener 1883 192.168.178.56
allow_anonymous true
EOF
and restart the service
For an initial setup confined to the Pi and a trusted LAN, the local broker is sufficient. Configure authentication before exposing MQTT outside your trusted network.
Reference: Mosquitto downloads
3. Install Docker and Docker Compose
Identify the operating system and architecture first:
grep -E '^(ID|VERSION_CODENAME)=' /etc/os-release
dpkg --print-architecture
On a Raspberry Pi 5, the architecture should normally be arm64. Select the
repository below that matches the reported ID. Do not combine an Ubuntu
codename such as noble with the Debian repository.
Install the prerequisites:
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
Ubuntu (ID=ubuntu, including VERSION_CODENAME=noble)
If an earlier attempt installed the wrong Debian source, replace it with Docker's Ubuntu source:
sudo rm -f /etc/apt/sources.list.d/docker.list /etc/apt/sources.list.d/docker.sources
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" |
sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt-get update
For Ubuntu 24.04, that correctly uses the Ubuntu repository and noble suite.
Raspberry Pi OS or Debian (ID=raspbian or ID=debian)
Add Docker's Debian source:
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt-get update
Install Docker Engine and the Compose plugin:
sudo apt-get install -y \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin
Enable Docker at boot and verify the installation:
sudo systemctl enable --now docker
sudo docker run --rm hello-world
sudo docker version
sudo docker compose version
Docker Compose is now invoked as docker compose. The old docker-compose
command is the legacy standalone version and is not needed.
Optional: run Docker without sudo
Add your current account to the docker group:
sudo usermod -aG docker "$USER"
Then log out and back in, or reboot:
sudo reboot
After reconnecting, verify that it works without sudo:
docker run --rm hello-world
docker compose version
Membership of the docker group effectively gives the account root-level
control of the machine. Only add trusted users.
If dpkg --print-architecture reports armhf, you are using a 32-bit OS. A
64-bit Raspberry Pi OS or Ubuntu installation is recommended on the Pi 5.
References:
- Install Docker Engine on Debian
- Install Docker Engine on Ubuntu
- Install Docker Engine on 32-bit Raspberry Pi OS
- Install the Docker Compose plugin
4. Run Zigbee2MQTT with Docker
Zigbee2MQTT recommends Docker because it avoids most Node.js and dependency setup problems. Its image supports the Raspberry Pi's ARM architecture.
If Docker and Docker Compose are already installed, create a working directory:
mkdir -p "$HOME/zigbee2mqtt/data"
cd "$HOME/zigbee2mqtt"
Create a file named compose.yaml containing:
services:
zigbee2mqtt:
container_name: zigbee2mqtt
image: ghcr.io/koenkk/zigbee2mqtt:latest
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- ./data:/app/data
- /run/udev:/run/udev:ro
devices:
- /dev/serial/by-id/REPLACE_WITH_YOUR_DONGLE:/dev/ttyACM0
environment:
- TZ=Europe/Amsterdam
Replace REPLACE_WITH_YOUR_DONGLE with the exact filename returned by:
ls /dev/serial/by-id/
Start Zigbee2MQTT and follow its logs:
docker compose up -d
docker compose logs -f zigbee2mqtt
From another computer, open:
http://RASPBERRY_PI_IP:8080
For example:
http://192.168.1.50:8080
The first-run onboarding page should appear. Configure these values:
MQTT server: mqtt://172.17.0.1
Serial port: /dev/ttyACM0
Adapter:
ZBDongle-P -> zstack
ZBDongle-E -> ember
If 172.17.0.1 does not reach Mosquitto, use the Raspberry Pi's LAN IP
instead. Zigbee2MQTT may detect the adapter automatically.
vi "$HOME/zigbee2mqtt/data/configuration.yaml
frontend:
enabled: true
restart the container:
sudo docker compose restart zigbee2mqtt
sudo docker compose logs -f zigbee2mqtt
References:
5. Pair the SNZB-02P sensor
In the Zigbee2MQTT web interface:
- Click Permit join.
- Open the SNZB-02P.
- Remove the battery insulation tab.
- Hold its button for about five seconds, until the LED flashes.
- Keep the sensor close to the dongle during pairing.
- Wait for Zigbee2MQTT to report that the device was successfully interviewed.
- Rename it to something simple, such as
living_room_sensor. - Turn Permit join off afterward.
The device page should now show temperature, humidity, and battery level. The SNZB-02P uses a CR2477 battery.
Reference: SNZB-02P device support
6. Read the temperature
Listen for sensor updates on the Raspberry Pi:
mosquitto_sub -h localhost \
-t 'zigbee2mqtt/living_room_sensor' \
-v
A message will look similar to:
{
"battery": 100,
"humidity": 48.2,
"linkquality": 132,
"temperature": 21.6
}
To print only the temperature, install jq if necessary:
sudo apt install -y jq
Then run:
mosquitto_sub -h localhost \
-t 'zigbee2mqtt/living_room_sensor' |
jq --unbuffered '.temperature'
Battery-powered sensors normally report periodically or when their readings
change, rather than continuously. The SNZB-02P publishes temperature in °C
and humidity as a percentage.
Reference: Zigbee2MQTT MQTT topics
7, reading with curl
curl --silent --max-time 2 "mqtt://192.168.178.247/$topic" | dd bs=1 skip=$((2 + ${#topic})) status=none
{"battery":100,"humidity":56.3,"humidity_calibration":0,"temperature":27.1,"temperature_calibration":0,"update":{"installed_version":8704,"latest_release_notes":null,"latest_source":"https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/snzb-02p_v2.2.0.ota","latest_version":8704,"state":"idle"}}
8. Store six sensors in RRD files and create graphs
Zigbee2MQTT must retain each sensor's state so the collector can obtain the latest reading without waiting for a sleeping sensor:
device_options:
retain: true
Install RRDtool and create a Python virtual environment in this project:
sudo apt install -y rrdtool python3-venv
cd "$HOME/service_zigbee"
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
Edit the supplied sensors.yaml. MQTT settings, collection timing, storage
paths, logging, all six sensors, RRD archives, and graph presentation are
configured in this one file. Replace all five CHANGE_ME topics with the
friendly names shown by Zigbee2MQTT:
nano sensors.yaml
Sensor entries whose name begins with CHANGE_ME are placeholders and are
excluded from MQTT polling, RRD creation, graphs, and the HTML dashboard. Rename
an entry when its physical sensor is ready. The marker is configurable:
collection:
excluded_name_prefix: CHANGE_ME
By default, the databases are written to /var/lib/rrd and the generated
graphs to /var/www/html/sensors. Create those directories and grant the user
running the collector ownership:
sudo install -d -o "$USER" -g "$USER" /var/lib/rrd
sudo install -d -o "$USER" -g "$USER" /var/www/html/sensors
Both paths can be changed under storage in sensors.yaml:
storage:
data_directory: /var/lib/rrd
graph_directory: /var/www/html/sensors
Test one collection cycle:
.venv/bin/python zigbee_sensors.py --once
The script creates one RRD file per sensor in the configured data directory and three PNG files per sensor in the configured graph directory:
*_day.pngcovers the last 24 hours.*_month.pngcovers the last 31 days.*_two_years.pngcovers the last two years.
Each graph contains a filled temperature area, a solid humidity line, and a dotted battery line. Initially, most of each graph is blank because no historic samples existed before the RRD was created.
Temperature uses the left axis with a default range of 0–30°C. Humidity and
battery use the right axis with a default range of 0–100%. These ranges and
labels can be changed in sensors.yaml under graph.axes. Temperature grid
labels default to five-degree intervals, and both axes display integers. Since
RRDtool ties the right axis to the left, the corresponding percentage labels
are rounded to integers. Light-grey reference lines are drawn at 15, 20, and
25°C by default and can be changed under
graph.axes.temperature.reference_lines.
The collector also generates /var/www/html/sensors/index.html. It displays
six graphs at a time and has controls for switching between the daily, monthly,
and two-year periods. It remembers the selected period and refreshes the PNGs
at the interval configured under html.refresh_seconds.
With a web server serving /var/www/html, open:
http://RASPBERRY_PI_IP/sensors/
The page settings are configurable in sensors.yaml:
html:
filename: index.html
title: Zigbee sensor history
default_period: day
refresh_seconds: 600
RRD structure settings are used only when each .rrd file is first created.
Changing rrd.step_seconds, data sources, or archives does not rewrite an
existing database; preserve or remove the old RRD deliberately before creating
a replacement.
Run the ten-minute collection loop in the foreground with:
.venv/bin/python zigbee_sensors.py
To run it automatically as a user service, copy the supplied service file. It
assumes this repository is located at $HOME/service_zigbee:
cp zigbee-sensors.service.example /etc/systemd/system
systemctl daemon-reload
systemctl enable --now zigbee-sensors.service
Inspect its status and live logs with:
systemctl --user status zigbee-rrd.service
journalctl --user -u zigbee-rrd.service -f
Graphs can be regenerated without collecting a new MQTT reading:
.venv/bin/python zigbee_sensors.py --graph-only
References:
9. Log button and remote-control events
zigbee_events.py runs continuously and logs Zigbee2MQTT messages that contain
the configured event field. Edit events.yaml and set the MQTT broker's LAN IP,
credentials if required, and the exact zigbee2mqtt/<friendly_name> topic for
each device. Between one and five entries may be enabled.
Validate the configuration without connecting to MQTT:
.venv/bin/python zigbee_events.py --check-config
For a foreground test, start the listener and then press a configured button:
.venv/bin/python zigbee_events.py
tail -f zigbee-events.log
Ordinary state messages, such as battery or link-quality reports, are ignored
unless they also contain the device's event_field. For the SONOFF SNZB-01M
Orb, leave that field set to action.
Each device's events mapping connects an event value to an allow-listed
function. Unmapped events are logged but do not call a function:
events:
single_button_1: denon.on
single_button_2: denon.off
single_button_3: denon.previous
single_button_4: denon.next
Volume actions are also available as denon.volume_up and
denon.volume_down. Each call sends five volume steps to the receiver.
denon.volume_mute explicitly mutes the main output.
The Denon receiver connection is also configured in events.yaml. Power
actions use Denon's HTTP control endpoint; set Settings → Network → Network
Control to Always On on the receiver so it remains reachable in standby:
denon:
host: 192.168.178.177
port: 8080
timeout_seconds: 3
verify_tls: true
The supplied system service assumes the project is installed at
/opt/service_zigbee:
sudo cp zigbee-events.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now zigbee-events.service
sudo systemctl status zigbee-events.service
After changing events.yaml, restart the listener:
sudo systemctl restart zigbee-events.service
HEOS favorite stations
The receiver's HEOS Favorites can be discovered and played through its CLI service on port 1255. The library resolves the player and Favorites source IDs dynamically:
python3 lib_heos.py list
python3 lib_heos.py play 1
python3 lib_heos.py play "NPO Radio 2"
Favorite numbers are the current 1-based positions shown by list. Names are
matched case-insensitively. Playing a favorite starts playback on the first
HEOS player returned by the receiver.
denon.next checks the AVR power state before changing stations. In standby it
starts favorite 1. When powered on, it advances only if the current station is
in HEOS Favorites, wrapping the final favorite back to the first. It does
nothing when another source or a non-favorite station is playing.
denon.previous has the same rules in reverse and wraps favorite 1 back to the
final favorite.