ESP32-H2 firmware for a Zigbee-controlled WS2812B LED strip. Integrates with zigbee2mqtt (Home Assistant) and the Philips Hue bridge as an Extended Color Light (ZHA device type 0x010D).
| Component | Detail |
|---|---|
| MCU | ESP32-H2-DevKitM-1 |
| LED strip | WS2812B addressable LEDs |
| Data pin | GPIO8 |
| Factory reset | GPIO9 (BOOT button, hold ≥ 3 s) |
| Power | 5 V / 2 A (USB-C or dedicated supply) |
5V supply ──┬── ESP32-H2 5V pin
└── WS2812B VCC
GND ────────┬── ESP32-H2 GND
└── WS2812B GND
ESP32-H2 GPIO8 ──[300Ω]── WS2812B DIN
For permanent installations, power the strip directly from a 5 V supply and connect only GND and the data line to the ESP32-H2.
Edit src/config.h before building:
| Define | Default | Description |
|---|---|---|
LED_DATA_PIN |
8 |
GPIO connected to WS2812B DIN |
NUM_LEDS |
8 |
Number of LEDs in the strip |
COLOR_ORDER |
GRB |
Byte order (WS2812B = GRB) |
DEFAULT_BRIGHTNESS |
50 |
Power-on brightness (0–255) |
ZIGBEE_ENDPOINT |
10 |
Zigbee endpoint number |
ZIGBEE_CHANNEL |
25 |
Zigbee channel (must match coordinator) |
DEVICE_MANUFACTURER |
"JINX" |
Shown in Hue app / z2m |
DEVICE_MODEL |
"ZBLightStrip" |
Shown in Hue app / z2m |
DEVICE_FW_BUILD |
"0.1.x" |
Firmware version reported to bridge — patch number auto-increments each build; see Build versioning |
FACTORY_RESET_PIN |
9 |
BOOT button GPIO |
Requires PlatformIO.
# Build
pio run
# Flash
pio run --target upload
# Full erase + flash (required when changing Zigbee mode or partition table)
pio run --target erase
pio run --target upload
# Serial monitor
pio device monitorEvery compile automatically increments a build counter. The resulting version
string (0.1.<N>) is written to the ZCL Basic cluster attribute SWBuildID
(0x4000) and reported to the Hue bridge during device interview.
build_number.txt— plain integer, committed to source control; increment it manually to reset counting.src/build_number.h— auto-generated at build time, git-ignored. Do not edit by hand.scripts/increment_build.py— the PlatformIOpre:script that drives the increment.
To bump the major or minor version, change the "0.1." prefix in
DEVICE_FW_BUILD inside src/config.h.
The project includes a patched copy of the Arduino Zigbee library under
lib/Zigbee/. PlatformIO uses it automatically in place of the global package. Patches applied vs upstream:
TriggerEffectZCL command (ESP_ZB_CORE_IDENTIFY_EFFECT_CB_ID) — added toZigbeeHandlers.cpp; missing from upstream- Scene store / recall (
ESP_ZB_CORE_SCENES_STORE_SCENE_CB_ID/_RECALL_SCENE_CB_ID) — added toZigbeeHandlers.cpp; missing from upstream- Philips Hue TC link key —
ZigbeeCore.cppcallsesp_zb_enable_joining_to_distributed()and sets the ZLL commissioning key before stack start; without this the bridge sends an immediateZDO LeaveOn/Offcluster attributes —on_timeandglobal_scene_controladded toZigbeeColorDimmableLight; required for the Hue "Off with Effect" command (ZCL 0x40) to be processed- Color capabilities guard —
setLightColorCapabilities(0x001F)exposed; without it the internal guard silently drops all HS and CT commands from the bridge- CT range —
setLightColorTemperatureRange(153, 500)exposed; advertises the physical 2000–6500 K range to the bridge and Google Home- ZHA device type 0x010D —
app_device_idset to Extended Color Light inZigbeeColorDimmableLight; enables the colour-temperature slider in the Hue app (0x0102 suppresses it)- CT-to-RGB formula — Planckian-locus approximation replaces the old warm/cool formula; correctly maps 2000–6500 K to perceptually accurate RGB on WS2812B
- Color gamut primaries —
NumberOfPrimaries(0x0010) andPrimary1–3 X/Y/Intensity(0x0011–0x001B) added to the Color Control cluster with Hue Gamut C coordinates; allows the bridge to determine the device's color gamutonFactoryResetcallback —ZigbeeCorenow exposesonFactoryReset(fn), invoked beforeesp_zb_factory_reset()on both local button press and remote ZDO Leave; used to erase the"light"and"scenes"NVS namespaces so stale state does not survive a re-pairsetSWBuildID()— new method onZigbeeEPthat writes a ZCL character string to Basic cluster attribute 0x4000 (SWBuildID); allows the Hue bridge and zigbee2mqtt to display the firmware version string in device details
- Set
ZIGBEE_CHANNELin src/config.h to match your z2m channel (check zigbee2mqtt → Settings → Advanced → ZigBee channel) - Build and upload
- In zigbee2mqtt, click Permit join (All)
- Press the RESET button on the ESP32-H2
- The device joins within one scan cycle and appears in the Devices list
If the device has previously joined a different network, hold the BOOT button for 3 seconds after boot to factory-reset before joining.
- Set
ZIGBEE_CHANNELto match your bridge (Hue app → Settings → Hue Bridges → ⓘ → Zigbee channel — typically 15, 20, or 25) - Build and upload
- If the device has previously been on a different network, hold BOOT for 3 s to factory-reset it first
- Hue app → Settings → Light setup → Add light → Search
- The device joins automatically — no button press required during search
The firmware includes all compatibility fixes required by the Hue bridge: ZLL TC link key, ZHA device type 0x010D (Extended Color Light, required for the CT slider), On/Off cluster attributes for "Off with Effect", and color gamut primaries. No bridge configuration changes are needed.
When the device successfully joins the network it blinks white three times as visual confirmation.
When the device is removed from the bridge (ZDO Leave) or factory-reset via the BOOT button, all stored state and scenes are wiped from NVS so the next pair starts clean.
| Feature | ZCL cluster | Notes |
|---|---|---|
| On / Off | OnOff (0x0006) |
|
| Brightness | LevelControl (0x0008) |
0–255 |
| RGB colour | ColorControl (0x0300) |
Hue/Saturation, Enhanced Hue, XY |
| Colour temperature | ColorControl (0x0300) |
153–500 mireds (2000–6500 K) |
| Color Loop | ColorControl (0x0300) |
Rainbow cycle via ZCL Color Loop Set (0x44) |
| Scenes | Scenes (0x0005) |
Store / recall via Hue scenes |
| State persistence | NVS | On/RGB/level/CT/mireds saved across reboots; wiped on factory reset or network leave |
| Identify / Effects | Identify (0x0003) |
See effects table below |
Effects are triggered from the zigbee2mqtt device page (Effect dropdown) or via
ZCL TriggerEffect commands:
| ID | z2m name | LED behaviour |
|---|---|---|
| 0x00 | blink |
3 × white flash (200 ms on/off, ~1.2 s total) |
| 0x01 | breathe |
Sine-wave brightness pulse (~4 s) |
| 0x02 | okay |
Continuous rainbow cycle across all LEDs |
| 0x0b | channel_change |
Fade out → pause → fade in (1.5 s) |
| 0xfe | finish_effect |
Stop effect, restore light state |
| 0xff | stop_effect |
Stop effect, restore light state |
Any normal light command (colour, brightness, on/off) also stops an active effect.
Activate the color loop from the Hue app's scene / entertainment effects menu.
The bridge sends a ZCL Color Loop Set command (0x44) which writes the
ColorLoopActive attribute. The firmware then cycles the full hue wheel across
all LEDs continuously. Direction and speed are controlled by the ColorLoopDirection
and ColorLoopTime ZCL attributes sent with the command.
The loop runs until the bridge sends ColorLoopActive = 0, or until any normal
colour / brightness / on/off command is received.
The Hue app stores and recalls scenes using ZCL Store Scene and Recall Scene
commands. This firmware saves a complete light-state snapshot (on/off, RGB, level,
colour temperature, CT mode) in NVS keyed by (group_id, scene_id) and restores it
on recall. An arbitrary number of scenes can be stored up to the NVS capacity.
A custom partition table (partitions.csv) is required to provide the Zigbee stack storage partitions used by ZBOSS:
| Partition | Size | Purpose |
|---|---|---|
nvs |
20 KB | Arduino NVS |
otadata |
8 KB | OTA state |
app0 / app1 |
1280 KB each | OTA firmware slots |
zb_storage |
16 KB | Zigbee NVRAM (network keys, tables) |
zb_fct |
4 KB | Zigbee factory/calibration data |
coredump |
64 KB | Crash core dump |
Device does not appear in zigbee2mqtt after joining
- Click the re-interview button (↺) next to the device in z2m Devices
- Ensure the device is physically close to the coordinator during interview
Interview fails with "can not get node descriptor"
- Add to
configuration.yamlin your zigbee2mqtt config:advanced: interview_retry_timeout: 15000 node_descriptor_retrieval_timeout: 30
- Restart zigbee2mqtt
Device keeps rebooting without joining
- Check
ZIGBEE_CHANNELmatches the coordinator channel - Hold BOOT for 3 s to factory-reset and clear stale network state
- Move the device closer to the coordinator during pairing
No serial output
- Ensure the serial monitor is open before the device boots
- Confirm
ARDUINO_USB_CDC_ON_BOOT=1is set in platformio.ini