Firmware workspace for a virtual Zigbee device built from:
- Waveshare ESP32-P4-POE-ETH as the Ethernet, Web UI and Home Assistant bridge.
- Waveshare ESP32-H2-Zero-M as the Zigbee radio/device.
The MVP mirrors selected Home Assistant entities to Zigbee endpoints so a TaHoma Switch can see and control them as ordinary Zigbee devices.
This repository currently contains:
- Shared C++ model for supported Home Assistant domains.
- Shared framed protocol for P4 <-> H2 runtime traffic and H2 application OTA.
- Shared bridge logic for mapping Zigbee commands to Home Assistant service calls.
- Native CMake tests for the shared code.
- PlatformIO subprojects for P4 and H2 firmware.
- P4 HTTP API, Ethernet/HA configuration and embedded Vue/Vuetify Web UI.
- Signed stable/beta online update channels backed by GitHub Releases and GitHub Pages, with H2-first transactional installation from the P4 Web UI.
- P4 Web UI/API first-run admin password, session cookie login and CSRF protection for mutating API requests.
- Guided first-run setup, Home Assistant entity mapping, device state inspection, firmware/configuration maintenance and diagnostics.
- English and Czech UI, automatic/light/dark theme and configurable automatic status refresh.
- Page-level lazy loading keeps the initial embedded Web UI bundle small. Runtime refreshes preserve unsaved MQTT drafts, and explicit Network/MQTT refresh actions do not replace edited form values.
- Optional MQTT telemetry with TLS/authentication, Home Assistant device discovery, retained availability and event-driven bridge diagnostics. MQTT remains secondary to the direct Home Assistant integration and does not duplicate mapped entities.
- P4 to H2 runtime UART link with H2 OTA, endpoint config sync and H2-reported endpoint state monitoring.
- H2 persists the latest endpoint config and P4 can resync it during status checks if H2 restarts independently.
- H2 OTA partition layout includes ESP Zigbee NVRAM partitions (
zb_storage,zb_fct). - H2 ESP Zigbee runtime configured as a Zigbee router.
- H2 registers every mapped
switchentity as a Zigbee Mains Power Outlet and every mappedlightentity as a Zigbee On/Off Light. - Explicit Zigbee pairing control from the P4 UI with a bounded 180 second window; H2 does not automatically join an open network at an arbitrary time.
- P4 Home Assistant WebSocket state monitor with initial/fallback REST polling and P4-to-H2 On/Off state reports for On/Off-capable mapped entities.
- H2-to-P4 Zigbee On/Off command forwarding to Home Assistant
switch/lightservice calls.
The mapping UI and P4 device-state API also support sensor and
binary_sensor entities. Their endpoint plans are validated and persisted, but
the current H2 runtime activates only switch and light On/Off endpoints.
Native measurement/IAS registration and reporting for sensor and
binary_sensor remain future work. Light brightness, color temperature and RGB
are not implemented yet.
firmware/p4/ ESP32-P4 host PlatformIO project
firmware/h2/ ESP32-H2 Zigbee PlatformIO project
lib/ shared portable C++ library
tests/native/ native tests for shared logic
web/p4/ Vue 3 + Vuetify 4 Web UI and Playwright tests
docs/ implementation notes and API contracts
Detailed wiring, transport and recovery notes are available in docs/wiring.md.
The default and currently complete transport is a crossed 3.3 V UART link:
| ESP32-P4-POE-ETH | ESP32-H2-Zero-M | Purpose |
|---|---|---|
| GPIO20 (UART1 TX) | GPIO23 / U0RXD | P4 to H2 data |
| GPIO21 (UART1 RX) | GPIO24 / U0TXD | H2 to P4 data |
| GND | GND | Common signal reference |
TX must connect to RX and RX to TX. Both sides use 3.3 V logic. Power the boards through their normal inputs; do not connect 5 V/VBUS between the boards unless the power path has been deliberately verified. A common GND is required.
The default runtime baud rate is 460800 baud. P4 can probe common fallback
rates, including 115200, when talking to an older H2 firmware. The Web UI can
select the H2 link transport, but UART port and pins are currently compile-time
settings in the two platformio.ini files. The relevant P4 flags are
ZM_H2_UART_TX_GPIO and ZM_H2_UART_RX_GPIO; the H2 side uses the corresponding
crossed ZM_H2_UART_RX_GPIO and ZM_H2_UART_TX_GPIO values.
BOOT and RESET wiring is not required for normal runtime operation or resident H2 OTA. The first H2 installation and recovery from a non-booting H2 image still require a direct H2 USB/ROM flash. Automatic BOOT/RESET control from P4 is not implemented, so no default GPIOs are assigned to those signals.
The source contains an optional P4 USB-host mode and USB device diagnostics,
but it is disabled in the default build. Runtime communication and H2 flashing
over a direct P4-to-H2 USB-C cable are not implemented yet. Do not select or
wire USB-C as the only P4-H2 path for a production installation; use
uart_gpio.
Prerequisites are PlatformIO, CMake, Python 3 and Node.js/npm. The P4 build runs
npm ci automatically when web/p4/node_modules is missing.
- Flash H2 directly once so its bootloader, OTA partition table, Zigbee NVRAM partitions and application are installed.
- Flash P4, connect Ethernet and wire the UART link shown above.
- Open the IP address printed by P4 on its serial console, create the admin password and complete the Setup guide.
- Add Home Assistant mappings on the Mappings page. Endpoint changes cause H2 to rebuild its Zigbee endpoints and may trigger an H2 restart.
- Start the bounded pairing window on the H2 + Zigbee page and pair the bridge with the Zigbee coordinator/TaHoma Switch.
PlatformIO is not required for the shared-code tests:
cmake -S . -B /tmp/zigbee-mirror-build
cmake --build /tmp/zigbee-mirror-build
/tmp/zigbee-mirror-build/native_testsWeb UI verification:
cd web/p4
npm ci
npm run build
npm run test:e2eThe automatic refresh interval is configured in the user menu and pauses while the browser tab is hidden. Setup, Mappings, Network, MQTT and H2 transport forms participate in the shared unsaved-changes guard. MQTT runtime telemetry continues to refresh while its configuration draft is being edited; use the form's Discard action to reload the latest stored values deliberately.
For local UI development against a physical P4:
cd web/p4
P4_PROXY_TARGET=http://192.168.1.50 npm run devThe firmware projects are intentionally split because the two boards have different targets and partition layouts.
pio run -d firmware/p4
pio run -d firmware/h2Recommended build targets are exposed through the root Makefile:
make build-p4
make build-h2These targets copy the OTA application images into dist/ as stable filenames:
dist/firmware.p4.bin
dist/firmware.p4.factory.bin
dist/firmware.h2.bin
dist/firmware.h2.factory.bin
dist/partitions.h2.bin
firmware.*.bin files are application-only images for OTA. The factory images
contain the complete flash layout used for initial/recovery flashing. Builds are
fingerprinted and skipped when their inputs and outputs are unchanged; use
FORCE_REBUILD=1 make build-p4 or FORCE_REBUILD=1 make build-h2 to force one.
The shared base version is stored once in the root VERSION file.
PlatformIO upload targets build as needed and flash the board connected to the selected serial port:
make upload-h2 H2_PORT=/dev/ttyACM0
make upload-p4 P4_PORT=/dev/ttyACM1
make monitor-h2 H2_PORT=/dev/ttyACM0
make monitor-p4 P4_PORT=/dev/ttyACM1PORT=/dev/ttyACM0 can be used as a common override for any single-board
command. Port names are examples and must match the local machine. For a full
P4 factory/recovery flash use:
make factory-upload-p4 P4_PORT=/dev/ttyACM1
make recovery-upload-p4 P4_PORT=/dev/ttyACM1recovery-upload-p4 erases the complete P4 flash first and therefore removes
stored configuration.
The Firmware page checks signed online update channels by default. Stable
builds are published from v* tags, while every commit on main publishes a
beta prerelease. Release application images are stored as immutable GitHub
Release assets; fixed channel manifests are deployed under GitHub Pages. See
docs/online-updates.md for repository setup, release
commands, key handling and the security model.
Online installation updates and verifies H2 first, waits for H2 to report the
target version, then updates and restarts P4. It requires the uart_gpio H2
transport. Manual file upload remains available on the P4 and H2 firmware tabs
as a local recovery path.
Application images can be uploaded over the P4 HTTP API or from the Firmware
page in Web UI. The H2 firmware is sent
through the P4 bridge to the resident H2 OTA agent, so IP is the P4 API
address:
make ota-upload IP=192.168.1.50
make ota-upload-h2 IP=192.168.1.50
make ota-upload-p4 IP=192.168.1.50These targets log in to the P4 web API first and prompt for the P4 UI password when no password is provided. If the interactive password is wrong, the prompt allows up to three attempts. For non-interactive use:
OTA_PASSWORD='admin-password' make ota-upload IP=192.168.1.50
make ota-upload-p4 IP=192.168.1.50 PASSWORD='admin-password'For HTTPS P4 URLs with an untrusted local certificate, add OTA_SCHEME=https
and OTA_INSECURE=1.
ota-upload uploads H2 first and P4 second because a successful P4 OTA schedules
a P4 restart.
P4-to-H2 OTA updates only the H2 application image. If the H2 partition layout changes, connect to the H2 USB/UART port once and flash the partition table too:
make upload-h2 PORT=/dev/ttyACM0The current H2 layout requires the Zigbee NVRAM partitions zb_storage and
zb_fct. A boot log containing Failed to find zb_storage partition means the
physical H2 still has an older partition table and cannot be fixed by H2 OTA
alone.
Each local firmware build embeds build metadata. The ESP-IDF app version uses
the format <VERSION>-0.local+YYYYMMDD.HHMMSS; the low 0.local prerelease
marker ensures that beta and stable builds of the same planned version are
valid upgrades. CI release builds use the exact stable or beta release version.
The P4 API also exposes the full local ISO
timestamp in /api/health and /api/ota/status.
The MQTT page uses MQTT 3.1.1 and can publish one diagnostic Home Assistant device containing P4, Home Assistant, H2, Zigbee, synchronization, update, version, uptime and count entities. The broker connection supports username/password authentication, TLS with the built-in public CA bundle or a custom CA, a configurable discovery prefix and a configurable heartbeat. Broker passwords and custom CA material are write-only in normal API responses.
This channel is observability-only: mapped switch, light, sensor and
binary_sensor entities are not recreated over MQTT, and MQTT command topics
are not accepted. Direct Home Assistant REST/WebSocket communication remains
the source of control and confirmed state. Topic layout, discovery entities,
payload examples and setup instructions are in docs/mqtt.md.
The P4 build runs web/p4 through Vite before compiling firmware, converts the
generated dist/ files into C++ assets, and embeds them into the app image.
Use ZM_SKIP_WEBUI_BUILD=1 pio run -d firmware/p4 only for a fallback page when
Node.js dependencies are not available.
The local pio executable must have a working PlatformIO Python environment.
If it fails with ModuleNotFoundError: No module named 'platformio', reinstall
or repair PlatformIO before building firmware.