From aa929478f640f29588712146966ded9ef7cc68ce Mon Sep 17 00:00:00 2001 From: MangaValk Date: Thu, 5 Nov 2026 17:22:24 +0100 Subject: [PATCH] Add local time source, station buttons and link status Prefer Home Assistant time with public NTP as fallback. Add next and previous station, ESPHome and LibreTiny versions, and an MCU link sensor. Update notes with the raw capture results and tidy the README to-do list. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 7 ++- README.md | 23 ++++------ server-example.yaml | 2 +- wake-up-light-ii.yaml | 102 +++++++++++++++++++++++++++++++++++++----- 4 files changed, 105 insertions(+), 29 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index deeeef3..9ea71fa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,7 +57,7 @@ From Tuya's definition for a sibling product (tuya-local issue #3821). See the R - DP127 writes are ignored while an alarm is snoozed. - A mode without light sets brightness to 0, and a light mode with brightness 0 falls back to no light, so the firmware sets a level when choosing a light mode. - Alarm radio stations were reset to 1 while no stations were saved. -- Radio Volume writes were rejected with the radio off. +- Radio Volume and station writes are ignored with the radio off. With it on, both work, and the station stays selected after turning it off. - Keep `default_transition_length` at 0. A transition sends about 50 MCU writes a second. - LibreTiny before v1.14.0 can hang after a software restart or OTA until powered off for about 30 seconds (LibreTiny issue #396). The config pins v1.14.0. Drop the pin once ESPHome defaults to it or newer. - "Last Restart Reason" reports Power-On even after software restarts. @@ -69,9 +69,12 @@ Verified: - Wednesday-only rang on a Wednesday, which fits bit 0 = Monday. A Tuesday-only silent test was skipped. - Sunset starts and stops. Its sound was heard, and a glow was seen but not clearly caused by it. - A manual scan found 2 stations, while a scan started from the firmware found 0. +- Next/Previous Station and Radio Station change the MCU's station, with wrap-around. Audio on them wasn't confirmed: the owner heard no radio signal. +- Home Assistant is used as the time source when connected, with public NTP as the fallback. Not tested: -- Radio alarms, Radio Station changes and Radio Volume with the radio on. +- Radio alarms. +- Running without internet, or without Home Assistant. - Bedtime Fade and double knock on the real light. - Whether Ambient Color and Snooze Action labels match what the lamp does. diff --git a/README.md b/README.md index 3c1618c..a4efbfb 100644 --- a/README.md +++ b/README.md @@ -418,22 +418,17 @@ That manual is for a related model/revision, so use it as behavioral reference r ## TODO / worthwhile additions -For this lamp, the most worthwhile additions are: +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. + +Still open: -- **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. +- **Astronomical sunrise/sunset:** use ESPHome's local sun calculations for real dusk/dawn behavior. +- **Clock drift:** optional drift measurement and correction. +- **Fully autonomous schedules:** software-only alarms, if 4 hardware alarms ever aren't enough. +- **Identify IC1:** obtain a board with readable top marking or otherwise identify the main appliance MCU. + +FM frequency isn't available: DP107 only holds the station count and current station number. ## Safety / recovery notes diff --git a/server-example.yaml b/server-example.yaml index 5b6d3b3..0adf9a8 100644 --- a/server-example.yaml +++ b/server-example.yaml @@ -7,6 +7,6 @@ substitutions: packages: lamp: url: https://mangavalk.com/mangavalk/esphome-wake-up-light-ii.git - ref: v1.0.0 + ref: v1.0.1 files: [wake-up-light-ii.yaml] refresh: 1d diff --git a/wake-up-light-ii.yaml b/wake-up-light-ii.yaml index 87e6fc5..4a35921 100644 --- a/wake-up-light-ii.yaml +++ b/wake-up-light-ii.yaml @@ -22,7 +22,7 @@ external_components: - source: type: git url: https://mangavalk.com/mangavalk/esphome-wake-up-light-ii.git - ref: v1.0.0 + ref: v1.0.1 components: [uart] refresh: 1d @@ -136,6 +136,12 @@ globals: restore_value: no initial_value: '0' + # When the MCU last sent its once-a-minute clock report. + - id: last_mcu_report + type: uint32_t + restore_value: no + initial_value: '0' + # Last display state the schedule applied: -1 none, 0 day, 1 night. - id: display_schedule_last type: int @@ -165,14 +171,22 @@ time: - script.execute: sync_lamp_clock - script.execute: apply_display_schedule - on_time: - - seconds: 0 - minutes: 0 - then: - - script.execute: sync_lamp_clock - - seconds: 0 - then: - - script.execute: apply_display_schedule + # Home Assistant is preferred when connected, so the clock doesn't need internet. + - platform: homeassistant + id: ha_time + on_time_sync: + then: + - delay: 2s + - script.execute: sync_lamp_clock + - script.execute: apply_display_schedule + +interval: + - interval: 1h + then: + - script.execute: sync_lamp_clock + - interval: 60s + then: + - script.execute: apply_display_schedule ############################################################################### # Tuya MCU @@ -188,6 +202,7 @@ tuya: datapoint_type: string then: - lambda: |- + id(last_mcu_report) = millis(); 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]); @@ -329,7 +344,8 @@ script: then: - lambda: |- if (!id(display_schedule_enabled).state) return; - auto now = id(network_time).now(); + auto now = id(ha_time).now(); + if (!now.is_valid()) now = id(network_time).now(); if (!now.is_valid()) return; int start = id(display_night_start).hour * 60 + id(display_night_start).minute; int end = id(display_night_end).hour * 60 + id(display_night_end).minute; @@ -370,6 +386,21 @@ script: call.set_transition_length(0); call.perform(); + - id: radio_step + mode: queued + parameters: + step: int + then: + - lambda: |- + if (!id(radio_valid) || id(radio_total) == 0) { + ESP_LOGW("radio", "No stations found yet; run Radio Scan first"); + return; + } + int total = id(radio_total); + int next = ((id(radio_current) - 1 + step) % total + total) % total + 1; + std::vector data = {id(radio_total), static_cast(next)}; + id(tuya_mcu).set_raw_datapoint_value(107, data); + - id: radio_sleep mode: restart then: @@ -426,9 +457,10 @@ script: mode: restart then: - lambda: |- - auto now = id(network_time).now(); + auto now = id(ha_time).now(); + if (!now.is_valid()) 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"); + ESP_LOGW("clock", "No valid time source; use the Lamp Clock control in the web UI"); return; } char buf[7]; @@ -932,6 +964,20 @@ text_sensor: sorting_group_id: group_device sorting_weight: 4 + - platform: version + name: "ESPHome Version" + hide_timestamp: true + web_server: + sorting_group_id: group_device + sorting_weight: 6 + + - platform: libretiny + version: + name: "LibreTiny Version" + web_server: + sorting_group_id: group_device + sorting_weight: 7 + ############################################################################### # Device health ############################################################################### @@ -969,6 +1015,16 @@ sensor: ############################################################################### binary_sensor: + # The MCU reports its clock every minute, so silence means the link is down. + - platform: template + name: "MCU Link" + device_class: connectivity + web_server: + sorting_group_id: group_device + sorting_weight: 0.5 + lambda: |- + return id(last_mcu_report) != 0 && millis() - id(last_mcu_report) < 150000; + - platform: gpio name: "Knock" icon: "mdi:gesture-tap" @@ -1049,6 +1105,28 @@ button: on_press: - lambda: id(tuya_mcu).force_set_boolean_datapoint_value(126, false); + - platform: template + name: "Next Station" + icon: "mdi:skip-next" + web_server: + sorting_group_id: group_radio + sorting_weight: 2.7 + on_press: + - script.execute: + id: radio_step + step: 1 + + - platform: template + name: "Previous Station" + icon: "mdi:skip-previous" + web_server: + sorting_group_id: group_radio + sorting_weight: 2.6 + on_press: + - script.execute: + id: radio_step + step: -1 + - platform: template name: "Start Radio Sleep Timer" icon: "mdi:timer-outline"