# Wake Up Light II (Tuya WBR3 / RTL8720CF) → ESPHome
Local, cloud-free ESPHome firmware and reverse-engineering notes for a **Wake Up Light II** sunrise alarm clock using a **Tuya WBR3** Wi-Fi module and a separate appliance MCU.
This work was reverse-engineered from a real unit by sniffing the original Tuya MCU UART, dumping the startup handshake, mapping datapoints one function at a time, desoldering/programming the WBR3, and fixing ESPHome's WBR3 alternate-UART pin routing.
> **Status:** usable. Main lamp, brightness, clock display mode, FM power/volume, both alarm enables/times, Alarm 1 sound/volume/wake-light/sunrise duration, sunset mode, ambient-light power, 12/24 h mode, local web UI, OTA, manual clock setting, and optional SNTP time sync are implemented. Several raw structures remain partially decoded.
That file is useful as a second source for the datapoint map, but it appears to be a **superset** of the physical unit tested here. In particular it defines Alarm 3/4 datapoints, while this lamp's physical interface and indications expose **two alarms**. This repository therefore does not assume DP123/124 are usable.
## The two controllers
### 1. Tuya WBR3 — Wi-Fi / ESPHome MCU

The WBR3 is the module being replaced with ESPHome firmware. LibreTiny identifies it as a Realtek RTL8720CF/AmebaZ2 target.
| Specification | Value |
|---|---|
| CPU | ARM Cortex-M33 / KM4 class |
| Clock | 100 MHz |
| SRAM | 256 KiB |
| Flash | 2 MiB |
| Supply | 3.0–3.6 V |
| Wi-Fi | 802.11 b/g/n, 2.4 GHz |
| LibreTiny board | `wbr3` |
| Programming UART | UART2: PA15/RX2, PA16/TX2 |
| Lamp communication UART | UART0: PA13/RX0, PA14/TX0 |
- It controls the lamp's local appliance logic: display/buttons, alarms, light modes and radio-facing state.
- It communicates with the WBR3 using the Tuya MCU serial protocol at **9600 8N1**.
- It reports product PID `ya39qahrcdhvxowz` and MCU firmware string `6.0.2` in the Tuya product-info response.
- The original device continues to operate locally when the WBR3 firmware is replaced, because ESPHome talks to this MCU instead of directly driving all lamp hardware.

