Initial ESPHome port for Wake Up Light II
This commit is contained in:
commit
086a040802
10 files changed
+1132
No files matched your search
@@ -0,0 +1,8 @@
|
|||||||
|
# Gitignore settings for ESPHome
|
||||||
|
# This is an example and may include too much for your use-case.
|
||||||
|
# You can modify this file to suit your needs.
|
||||||
|
*.log
|
||||||
|
__pycache__/
|
||||||
|
.DS_Store
|
||||||
|
/.esphome/
|
||||||
|
/secrets.yaml
|
||||||
@@ -0,0 +1,442 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Identified device
|
||||||
|
|
||||||
|
The stock MCU product-information reply is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
55 aa 03 01 ...
|
||||||
|
{"p":"ya39qahrcdhvxowz","v":"6.0.2","m":0}
|
||||||
|
```
|
||||||
|
|
||||||
|
So the identifiers found on this unit are:
|
||||||
|
|
||||||
|
| Item | Value |
|
||||||
|
|---|---|
|
||||||
|
| Product name | Wake Up Light II |
|
||||||
|
| Tuya PID | `ya39qahrcdhvxowz` |
|
||||||
|
| Appliance-MCU firmware string | `6.0.2` |
|
||||||
|
| Wi-Fi module | Tuya `WBR3` |
|
||||||
|
| Wireless SoC / LibreTiny target | Realtek `RTL8720CF`, AmebaZ2 |
|
||||||
|
| ESPHome board | `wbr3` |
|
||||||
|
| Main-MCU protocol | TuyaMCU serial protocol |
|
||||||
|
| Main-MCU UART | 9600 baud, 8 data bits, no parity, 1 stop bit |
|
||||||
|
| WBR3 MCU-UART pins | PA13 = RX0, PA14 = TX0 |
|
||||||
|
| Flash layout | Tuya RTL8720CF layout, 2 × 896 KiB OTA application slots |
|
||||||
|
|
||||||
|
The current `tuya-local` device definition contains **this exact PID** and identifies it as **Wake Up Light II / ACA002-II-WWA**:
|
||||||
|
|
||||||
|
- Source: <https://github.com/make-all/tuya-local/blob/main/custom_components/tuya_local/devices/moes_zcjk_alarmclock.yaml>
|
||||||
|
- `tuya-local` project: <https://github.com/make-all/tuya-local>
|
||||||
|
|
||||||
|
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 |
|
||||||
|
|
||||||
|
References:
|
||||||
|
|
||||||
|
- LibreTiny WBR3 board page: <https://docs.libretiny.eu/boards/wbr3/>
|
||||||
|
- LibreTiny RTL8720CF/AmebaZ2 flashing guide: <https://docs.libretiny.eu/docs/platform/realtek-ambz2/>
|
||||||
|
- Tuya WBR3 datasheet: <https://developer.tuya.com/en/docs/iot/wbr3-module-datasheet?id=K9dujs2k5nriy>
|
||||||
|
|
||||||
|
### 2. Main appliance MCU — IC1
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
What is known experimentally:
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
| DP | Tuya type | Meaning | Status / notes |
|
||||||
|
|---:|---|---|---|
|
||||||
|
| 101 | bool | Main/dusk light power | **Confirmed** |
|
||||||
|
| 102 | integer | Main light brightness | **Confirmed**, range 10–1000. Observed steps: 10, 60, 110, 160, 210, 270, 320, 370, 420, 470, 530, 580, 630, 680, 730, 790, 840, 890, 940, 1000 |
|
||||||
|
| 103 | string | Clock time | **Confirmed**, `HHMMSS`, e.g. `173700` = 17:37:00; writable |
|
||||||
|
| 104 | integer | Clock/display brightness mode | **Confirmed** 0=off, 1=day/on, 3=auto; exact-PID definition also gives 2=night |
|
||||||
|
| 105 | bool | FM radio power | **Confirmed** |
|
||||||
|
| 106 | integer | Radio volume | **Confirmed**, 1–16 |
|
||||||
|
| 107 | raw/base64 | FM station/preset data | **Confirmed present**, encoding not decoded; examples `14:02`, `14:04`, `14:05`, `14:06` |
|
||||||
|
| 109 | bool | Alarm 1 enable | **Confirmed** |
|
||||||
|
| 116 | integer | Snooze duration | Exact PID definition only, 8–15 min |
|
||||||
|
| 117 | string | Snooze action | Exact PID definition only |
|
||||||
|
| 121 | bool | Sunset / sleep-aid mode | **Confirmed** |
|
||||||
|
| 122 | bool | Alarm 2 enable | **Confirmed** |
|
||||||
|
| 123 | bool | Alarm 3 | Exact PID definition only; **not exposed by this physical unit** |
|
||||||
|
| 124 | bool | Alarm 4 | Exact PID definition only; **not exposed by this physical unit** |
|
||||||
|
| 125 | bool | Network-time setting | Exact PID definition only |
|
||||||
|
| 126 | bool/button | Radio seek/stop | Exact PID definition only |
|
||||||
|
| 127 | raw | Alarm configuration structure | **Confirmed and partially decoded**, see below |
|
||||||
|
| 128 | raw | Sunset/sleep configuration | **Confirmed**, partially decoded only |
|
||||||
|
| 129 | bool | Ambient/color-light power | **Confirmed**; exact PID definition agrees |
|
||||||
|
| 130 | bool | Snooze | Exact PID definition only |
|
||||||
|
| 131 | bool | 12/24-hour mode | **Confirmed**, false/off=12 h, true/on=24 h |
|
||||||
|
| 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.
|
||||||
|
|
||||||
|
## DP127 alarm structure
|
||||||
|
|
||||||
|
DP127 is 16 bytes on this unit. The safest implementation is to **cache the MCU-provided packet and modify only decoded bytes**.
|
||||||
|
|
||||||
|
| Byte(s) | Meaning | Confidence |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 | Alarm 1 enable | confirmed |
|
||||||
|
| 1–2 | Alarm 1 time, big-endian minutes since midnight | confirmed |
|
||||||
|
| 3 | Alarm 1 weekday mask | confirmed field; bit-to-weekday order not decoded; `0x7F` = all days observed |
|
||||||
|
| 4 | Unknown; `0x1E` commonly observed | unknown |
|
||||||
|
| 5 | Alarm 1 wake-light enable | confirmed, 0/1 |
|
||||||
|
| 6 | Alarm 1 sound | confirmed: 1–7 built-in, 8=FM; family manual documents 0=no sound |
|
||||||
|
| 7 | Alarm 1 volume | confirmed, 1–16 |
|
||||||
|
| 8 | Unknown; `0x04` commonly observed | unknown |
|
||||||
|
| 9 | Sunrise lead/duration in minutes | confirmed; 10 and 20 observed; family manual says 10–60 min |
|
||||||
|
| 10 | Wake-light brightness | confirmed, 0–20; 0=off |
|
||||||
|
| 11 | Unknown | unknown |
|
||||||
|
| 12 | Alarm 2 enable | confirmed |
|
||||||
|
| 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:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Initialization failed at init_state 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Relevant upstream references:
|
||||||
|
|
||||||
|
- ESPHome issue #13596: <https://github.com/esphome/esphome/issues/13596>
|
||||||
|
- Closed PR #13629 (“libretiny uart now allows more than one set of pins per uart”): <https://github.com/esphome/esphome/pull/13629>
|
||||||
|
|
||||||
|
The PR moved toward LibreTiny's `setPins()` API but was not merged. The targeted workaround used here does two things:
|
||||||
|
|
||||||
|
1. Accepts `(TX=PA14, RX=PA13)` as hardware UART0.
|
||||||
|
2. Calls `Serial0.setPins(rx_pin, tx_pin)` before the normal two-argument `begin()`.
|
||||||
|
|
||||||
|
> This is a **UART patch, not an I²C patch**.
|
||||||
|
|
||||||
|
Patch files in this repository:
|
||||||
|
|
||||||
|
- [`patches/wbr3-pa13-pa14-uart.patch`](patches/wbr3-pa13-pa14-uart.patch)
|
||||||
|
- [`patches/apply_uart_patch.py`](patches/apply_uart_patch.py)
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Open Download Mode on UART
|
||||||
|
```
|
||||||
|
|
||||||
|
Authoritative references:
|
||||||
|
|
||||||
|
- WBR3 quick flashing guide: <https://docs.libretiny.eu/boards/wbr3/#quick-flashing-guide>
|
||||||
|
- RTL8720CF/AmebaZ2 flashing guide: <https://docs.libretiny.eu/docs/platform/realtek-ambz2/#flashing>
|
||||||
|
|
||||||
|
## Backup, compile and flash
|
||||||
|
|
||||||
|
Install tools (preferably in a venv/Conda environment):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m pip install -U esphome ltchiptool
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1. Back up the stock WBR3 flash first
|
||||||
|
|
||||||
|
Enter download mode using the sequence above, then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ltchiptool list families
|
||||||
|
ltchiptool flash read ambz2 wake-up-light-stock.bin -d /dev/ttyUSB0
|
||||||
|
sha256sum wake-up-light-stock.bin
|
||||||
|
ls -lh wake-up-light-stock.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
WBR3 has 2 MiB flash, so preserve that dump somewhere safe.
|
||||||
|
|
||||||
|
### 2. Apply the UART patch and compile
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 patches/apply_uart_patch.py
|
||||||
|
esphome clean wake-up-light-ii.yaml
|
||||||
|
esphome compile wake-up-light-ii.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Find the generated UF2:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
find .esphome/build/wake-up-light-ii -name firmware.uf2 -type f -print
|
||||||
|
```
|
||||||
|
|
||||||
|
You can ask `ltchiptool` to identify it before writing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ltchiptool flash file \
|
||||||
|
.esphome/build/wake-up-light-ii/.pioenvs/wake-up-light-ii/firmware.uf2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Write ESPHome over UART2
|
||||||
|
|
||||||
|
Re-enter download mode and flash:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ltchiptool flash write \
|
||||||
|
.esphome/build/wake-up-light-ii/.pioenvs/wake-up-light-ii/firmware.uf2 \
|
||||||
|
-d /dev/ttyUSB0
|
||||||
|
```
|
||||||
|
|
||||||
|
`ltchiptool` documentation recommends the generated LibreTiny `.uf2` and auto-detects the correct file type/offsets:
|
||||||
|
<https://docs.libretiny.eu/docs/flashing/tools/ltchiptool/>
|
||||||
|
|
||||||
|
### 4. Re-solder WBR3 and use OTA afterwards
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Native ESPHome OTA:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
esphome upload wake-up-light-ii.yaml --device 192.168.4.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Web-server OTA if explicitly needed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
esphome upload wake-up-light-ii.yaml \
|
||||||
|
--device 192.168.4.1 \
|
||||||
|
--ota-platform web_server
|
||||||
|
```
|
||||||
|
|
||||||
|
## ESPHome configuration
|
||||||
|
|
||||||
|
The current self-contained configuration is:
|
||||||
|
|
||||||
|
- [`wake-up-light-ii.yaml`](wake-up-light-ii.yaml)
|
||||||
|
|
||||||
|
It includes:
|
||||||
|
|
||||||
|
- WBR3 / RTL8720CF board definition.
|
||||||
|
- Patched UART0 on PA13/PA14 at 9600 8N1.
|
||||||
|
- TuyaMCU integration.
|
||||||
|
- Main light DP101/102.
|
||||||
|
- Writable lamp clock DP103.
|
||||||
|
- Clock display mode DP104.
|
||||||
|
- FM power DP105 and volume DP106.
|
||||||
|
- Alarm 1/2 enable and writable times.
|
||||||
|
- 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):
|
||||||
|
|
||||||
|
<https://device.report/m/a5d28aca7037b8e58e776419b04c3402ced77f285a16ca3fce887c406d7b0bd2.pdf>
|
||||||
|
|
||||||
|
That manual is for a related model/revision, so use it as behavioral reference rather than proof of identical electronics.
|
||||||
|
|
||||||
|
## TODO / worthwhile additions
|
||||||
|
|
||||||
|
For this lamp, the most worthwhile additions are:
|
||||||
|
|
||||||
|
- **Finish DP127 decoding:** weekday-bit order and unknown bytes 4, 8 and 11.
|
||||||
|
- **Find Alarm 2 extended settings:** sound, volume, wake-light level and sunrise duration are stored somewhere other than the DP127 middle block on this unit.
|
||||||
|
- **Fully decode DP128:** make sunset/sleep duration, brightness, sound/radio choice and volume writable.
|
||||||
|
- **Decode DP107:** turn the raw FM payload into actual frequency/preset information and add previous/next/scan controls; verify DP126.
|
||||||
|
- **Ambient/color lamp control:** verify DP132 on this revision and expose color/effect controls rather than only DP129 power.
|
||||||
|
- **Snooze:** verify DP116/117/130 from the exact-PID definition and expose them safely.
|
||||||
|
- **Better local web UI:** move to Web Server v3 and group Light / Clock / Alarm 1 / Alarm 2 / Radio / Sunset / Diagnostics.
|
||||||
|
- **Fully autonomous schedules:** add software-only extra alarms or weekday schedules without depending on Tuya cloud/app limits.
|
||||||
|
- **Better clock correction:** configurable local NTP server, LAN-only synchronization, and optional drift measurement/correction.
|
||||||
|
- **Astronomical sunrise/sunset:** use ESPHome's local sun calculations for real dusk/dawn behavior.
|
||||||
|
- **Home Assistant native API:** optional `api:` support while retaining the standalone web UI.
|
||||||
|
- **Diagnostics:** Wi-Fi RSSI, uptime, reboot reason, ESPHome/LibreTiny version, Tuya-link state and raw-packet viewer.
|
||||||
|
- **Identify IC1:** obtain a board with readable top marking or otherwise identify the main appliance MCU.
|
||||||
|
- **Upstream the UART fix:** replace the local PA13/PA14 workaround when ESPHome gains proper LibreTiny alternate-UART pin mapping.
|
||||||
|
|
||||||
|
## Safety / recovery notes
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
- ESPHome: <https://esphome.io/>
|
||||||
|
- ESPHome Tuya MCU component: <https://esphome.io/components/tuya/>
|
||||||
|
- ESPHome Web Server: <https://esphome.io/components/web_server/>
|
||||||
|
- LibreTiny WBR3: <https://docs.libretiny.eu/boards/wbr3/>
|
||||||
|
- LibreTiny AmebaZ2: <https://docs.libretiny.eu/docs/platform/realtek-ambz2/>
|
||||||
|
- ltchiptool: <https://docs.libretiny.eu/docs/flashing/tools/ltchiptool/>
|
||||||
|
- Tuya WBR3 datasheet: <https://developer.tuya.com/en/docs/iot/wbr3-module-datasheet?id=K9dujs2k5nriy>
|
||||||
|
- `tuya-local` exact-PID definition: <https://github.com/make-all/tuya-local/blob/main/custom_components/tuya_local/devices/moes_zcjk_alarmclock.yaml>
|
||||||
|
- ESPHome issue #13596: <https://github.com/esphome/esphome/issues/13596>
|
||||||
|
- ESPHome PR #13629: <https://github.com/esphome/esphome/pull/13629>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 374 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 286 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 299 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 160 KiB |
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Apply the Wake Up Light II WBR3 PA13/PA14 UART0 workaround to installed ESPHome.
|
||||||
|
|
||||||
|
This is intentionally narrow: it only adds the alternate PA13/PA14 UART0 mapping
|
||||||
|
needed by this lamp, then calls LibreTiny Serial0.setPins() before begin().
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
import esphome
|
||||||
|
except ImportError:
|
||||||
|
sys.exit("ERROR: ESPHome is not importable in this Python environment")
|
||||||
|
|
||||||
|
p = Path(esphome.__file__).parent / "components" / "uart" / "uart_component_libretiny.cpp"
|
||||||
|
if not p.exists():
|
||||||
|
sys.exit(f"ERROR: not found: {p}")
|
||||||
|
|
||||||
|
s = p.read_text()
|
||||||
|
marker = 'Using alternate UART0 pins RX=PA13 TX=PA14'
|
||||||
|
if marker in s:
|
||||||
|
print(f"Already patched: {p}")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
old_select = '''#if LT_HW_UART0
|
||||||
|
else if ((tx_pin == -1 || tx_pin == PIN_SERIAL0_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL0_RX) &&
|
||||||
|
!should_fallback_to_software_serial()) {
|
||||||
|
this->serial_ = &Serial0;
|
||||||
|
this->hardware_idx_ = 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
'''
|
||||||
|
new_select = '''#if LT_HW_UART0
|
||||||
|
else if ((((tx_pin == -1 || tx_pin == PIN_SERIAL0_TX) &&
|
||||||
|
(rx_pin == -1 || rx_pin == PIN_SERIAL0_RX)) ||
|
||||||
|
(tx_pin == 14 && rx_pin == 13)) &&
|
||||||
|
!should_fallback_to_software_serial()) {
|
||||||
|
this->serial_ = &Serial0;
|
||||||
|
this->hardware_idx_ = 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
'''
|
||||||
|
|
||||||
|
old_begin = ' this->serial_->begin(this->baud_rate_, get_config());\n'
|
||||||
|
new_begin = ''' if (this->hardware_idx_ == 0 && tx_pin == 14 && rx_pin == 13) {
|
||||||
|
ESP_LOGI(TAG, "Using alternate UART0 pins RX=PA13 TX=PA14");
|
||||||
|
Serial0.setPins(rx_pin, tx_pin);
|
||||||
|
}
|
||||||
|
this->serial_->begin(this->baud_rate_, get_config());
|
||||||
|
'''
|
||||||
|
|
||||||
|
if old_select not in s:
|
||||||
|
sys.exit(
|
||||||
|
"ERROR: UART0 selection block not found. ESPHome source has changed; "
|
||||||
|
"do not patch blindly. Compare with ESPHome issue #13596 / PR #13629."
|
||||||
|
)
|
||||||
|
if old_begin not in s:
|
||||||
|
sys.exit("ERROR: serial begin() line not found; ESPHome source has changed")
|
||||||
|
|
||||||
|
backup = p.with_suffix(p.suffix + ".wake-up-light-backup")
|
||||||
|
if not backup.exists():
|
||||||
|
shutil.copy2(p, backup)
|
||||||
|
print(f"Backup: {backup}")
|
||||||
|
|
||||||
|
s = s.replace(old_select, new_select, 1)
|
||||||
|
s = s.replace(old_begin, new_begin, 1)
|
||||||
|
p.write_text(s)
|
||||||
|
print(f"Patched: {p}")
|
||||||
|
print("Run: esphome clean wake-up-light-ii.yaml && esphome compile wake-up-light-ii.yaml")
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
--- a/esphome/components/uart/uart_component_libretiny.cpp
|
||||||
|
+++ b/esphome/components/uart/uart_component_libretiny.cpp
|
||||||
|
@@ -64,8 +64,10 @@ void LibreTinyUARTComponent::setup() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#if LT_HW_UART0
|
||||||
|
- else if ((tx_pin == -1 || tx_pin == PIN_SERIAL0_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL0_RX) &&
|
||||||
|
- !should_fallback_to_software_serial()) {
|
||||||
|
+ else if ((((tx_pin == -1 || tx_pin == PIN_SERIAL0_TX) &&
|
||||||
|
+ (rx_pin == -1 || rx_pin == PIN_SERIAL0_RX)) ||
|
||||||
|
+ (tx_pin == 14 && rx_pin == 13)) &&
|
||||||
|
+ !should_fallback_to_software_serial()) {
|
||||||
|
this->serial_ = &Serial0;
|
||||||
|
this->hardware_idx_ = 0;
|
||||||
|
}
|
||||||
|
@@ -111,6 +113,10 @@ void LibreTinyUARTComponent::setup() {
|
||||||
|
return;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
+ if (this->hardware_idx_ == 0 && tx_pin == 14 && rx_pin == 13) {
|
||||||
|
+ ESP_LOGI(TAG, "Using alternate UART0 pins RX=PA13 TX=PA14");
|
||||||
|
+ Serial0.setPins(rx_pin, tx_pin);
|
||||||
|
+ }
|
||||||
|
this->serial_->begin(this->baud_rate_, get_config());
|
||||||
|
}
|
||||||
|
void LibreTinyUARTComponent::dump_config() {
|
||||||
@@ -0,0 +1,586 @@
|
|||||||
|
###############################################################################
|
||||||
|
# Wake Up Light II -> ESPHome
|
||||||
|
# Tuya PID: ya39qahrcdhvxowz
|
||||||
|
# Hardware observed in this unit: Tuya WBR3 / Realtek RTL8720CF
|
||||||
|
# Main appliance MCU <-> WBR3: TuyaMCU protocol, UART0, 9600 8N1
|
||||||
|
# WBR3 PA13 = RX0 (from appliance MCU)
|
||||||
|
# WBR3 PA14 = TX0 (to appliance MCU)
|
||||||
|
#
|
||||||
|
# IMPORTANT: stock ESPHome/LibreTiny UART selection may not route UART0 to
|
||||||
|
# PA13/PA14 on WBR3. Apply patches/wbr3-pa13-pa14-uart.patch (or the helper
|
||||||
|
# script in patches/) before compiling if Tuya stays at init_state 0.
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
substitutions:
|
||||||
|
device_name: wake-up-light-ii
|
||||||
|
friendly_name: "Wake Up Light II"
|
||||||
|
ap_ssid: "Wake-Up-Light-II"
|
||||||
|
ap_password: "wakeup1234" # Change before publishing/regular use.
|
||||||
|
timezone: "Europe/Berlin" # Change to your IANA timezone.
|
||||||
|
|
||||||
|
esphome:
|
||||||
|
name: ${device_name}
|
||||||
|
friendly_name: ${friendly_name}
|
||||||
|
|
||||||
|
rtl87xx:
|
||||||
|
board: wbr3
|
||||||
|
|
||||||
|
# Disable serial logging: PA13/PA14 belong to the Tuya MCU link.
|
||||||
|
logger:
|
||||||
|
baud_rate: 0
|
||||||
|
level: DEBUG
|
||||||
|
|
||||||
|
uart:
|
||||||
|
id: tuya_uart
|
||||||
|
rx_pin: PA13
|
||||||
|
tx_pin: PA14
|
||||||
|
baud_rate: 9600
|
||||||
|
data_bits: 8
|
||||||
|
parity: NONE
|
||||||
|
stop_bits: 1
|
||||||
|
|
||||||
|
# Useful while reverse-engineering additional datapoints:
|
||||||
|
# debug:
|
||||||
|
# direction: BOTH
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Cached state
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
globals:
|
||||||
|
# DP127 is a 16-byte alarm structure on this unit. Never build it from
|
||||||
|
# scratch: cache what the appliance MCU reported and only edit known bytes.
|
||||||
|
- id: alarm_raw
|
||||||
|
type: uint8_t[16]
|
||||||
|
restore_value: no
|
||||||
|
|
||||||
|
- id: alarm_raw_valid
|
||||||
|
type: bool
|
||||||
|
restore_value: no
|
||||||
|
initial_value: 'false'
|
||||||
|
|
||||||
|
- id: lamp_clock_hour
|
||||||
|
type: uint8_t
|
||||||
|
restore_value: no
|
||||||
|
initial_value: '0'
|
||||||
|
|
||||||
|
- id: lamp_clock_minute
|
||||||
|
type: uint8_t
|
||||||
|
restore_value: no
|
||||||
|
initial_value: '0'
|
||||||
|
|
||||||
|
- id: lamp_clock_second
|
||||||
|
type: uint8_t
|
||||||
|
restore_value: no
|
||||||
|
initial_value: '0'
|
||||||
|
|
||||||
|
- id: lamp_clock_valid
|
||||||
|
type: bool
|
||||||
|
restore_value: no
|
||||||
|
initial_value: 'false'
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Optional network time
|
||||||
|
#
|
||||||
|
# SNTP works only if the lamp can reach an NTP server. In AP-only operation
|
||||||
|
# there is no upstream network, so use the writable "Lamp Clock" control.
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
time:
|
||||||
|
- platform: sntp
|
||||||
|
id: network_time
|
||||||
|
timezone: ${timezone}
|
||||||
|
servers:
|
||||||
|
- 0.pool.ntp.org
|
||||||
|
- 1.pool.ntp.org
|
||||||
|
- 2.pool.ntp.org
|
||||||
|
update_interval: 1h
|
||||||
|
|
||||||
|
on_time_sync:
|
||||||
|
then:
|
||||||
|
- delay: 2s
|
||||||
|
- script.execute: sync_lamp_clock
|
||||||
|
|
||||||
|
on_time:
|
||||||
|
- seconds: 0
|
||||||
|
minutes: 0
|
||||||
|
then:
|
||||||
|
- script.execute: sync_lamp_clock
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Tuya MCU
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
tuya:
|
||||||
|
id: tuya_mcu
|
||||||
|
time_id: network_time
|
||||||
|
|
||||||
|
on_datapoint_update:
|
||||||
|
# DP103: ASCII HHMMSS, for example "173700" = 17:37:00.
|
||||||
|
- sensor_datapoint: 103
|
||||||
|
datapoint_type: string
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
if (x.size() != 6) return;
|
||||||
|
auto d = [](char c) -> int { return (c >= '0' && c <= '9') ? c - '0' : -1; };
|
||||||
|
int h1=d(x[0]), h2=d(x[1]), m1=d(x[2]), m2=d(x[3]), s1=d(x[4]), s2=d(x[5]);
|
||||||
|
if (h1 < 0 || h2 < 0 || m1 < 0 || m2 < 0 || s1 < 0 || s2 < 0) return;
|
||||||
|
uint8_t hh = h1 * 10 + h2;
|
||||||
|
uint8_t mm = m1 * 10 + m2;
|
||||||
|
uint8_t ss = s1 * 10 + s2;
|
||||||
|
if (hh > 23 || mm > 59 || ss > 59) return;
|
||||||
|
id(lamp_clock_hour) = hh;
|
||||||
|
id(lamp_clock_minute) = mm;
|
||||||
|
id(lamp_clock_second) = ss;
|
||||||
|
id(lamp_clock_valid) = true;
|
||||||
|
char buf[16];
|
||||||
|
snprintf(buf, sizeof(buf), "%02u:%02u:%02u", hh, mm, ss);
|
||||||
|
id(clock_time_text).publish_state(buf);
|
||||||
|
|
||||||
|
# DP107: FM station/preset data. Encoding is not decoded yet.
|
||||||
|
- sensor_datapoint: 107
|
||||||
|
datapoint_type: raw
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
id(radio_station_raw).publish_state(format_hex_pretty(x));
|
||||||
|
|
||||||
|
# Keep cached DP127 enable bytes coherent when the dedicated enable DPs move.
|
||||||
|
- sensor_datapoint: 109
|
||||||
|
datapoint_type: bool
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
if (id(alarm_raw_valid)) id(alarm_raw)[0] = x ? 1 : 0;
|
||||||
|
|
||||||
|
- sensor_datapoint: 122
|
||||||
|
datapoint_type: bool
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
if (id(alarm_raw_valid)) id(alarm_raw)[12] = x ? 1 : 0;
|
||||||
|
|
||||||
|
# DP127: alarm configuration. Confirmed 16-byte layout on this unit:
|
||||||
|
# [00] Alarm 1 enabled
|
||||||
|
# [01:02] Alarm 1 time = minutes since midnight, big-endian
|
||||||
|
# [03] Alarm 1 weekday mask (0x7F observed = all days; bit order unknown)
|
||||||
|
# [04] unknown (0x1E observed)
|
||||||
|
# [05] Alarm 1 wake-light enable (0/1)
|
||||||
|
# [06] Alarm 1 sound: 0=off, 1..7=built-in, 8=FM
|
||||||
|
# [07] Alarm 1 volume 1..16
|
||||||
|
# [08] unknown (0x04 observed)
|
||||||
|
# [09] sunrise lead/duration in minutes (10..60 according to manual)
|
||||||
|
# [10] wake-light brightness 0..20
|
||||||
|
# [11] unknown
|
||||||
|
# [12] Alarm 2 enabled
|
||||||
|
# [13:14] Alarm 2 time = minutes since midnight, big-endian
|
||||||
|
# [15] Alarm 2 weekday mask
|
||||||
|
# Alarm 2 sound/volume/light/duration changes did NOT change DP127.
|
||||||
|
- sensor_datapoint: 127
|
||||||
|
datapoint_type: raw
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
id(alarm_settings_raw).publish_state(format_hex_pretty(x));
|
||||||
|
if (x.size() != 16) {
|
||||||
|
ESP_LOGW("alarm", "Unexpected DP127 size: %u", (unsigned) x.size());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < 16; i++) id(alarm_raw)[i] = x[i];
|
||||||
|
id(alarm_raw_valid) = true;
|
||||||
|
|
||||||
|
uint16_t a1 = (static_cast<uint16_t>(x[1]) << 8) | x[2];
|
||||||
|
uint16_t a2 = (static_cast<uint16_t>(x[13]) << 8) | x[14];
|
||||||
|
unsigned a1_h = a1 / 60, a1_m = a1 % 60;
|
||||||
|
unsigned a2_h = a2 / 60, a2_m = a2 % 60;
|
||||||
|
|
||||||
|
char sound[20];
|
||||||
|
if (x[6] == 0) snprintf(sound, sizeof(sound), "Sound off");
|
||||||
|
else if (x[6] == 8) snprintf(sound, sizeof(sound), "FM");
|
||||||
|
else snprintf(sound, sizeof(sound), "Sound %u", (unsigned) x[6]);
|
||||||
|
|
||||||
|
char buf[256];
|
||||||
|
snprintf(buf, sizeof(buf),
|
||||||
|
"A1 %s %02u:%02u days=0x%02X | %s vol=%u | light=%s %u/20 | sunrise=%u min | A2 %s %02u:%02u days=0x%02X",
|
||||||
|
x[0] ? "ON" : "OFF", a1_h, a1_m, x[3], sound, x[7],
|
||||||
|
x[5] ? "ON" : "OFF", x[10], x[9],
|
||||||
|
x[12] ? "ON" : "OFF", a2_h, a2_m, x[15]);
|
||||||
|
id(alarm_summary).publish_state(buf);
|
||||||
|
|
||||||
|
# DP128: sleep/sunset structure, not fully decoded yet.
|
||||||
|
- sensor_datapoint: 128
|
||||||
|
datapoint_type: raw
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
id(sunset_settings_raw).publish_state(format_hex_pretty(x));
|
||||||
|
|
||||||
|
# DP132 exists in the exact-PID tuya-local definition as ambient color/effect.
|
||||||
|
# Keep it read-only until this hardware revision is mapped directly.
|
||||||
|
- sensor_datapoint: 132
|
||||||
|
datapoint_type: string
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
id(ambient_mode_raw).publish_state(x);
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Helper scripts
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
script:
|
||||||
|
- id: send_alarm_settings
|
||||||
|
mode: restart
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
if (!id(alarm_raw_valid)) {
|
||||||
|
ESP_LOGW("alarm", "DP127 has not been received yet; refusing to overwrite it");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::vector<uint8_t> data(id(alarm_raw), id(alarm_raw) + 16);
|
||||||
|
ESP_LOGD("alarm", "Writing DP127: %s", format_hex_pretty(data).c_str());
|
||||||
|
id(tuya_mcu).set_raw_datapoint_value(127, data);
|
||||||
|
|
||||||
|
- id: sync_lamp_clock
|
||||||
|
mode: restart
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
auto now = id(network_time).now();
|
||||||
|
if (!now.is_valid()) {
|
||||||
|
ESP_LOGW("clock", "No valid network time; use the Lamp Clock control in the web UI");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
char buf[7];
|
||||||
|
snprintf(buf, sizeof(buf), "%02u%02u%02u", now.hour, now.minute, now.second);
|
||||||
|
ESP_LOGI("clock", "Syncing lamp clock to %s", buf);
|
||||||
|
id(tuya_mcu).set_string_datapoint_value(103, std::string(buf));
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Main lamp
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
light:
|
||||||
|
- platform: tuya
|
||||||
|
id: main_light
|
||||||
|
name: "Main Light"
|
||||||
|
switch_datapoint: 101
|
||||||
|
dimmer_datapoint: 102
|
||||||
|
min_value: 10
|
||||||
|
max_value: 1000
|
||||||
|
gamma_correct: 1.0
|
||||||
|
default_transition_length: 0s
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Writable clock/alarm times
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
datetime:
|
||||||
|
- platform: template
|
||||||
|
id: lamp_clock
|
||||||
|
name: "Lamp Clock"
|
||||||
|
type: time
|
||||||
|
update_interval: 2s
|
||||||
|
lambda: |-
|
||||||
|
if (!id(lamp_clock_valid)) return {};
|
||||||
|
ESPTime t{};
|
||||||
|
t.hour = id(lamp_clock_hour);
|
||||||
|
t.minute = id(lamp_clock_minute);
|
||||||
|
t.second = id(lamp_clock_second);
|
||||||
|
return t;
|
||||||
|
set_action:
|
||||||
|
- lambda: |-
|
||||||
|
char buf[7];
|
||||||
|
snprintf(buf, sizeof(buf), "%02u%02u%02u", x.hour, x.minute, x.second);
|
||||||
|
ESP_LOGI("clock", "Setting lamp clock to %s", buf);
|
||||||
|
id(tuya_mcu).set_string_datapoint_value(103, std::string(buf));
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
id: alarm_1_time
|
||||||
|
name: "Alarm 1 Time"
|
||||||
|
type: time
|
||||||
|
update_interval: 2s
|
||||||
|
lambda: |-
|
||||||
|
if (!id(alarm_raw_valid)) return {};
|
||||||
|
uint16_t mins = (static_cast<uint16_t>(id(alarm_raw)[1]) << 8) | id(alarm_raw)[2];
|
||||||
|
if (mins >= 1440) return {};
|
||||||
|
ESPTime t{};
|
||||||
|
t.hour = mins / 60;
|
||||||
|
t.minute = mins % 60;
|
||||||
|
t.second = 0;
|
||||||
|
return t;
|
||||||
|
set_action:
|
||||||
|
- lambda: |-
|
||||||
|
if (!id(alarm_raw_valid)) return;
|
||||||
|
uint16_t mins = static_cast<uint16_t>(x.hour) * 60 + x.minute;
|
||||||
|
id(alarm_raw)[1] = (mins >> 8) & 0xFF;
|
||||||
|
id(alarm_raw)[2] = mins & 0xFF;
|
||||||
|
- script.execute: send_alarm_settings
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
id: alarm_2_time
|
||||||
|
name: "Alarm 2 Time"
|
||||||
|
type: time
|
||||||
|
update_interval: 2s
|
||||||
|
lambda: |-
|
||||||
|
if (!id(alarm_raw_valid)) return {};
|
||||||
|
uint16_t mins = (static_cast<uint16_t>(id(alarm_raw)[13]) << 8) | id(alarm_raw)[14];
|
||||||
|
if (mins >= 1440) return {};
|
||||||
|
ESPTime t{};
|
||||||
|
t.hour = mins / 60;
|
||||||
|
t.minute = mins % 60;
|
||||||
|
t.second = 0;
|
||||||
|
return t;
|
||||||
|
set_action:
|
||||||
|
- lambda: |-
|
||||||
|
if (!id(alarm_raw_valid)) return;
|
||||||
|
uint16_t mins = static_cast<uint16_t>(x.hour) * 60 + x.minute;
|
||||||
|
id(alarm_raw)[13] = (mins >> 8) & 0xFF;
|
||||||
|
id(alarm_raw)[14] = mins & 0xFF;
|
||||||
|
- script.execute: send_alarm_settings
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Selects
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
select:
|
||||||
|
- platform: tuya
|
||||||
|
id: clock_display_mode
|
||||||
|
name: "Clock Display"
|
||||||
|
int_datapoint: 104
|
||||||
|
options:
|
||||||
|
0: "Off"
|
||||||
|
1: "Day"
|
||||||
|
2: "Night"
|
||||||
|
3: "Auto"
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
id: alarm_1_sound
|
||||||
|
name: "Alarm 1 Sound"
|
||||||
|
options:
|
||||||
|
- "Off"
|
||||||
|
- "Sound 1"
|
||||||
|
- "Sound 2"
|
||||||
|
- "Sound 3"
|
||||||
|
- "Sound 4"
|
||||||
|
- "Sound 5"
|
||||||
|
- "Sound 6"
|
||||||
|
- "Sound 7"
|
||||||
|
- "FM Radio"
|
||||||
|
update_interval: 2s
|
||||||
|
lambda: |-
|
||||||
|
if (!id(alarm_raw_valid)) return {};
|
||||||
|
uint8_t s = id(alarm_raw)[6];
|
||||||
|
if (s == 0) return std::string("Off");
|
||||||
|
if (s == 8) return std::string("FM Radio");
|
||||||
|
if (s >= 1 && s <= 7) return std::string("Sound ") + std::to_string(s);
|
||||||
|
return {};
|
||||||
|
set_action:
|
||||||
|
- lambda: |-
|
||||||
|
if (!id(alarm_raw_valid)) return;
|
||||||
|
uint8_t value = 0;
|
||||||
|
if (x == "Sound 1") value = 1;
|
||||||
|
else if (x == "Sound 2") value = 2;
|
||||||
|
else if (x == "Sound 3") value = 3;
|
||||||
|
else if (x == "Sound 4") value = 4;
|
||||||
|
else if (x == "Sound 5") value = 5;
|
||||||
|
else if (x == "Sound 6") value = 6;
|
||||||
|
else if (x == "Sound 7") value = 7;
|
||||||
|
else if (x == "FM Radio") value = 8;
|
||||||
|
id(alarm_raw)[6] = value;
|
||||||
|
- script.execute: send_alarm_settings
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Numbers
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
number:
|
||||||
|
- platform: tuya
|
||||||
|
id: radio_volume
|
||||||
|
name: "Radio Volume"
|
||||||
|
number_datapoint: 106
|
||||||
|
min_value: 1
|
||||||
|
max_value: 16
|
||||||
|
step: 1
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
id: alarm_1_volume
|
||||||
|
name: "Alarm 1 Volume"
|
||||||
|
min_value: 1
|
||||||
|
max_value: 16
|
||||||
|
step: 1
|
||||||
|
update_interval: 2s
|
||||||
|
lambda: |-
|
||||||
|
if (!id(alarm_raw_valid)) return {};
|
||||||
|
return static_cast<float>(id(alarm_raw)[7]);
|
||||||
|
set_action:
|
||||||
|
- lambda: |-
|
||||||
|
if (!id(alarm_raw_valid)) return;
|
||||||
|
id(alarm_raw)[7] = static_cast<uint8_t>(x);
|
||||||
|
- script.execute: send_alarm_settings
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
id: alarm_1_wake_brightness
|
||||||
|
name: "Alarm 1 Wake Light"
|
||||||
|
min_value: 0
|
||||||
|
max_value: 20
|
||||||
|
step: 1
|
||||||
|
update_interval: 2s
|
||||||
|
lambda: |-
|
||||||
|
if (!id(alarm_raw_valid)) return {};
|
||||||
|
if (id(alarm_raw)[5] == 0) return 0.0f;
|
||||||
|
return static_cast<float>(id(alarm_raw)[10]);
|
||||||
|
set_action:
|
||||||
|
- lambda: |-
|
||||||
|
if (!id(alarm_raw_valid)) return;
|
||||||
|
uint8_t level = static_cast<uint8_t>(x);
|
||||||
|
if (level == 0) {
|
||||||
|
id(alarm_raw)[5] = 0;
|
||||||
|
id(alarm_raw)[10] = 0;
|
||||||
|
} else {
|
||||||
|
id(alarm_raw)[5] = 1;
|
||||||
|
id(alarm_raw)[10] = level;
|
||||||
|
}
|
||||||
|
- script.execute: send_alarm_settings
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
id: alarm_1_sunrise_duration
|
||||||
|
name: "Alarm 1 Sunrise Duration"
|
||||||
|
unit_of_measurement: "min"
|
||||||
|
min_value: 10
|
||||||
|
max_value: 60
|
||||||
|
step: 10
|
||||||
|
update_interval: 2s
|
||||||
|
lambda: |-
|
||||||
|
if (!id(alarm_raw_valid)) return {};
|
||||||
|
return static_cast<float>(id(alarm_raw)[9]);
|
||||||
|
set_action:
|
||||||
|
- lambda: |-
|
||||||
|
if (!id(alarm_raw_valid)) return;
|
||||||
|
id(alarm_raw)[9] = static_cast<uint8_t>(x);
|
||||||
|
- script.execute: send_alarm_settings
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Switches
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
switch:
|
||||||
|
- platform: tuya
|
||||||
|
id: fm_radio
|
||||||
|
name: "FM Radio"
|
||||||
|
switch_datapoint: 105
|
||||||
|
|
||||||
|
- platform: tuya
|
||||||
|
id: alarm_1_enable
|
||||||
|
name: "Alarm 1"
|
||||||
|
switch_datapoint: 109
|
||||||
|
|
||||||
|
- platform: tuya
|
||||||
|
id: alarm_2_enable
|
||||||
|
name: "Alarm 2"
|
||||||
|
switch_datapoint: 122
|
||||||
|
|
||||||
|
- platform: tuya
|
||||||
|
id: sunset_mode
|
||||||
|
name: "Sunset / Sleep Mode"
|
||||||
|
switch_datapoint: 121
|
||||||
|
|
||||||
|
- platform: tuya
|
||||||
|
id: ambient_light
|
||||||
|
name: "Ambient Light"
|
||||||
|
switch_datapoint: 129
|
||||||
|
|
||||||
|
- platform: tuya
|
||||||
|
id: clock_24_hour
|
||||||
|
name: "24 Hour Clock"
|
||||||
|
switch_datapoint: 131
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Diagnostics / still-undecoded payloads
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
text_sensor:
|
||||||
|
- platform: template
|
||||||
|
id: clock_time_text
|
||||||
|
name: "Clock Time"
|
||||||
|
update_interval: never
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
id: radio_station_raw
|
||||||
|
name: "Radio Station Raw"
|
||||||
|
update_interval: never
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
id: alarm_summary
|
||||||
|
name: "Alarm Settings"
|
||||||
|
update_interval: never
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
id: alarm_settings_raw
|
||||||
|
name: "Alarm Settings Raw"
|
||||||
|
update_interval: never
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
id: sunset_settings_raw
|
||||||
|
name: "Sunset Settings Raw"
|
||||||
|
update_interval: never
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
id: ambient_mode_raw
|
||||||
|
name: "Ambient Mode Raw"
|
||||||
|
update_interval: never
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Buttons
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
button:
|
||||||
|
- platform: template
|
||||||
|
name: "Sync Lamp Clock Now"
|
||||||
|
icon: "mdi:clock-sync"
|
||||||
|
on_press:
|
||||||
|
- script.execute: sync_lamp_clock
|
||||||
|
|
||||||
|
- platform: restart
|
||||||
|
name: "Restart Wake Up Light"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Networking
|
||||||
|
#
|
||||||
|
# Default: permanent standalone AP. Connect to Wake-Up-Light-II and open
|
||||||
|
# http://192.168.4.1/ . No Internet is required for the local web UI.
|
||||||
|
#
|
||||||
|
# To also join a normal 2.4 GHz network (needed for public SNTP), uncomment:
|
||||||
|
# ssid: !secret wifi_ssid
|
||||||
|
# password: !secret wifi_password
|
||||||
|
# The AP below then becomes fallback AP.
|
||||||
|
#
|
||||||
|
# Do NOT add captive_portal if you want the normal control UI at 192.168.4.1;
|
||||||
|
# captive_portal presents the Wi-Fi provisioning/OTA page instead.
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
# ssid: !secret wifi_ssid
|
||||||
|
# password: !secret wifi_password
|
||||||
|
ap:
|
||||||
|
ssid: ${ap_ssid}
|
||||||
|
password: ${ap_password}
|
||||||
|
|
||||||
|
web_server:
|
||||||
|
port: 80
|
||||||
|
version: 2
|
||||||
|
local: true
|
||||||
|
# Recommended on a shared LAN:
|
||||||
|
# auth:
|
||||||
|
# username: admin
|
||||||
|
# password: !secret web_password
|
||||||
|
|
||||||
|
ota:
|
||||||
|
- platform: esphome
|
||||||
|
- platform: web_server
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Exact-PID tuya-local definition also lists these DPs, but they are not enabled
|
||||||
|
# above until verified on this physical two-alarm hardware revision:
|
||||||
|
# DP116 snooze duration (8..15 min)
|
||||||
|
# DP117 snooze action
|
||||||
|
# DP125 network-time switch
|
||||||
|
# DP126 radio seek/stop
|
||||||
|
# DP130 snooze
|
||||||
|
# DP132 ambient color/effect (currently shown read-only as Ambient Mode Raw)
|
||||||
|
# It also lists DP123/124 as alarm 3/4, but this unit has only two physical
|
||||||
|
# alarms/indications, so they are intentionally not exposed here.
|
||||||
|
###############################################################################
|
||||||
Reference in new issue
Block a user