## Stock Tuya UART handshake
The original WBR3 sends the standard Tuya handshake to the appliance MCU. Examples captured from this unit:
```text
WBR3 -> MCU
55 aa 00 00 00 00 ff heartbeat
55 aa 00 01 00 00 00 product-info request
55 aa 00 02 00 00 01 working-mode request
55 aa 00 03 00 01 00 03 Wi-Fi state report
MCU -> WBR3
55 aa 03 00 00 01 01 04 heartbeat reply
55 aa 03 01 00 2a ... product-info reply
```
The product payload decodes to:
```json
{"p":"ya39qahrcdhvxowz","v":"6.0.2","m":0}
```
## Datapoint map
“Confirmed” means directly observed on the physical unit. “Exact PID definition” means present in the `tuya-local` profile for `ya39qahrcdhvxowz`, but not necessarily verified on this exact hardware revision.
| 132 | string | Ambient color/effect | Exact PID definition; exposed read-only in YAML until verified |
The exact-PID `tuya-local` profile maps DP132 color strings to white/red/orange/yellow/lime/cyan/blue/magenta and also uses the same DP for an effect field. Treat that as reference information until this hardware revision is tested directly.
Copy `server-example.yaml` to the server and add `wifi_ssid`, `wifi_password`, `ap_password` and `api_encryption_key` to its `secrets.yaml`. The patched UART in `components/uart` loads automatically, so the server's ESPHome install needs no patch. It's a copy from ESPHome 2026.10.2, so recheck it when updating ESPHome. To release a change, update `ref` in `wake-up-light-ii.yaml`, tag that commit, then update `ref` in the server's YAML.
## Corrections from Tuya's data model
Tuya's data model for this family was posted in [tuya-local issue #3821](https://github.com/make-all/tuya-local/issues/3821) for the sibling product `rslo8vqc1xjtaoy9`. It matches what this unit reports, and it replaces the older notes below where they differ.
- **DP127** is 48 bytes on the unit tested here: 12 bytes per alarm, 4 alarms. Logs cut it off at 16 bytes, which likely explains the older 16-byte notes. Bytes: 0 enabled; 1-2 minutes since midnight; 3 repeat days (bit 0 = Monday, 0 = one time); 4 ring duration; 5 mode (0 sound, 1 sound + light, 2 radio, 3 radio + light); 6 sound; 7 volume; 8 radio station; 9 sunrise minutes; 10 brightness after sunrise; 11 snooze (0 allowed, 1 not).
- **DP128** is 10 bytes: 0 unused; 1 mode, as above; 2 sound 1-4; 3 volume; 4 sound duration; 5 radio station; 6 radio duration; 7 start brightness; 8 sunset duration; 9 snooze. Bytes 4, 6 and 8 should match.
- **DP107** is stations found by a scan, then the current station. **DP126** starts (1) or ends (0) a channel search.
- **DP117** and **DP132** are enums on this unit, so values count from 0. DP132 runs cycle, red, orange, yellow, green, cyan, blue, purple.
- **DP125** is online time synchronization. **DP130** is snooze.
Observed behaviour on this unit: a mode without light sets brightness to 0, and a light mode with brightness 0 falls back to no light. Alarm stations were reset to 1 while no stations had been scanned. DP127 writes are ignored while an alarm is snoozed. DP130 stays on after snoozing a ringing alarm and returns to off at once otherwise. Default light transitions flood the MCU with writes, so keep them at 0.
Older notes, see the corrections above. DP127 is 16 bytes on this unit. The safest implementation is to **cache the MCU-provided packet and modify only decoded bytes**.
| 13–14 | Alarm 2 time, big-endian minutes since midnight | confirmed |
| 15 | Alarm 2 weekday mask | confirmed field; bit order not decoded |
Time encoding examples:
```text
14:00 = 840 minutes = 0x0348 -> 03:48
15:00 = 900 minutes = 0x0384 -> 03:84
15:01 = 901 minutes = 0x0385 -> 03:85
```
Measured Alarm 1 packets include:
```text
01:03:48:7F:1E:00:01:01:04:0A:00:00:00:03:48:7F
# A1 14:00, sound 1
01:03:85:7F:1E:01:02:02:04:14:0A:00:00:03:48:7F
# A1 15:01, sound 2, volume 2, wake light 10/20, sunrise 20 min
```
An important finding: changing **Alarm 2 sound, volume, wake-light brightness and sunrise time did not change DP127**. Only Alarm 2 enable/time/day fields are confirmed here. Do not assume the Alarm 1 middle block is shared with Alarm 2.
## DP128 sunset/sleep structure
DP128 is confirmed to carry sleep/sunset settings, but its full byte map is still TODO. Two captured examples are:
```text
01:01:01:0A:0A:04:0A:0C:0A:01
01:01:02:08:14:04:14:14:14:01
```
The physical family manual describes sleep-aid duration, light brightness, sleep sound/radio and volume. More one-setting-at-a-time captures are needed before making DP128 writable.
## Why an ESPHome UART patch is required
The lamp uses **UART0 on WBR3 PA13/PA14**. LibreTiny documents PA13=RX0 and PA14=TX0, but ESPHome's LibreTiny UART component has historically selected only one board-default pin pair for each hardware UART. With stock code the Tuya component transmitted in software logs but received no replies and repeatedly failed at:
The patch file is written against the 2026.9-era ESPHome source layout. The helper script fails instead of patching blindly if the source shape changes.
### Apply the patch to a pip/Conda ESPHome install
Activate the same Python environment that runs `esphome`, then:
```bash
python3 patches/apply_uart_patch.py
esphome clean wake-up-light-ii.yaml
esphome compile wake-up-light-ii.yaml
```
A successful patched boot should log:
```text
Using alternate UART0 pins RX=PA13 TX=PA14
```
ESPHome upgrades can overwrite the patched source file, so re-check after every upgrade. If upstream issue #13596 is fixed, remove the local patch rather than stacking it on top.
## WBR3 wired programming — official LibreTiny wiring and order

Do not use the normal appliance UART (PA13/PA14) for ROM flashing. **RTL8720CF is flashed through UART2.** The following connections and sequence come from the LibreTiny WBR3/AmebaZ2 documentation.
### Connections
| PC / supply | WBR3 |
|---|---|
| USB-UART **TX** | **PA15 / RX2** |
| USB-UART **RX** | **PA16 / TX2** |
| USB-UART GND | GND |
| Stable regulated **3.3 V** | VCC |
| **3.3 V** boot strap | **PA00** |
| Leave floating or 3.3 V | **PA13**; do **not** pull it to GND |
| Brief reset to GND | **CEN** |
PA00 is on the underside of WBR3, so LibreTiny notes that the module normally needs to be **desoldered** for UART flashing.
### Official download-mode order
LibreTiny's documented sequence is:
1. Connect the USB-UART adapter.
2. Connect **PA00 to 3.3 V**.
3. Power-cycle the module **or** briefly short **CEN to GND** and release it.
4. Start the flashing process.
If a serial terminal is open at the time, successful boot into ROM download mode may print:
The supplied YAML creates a permanent standalone AP by default:
```text
SSID: Wake-Up-Light-II
IP: 192.168.4.1
Web: http://192.168.4.1/
```
There is intentionally **no `captive_portal:`** in the final YAML. With a captive portal enabled, the user can be dropped into the Wi-Fi provisioning/OTA page instead of the normal ESPHome control UI. `web_server.local: true` embeds the web assets so the control page works without Internet access.
- Writable Alarm 1 sound, volume, wake-light brightness and sunrise duration while preserving unknown DP127 bytes.
- Sunset/sleep power DP121.
- Ambient-light power DP129.
- 12/24 h mode DP131.
- Read-only diagnostics for DP107/127/128/132.
- Manual clock setting from the local web UI.
- Optional SNTP synchronization when the device has upstream network access.
- Permanent offline AP + local web server.
- Native ESPHome OTA + web-server OTA.
### Time sync note
The lamp can be used entirely offline. In standalone AP-only mode there is no route to public NTP servers, so use the **Lamp Clock** time control in the web UI. If you uncomment station Wi-Fi in the YAML, SNTP can update ESPHome's time and the firmware writes the current `HHMMSS` string back to DP103. The `tuya.time_id` integration is also enabled for standard Tuya time queries.
## Flash/RAM headroom observed
A working build during development reported approximately:
```text
IRAM symbols: ~52 bytes
RAM: 5.1% (13,369 / 262,144 bytes)
Flash: 62.2% (570,973 / 917,504 bytes)
```
So there is substantial headroom for additional local features, while still leaving sensible OTA/application margin.
## Related family manual
A Dekala/Moes-style Wake Up Light II family manual is useful for understanding the physical feature ranges (alarm sounds, volume 1–16, wake-light brightness 0–20, sunrise 10–60 minutes, sleep aid, FM 87.5–108 MHz):
Done: alarm data for all 4 alarms, Alarm 2 settings, sunset settings, ambient colour, snooze controls, grouped web UI, Home Assistant API, Home Assistant as a local time source, next/previous station, and diagnostics including versions and MCU link status.
- Make a full stock flash dump before writing custom firmware.
- WBR3 is a **3.3 V** device; do not power it from 5 V.
- Use common ground between the module supply and USB-UART adapter.
- Do not pull PA13 to GND when entering RTL8720CF download mode.
## Credits / references
Made with help of ChatGPT. This reverse-engineering work builds on ESPHome, LibreTiny, ltchiptool, Tuya's public module documentation, and the community `tuya-local` device definition.