Files
esphome-wake-up-light-ii/wake-up-light-ii.yaml
T
mangavalkandClaude Opus 5.5 06b259cb1b Restart when the MCU stops reporting
After about 2.5 days the chip stopped receiving MCU replies while writes still
went out, so switches looked stuck. Restart after 5 quiet minutes, but only if
the link worked earlier in that boot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 01:17:11 +01:00

1225 lines
36 KiB
YAML

###############################################################################
# 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)
#
# Stock ESPHome/LibreTiny UART selection may not route UART0 to PA13/PA14 on
# WBR3, so this config loads the patched UART component in components/uart
# (patches/wbr3-pa13-pa14-uart.patch applied to ESPHome 2026.10.2).
###############################################################################
substitutions:
device_name: wake-up-light-ii
friendly_name: "Wake Up Light II"
ap_ssid: "Wake-Up-Light-II"
timezone: "Europe/Berlin" # Change to your IANA timezone.
# Patched UART component. Keep ref equal to the tag this file is released under.
external_components:
- source:
type: git
url: https://mangavalk.com/mangavalk/esphome-wake-up-light-ii.git
ref: v1.0.4
components: [uart]
refresh: 1d
# Controls for each alarm live in alarm_block.yaml.
packages:
alarm_1: !include { file: alarm_block.yaml, vars: { n: "1", o: "0", dp: "109" } }
alarm_2: !include { file: alarm_block.yaml, vars: { n: "2", o: "12", dp: "122" } }
alarm_3: !include { file: alarm_block.yaml, vars: { n: "3", o: "24", dp: "123" } }
alarm_4: !include { file: alarm_block.yaml, vars: { n: "4", o: "36", dp: "124" } }
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
rtl87xx:
board: wbr3
# v1.14.0 fixes the restart hang after OTA (LibreTiny issue #396). Drop this
# once ESPHome's default framework version is 1.14.0 or newer.
framework:
version: 1.14.0
source: https://github.com/libretiny-eu/libretiny.git#v1.14.0
# 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 48 bytes on this unit: 4 alarm blocks of 12 bytes. Never build it
# from scratch: cache what the appliance MCU reported and only edit known bytes.
- id: alarm_raw
type: uint8_t[48]
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'
# DP128: sunset settings, 10 bytes. Cached and edited like DP127.
- id: sunset_raw
type: uint8_t[10]
restore_value: no
- id: sunset_raw_valid
type: bool
restore_value: no
initial_value: 'false'
# DP107: stations found by a scan, and the current station.
- id: radio_total
type: uint8_t
restore_value: no
initial_value: '0'
- id: radio_current
type: uint8_t
restore_value: no
initial_value: '0'
- id: radio_valid
type: bool
restore_value: no
initial_value: 'false'
# Knocks counted in the current pattern window.
- id: knock_count
type: int
restore_value: no
initial_value: '0'
- id: bedtime_start_brightness
type: float
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
restore_value: no
initial_value: '-1'
###############################################################################
# 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
- 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
# After about 2.5 days the chip once stopped receiving the MCU's replies,
# while sending still worked. A restart fixed it, so restart after 5 quiet
# minutes. Only if the link worked earlier this boot, to avoid a restart loop.
- lambda: |-
uint32_t last = id(last_mcu_report);
if (last != 0 && millis() - last > 300000) {
ESP_LOGW("mcu", "No report from the MCU for %u s, restarting", (unsigned) ((millis() - last) / 1000));
App.safe_reboot();
}
###############################################################################
# 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: |-
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]);
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: [0] stations found by a scan (0 = none yet), [1] current station.
- sensor_datapoint: 107
datapoint_type: raw
then:
- lambda: |-
id(radio_station_raw).publish_state(format_hex_pretty(x));
if (x.size() != 2) return;
id(radio_total) = x[0];
id(radio_current) = x[1];
id(radio_valid) = true;
id(radio_stations_found).publish_state(x[0]);
# 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;
id(publish_alarm_summaries).execute();
- sensor_datapoint: 122
datapoint_type: bool
then:
- lambda: |-
if (id(alarm_raw_valid)) id(alarm_raw)[12] = x ? 1 : 0;
id(publish_alarm_summaries).execute();
- sensor_datapoint: 123
datapoint_type: bool
then:
- lambda: |-
if (id(alarm_raw_valid)) id(alarm_raw)[24] = x ? 1 : 0;
id(publish_alarm_summaries).execute();
- sensor_datapoint: 124
datapoint_type: bool
then:
- lambda: |-
if (id(alarm_raw_valid)) id(alarm_raw)[36] = x ? 1 : 0;
id(publish_alarm_summaries).execute();
# DP127: alarm configuration, 48 bytes on this unit: 4 blocks of 12 bytes,
# Alarm 1 at offset 0, 2 at 12, 3 at 24, 4 at 36. Layout per Tuya's data
# model for this family (tuya-local issue #3821):
# [00] enabled
# [01:02] time = minutes since midnight, big-endian
# [03] repeat days: bit 0 = Monday ... bit 6 = Sunday, 0 = one time
# [04] ring duration (Tuya says 10-20, this lamp reports 30; any value is kept)
# [05] mode bits: bit 0=light, bit 1=radio. Values above 3 are ignored.
# [06] sound 0..7, named in alarm_block.yaml. Writing 8 or more turns on the
# radio bit and resets it to 1.
# Locked at 1 in radio modes.
# [07] volume 1..16 (any value is kept)
# [08] radio station. Not per alarm: every write sets all 4 to the current
# radio station, so don't try to set it here.
# [09] sunrise lead time in minutes, 10..60 (any value is kept)
# [10] brightness after sunrise, 1..20. Forced to 0 without the light bit, and
# 0 clears the light bit.
# [11] snooze: 0=allowed, 1=not allowed. Values above 1 become 1.
# The MCU doesn't range-check most bytes, so the controls must.
- sensor_datapoint: 127
datapoint_type: raw
then:
- lambda: |-
id(alarm_settings_raw).publish_state(format_hex_pretty(x));
if (x.size() != 48) {
ESP_LOGW("alarm", "Unexpected DP127 size: %u", (unsigned) x.size());
return;
}
for (size_t i = 0; i < 48; i++) id(alarm_raw)[i] = x[i];
id(alarm_raw_valid) = true;
id(publish_alarm_summaries).execute();
# DP128: sunset settings, 10 bytes, per Tuya's data model:
# [0] switch (unused) [1] mode: 0=sound, 1=sound+light, 2=radio, 3=radio+light
# [2] sound 1..4 [3] sound volume 1..16 [4] sound duration 10..120
# [5] radio station 1..60 [6] radio duration 10..120
# [7] starting brightness 1..20 [8] sunset duration 10..120
# [9] snooze: 0=allowed, 1=not allowed
# Bytes 4, 6 and 8 are kept equal, as Tuya's definition asks.
- sensor_datapoint: 128
datapoint_type: raw
then:
- lambda: |-
id(sunset_settings_raw).publish_state(format_hex_pretty(x));
if (x.size() != 10) {
ESP_LOGW("sunset", "Unexpected DP128 size: %u", (unsigned) x.size());
return;
}
for (size_t i = 0; i < 10; i++) id(sunset_raw)[i] = x[i];
id(sunset_raw_valid) = true;
###############################################################################
# Helper scripts
###############################################################################
script:
# A knock asks the lamp's MCU to snooze (DP130). Forced because the MCU may
# leave DP130 on, and a normal write is skipped when the value looks unchanged.
# Mode single plus the delay ignores repeat knocks for 3 seconds.
- id: knock_snooze
mode: single
then:
- lambda: |-
ESP_LOGI("knock", "Sending snooze (DP130)");
id(tuya_mcu).force_set_boolean_datapoint_value(130, true);
- delay: 3s
# Restarted by every knock, so this runs once the knocking has stopped.
# Exactly two knocks toggle the light; any other count does nothing here.
- id: knock_pattern
mode: restart
then:
- delay: 800ms
- lambda: |-
int count = id(knock_count);
id(knock_count) = 0;
ESP_LOGI("knock", "Knock pattern finished: %d", count);
if (count == 2 && id(double_knock_light).state) {
id(main_light).toggle().perform();
}
# Writes the display mode only when the schedule crosses into day or night,
# so changing Clock Display by hand holds until the next crossing.
- id: apply_display_schedule
mode: restart
then:
- lambda: |-
if (!id(display_schedule_enabled).state) return;
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;
int cur = now.hour * 60 + now.minute;
bool night;
if (start == end) night = false;
else if (start < end) night = cur >= start && cur < end;
else night = cur >= start || cur < end;
int state = night ? 1 : 0;
if (state == id(display_schedule_last)) return;
id(display_schedule_last) = state;
// DP104: 0=off, 1=day, 2=night, 3=auto.
int value;
if (night) value = id(display_night_level).active_index().value_or(1) == 0 ? 0 : 2;
else value = id(display_day_level).active_index().value_or(0) == 0 ? 1 : 3;
ESP_LOGI("display", "Schedule: %s, setting DP104 to %d", night ? "night" : "day", value);
id(tuya_mcu).set_integer_datapoint_value(104, value);
# 30 steps over the chosen length. Mode restart means pressing start again
# begins a new fade from the current brightness.
- id: bedtime_fade
mode: restart
then:
- lambda: |-
float b = 0.0f;
id(main_light).remote_values.as_brightness(&b);
id(bedtime_start_brightness) = b;
- repeat:
count: 30
then:
- delay: !lambda 'return static_cast<uint32_t>(id(bedtime_fade_minutes).state) * 2000;'
- lambda: |-
if (!id(main_light).remote_values.is_on()) return;
float b = id(bedtime_start_brightness) * (29 - iteration) / 30.0f;
auto call = id(main_light).make_call();
if (iteration >= 29 || b < 0.02f) call.set_state(false);
else call.set_brightness(b);
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<uint8_t> data = {id(radio_total), static_cast<uint8_t>(next)};
id(tuya_mcu).set_raw_datapoint_value(107, data);
- id: radio_sleep
mode: restart
then:
- delay: !lambda 'return static_cast<uint32_t>(id(radio_sleep_minutes).state) * 60000;'
- switch.turn_off: fm_radio
- id: publish_alarm_summaries
mode: restart
then:
- lambda: |-
if (!id(alarm_raw_valid)) return;
text_sensor::TextSensor *summaries[4] = {
id(alarm_1_summary), id(alarm_2_summary), id(alarm_3_summary), id(alarm_4_summary)};
static const char *const modes[4] = {"sound", "sound+light", "radio", "radio+light"};
for (int n = 0; n < 4; n++) {
const uint8_t *b = id(alarm_raw) + n * 12;
uint16_t mins = (static_cast<uint16_t>(b[1]) << 8) | b[2];
char buf[120];
snprintf(buf, sizeof(buf),
"%s %02u:%02u days=0x%02X %s sound=%u vol=%u station=%u sunrise=%u min light=%u/20 ring=%u min",
b[0] ? "ON" : "OFF", mins / 60, mins % 60, b[3], b[5] < 4 ? modes[b[5]] : "?", b[6], b[7], b[8],
b[9], b[10], b[4]);
summaries[n]->publish_state(buf);
}
# The MCU echoes every write and that echo refreshes the cache, so wait for
# quick changes to finish and send them together.
- id: send_sunset_settings
mode: restart
then:
- delay: 500ms
- lambda: |-
if (!id(sunset_raw_valid)) {
ESP_LOGW("sunset", "DP128 has not been received yet; refusing to overwrite it");
return;
}
std::vector<uint8_t> data(id(sunset_raw), id(sunset_raw) + 10);
id(tuya_mcu).set_raw_datapoint_value(128, data);
- id: send_alarm_settings
mode: restart
then:
- delay: 500ms
- 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) + 48);
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(ha_time).now();
if (!now.is_valid()) now = id(network_time).now();
if (!now.is_valid()) {
ESP_LOGW("clock", "No valid time source; 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
# Keep at 0: a transition here writes to the MCU on every loop, about 50
# commands a second. Bedtime fade uses its own slow steps instead.
default_transition_length: 0s
web_server:
sorting_group_id: group_light
sorting_weight: 1
###############################################################################
# Clock and display schedule times
###############################################################################
datetime:
- platform: template
id: lamp_clock
name: "Lamp Clock"
type: time
update_interval: 2s
web_server:
sorting_group_id: group_clock
sorting_weight: 1
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: display_night_start
name: "Display Night Start"
type: time
optimistic: true
restore_value: true
initial_value: "22:30:00"
web_server:
sorting_group_id: group_clock
sorting_weight: 11
on_value:
- lambda: id(display_schedule_last) = -1;
- script.execute: apply_display_schedule
- platform: template
id: display_night_end
name: "Display Night End"
type: time
optimistic: true
restore_value: true
initial_value: "07:00:00"
web_server:
sorting_group_id: group_clock
sorting_weight: 12
on_value:
- lambda: id(display_schedule_last) = -1;
- script.execute: apply_display_schedule
###############################################################################
# Selects
###############################################################################
select:
- platform: template
id: sunset_mode_select
name: "Sunset Type"
options:
- "Sound"
- "Sound + Light"
- "Radio"
- "Radio + Light"
update_interval: 2s
web_server:
sorting_group_id: group_sunset
sorting_weight: 2
lambda: |-
if (!id(sunset_raw_valid)) return {};
switch (id(sunset_raw)[1]) {
case 0: return std::string("Sound");
case 1: return std::string("Sound + Light");
case 2: return std::string("Radio");
case 3: return std::string("Radio + Light");
default: return {};
}
set_action:
- lambda: |-
if (!id(sunset_raw_valid)) return;
uint8_t value = 0;
if (x == "Sound + Light") value = 1;
else if (x == "Radio") value = 2;
else if (x == "Radio + Light") value = 3;
id(sunset_raw)[1] = value;
// The MCU drops a light mode if brightness is 0, so give it a level.
if ((value == 1 || value == 3) && id(sunset_raw)[7] == 0) id(sunset_raw)[7] = 15;
- script.execute: send_sunset_settings
- platform: template
id: sunset_sound
name: "Sunset Sound"
options:
- "Sound 1"
- "Sound 2"
- "Sound 3"
- "Sound 4"
update_interval: 2s
web_server:
sorting_group_id: group_sunset
sorting_weight: 3
lambda: |-
if (!id(sunset_raw_valid)) return {};
uint8_t s = id(sunset_raw)[2];
if (s >= 1 && s <= 4) return std::string("Sound ") + std::to_string(s);
return {};
set_action:
- lambda: |-
if (!id(sunset_raw_valid)) return;
uint8_t value = 1;
if (x == "Sound 2") value = 2;
else if (x == "Sound 3") value = 3;
else if (x == "Sound 4") value = 4;
id(sunset_raw)[2] = value;
- script.execute: send_sunset_settings
- platform: tuya
id: clock_display_mode
name: "Clock Display"
int_datapoint: 104
options:
0: "Off"
1: "Day"
2: "Night"
3: "Auto"
web_server:
sorting_group_id: group_clock
sorting_weight: 3
- platform: template
id: display_night_level
name: "Display At Night"
options:
- "Off"
- "Night"
initial_option: "Night"
optimistic: true
restore_value: true
web_server:
sorting_group_id: group_clock
sorting_weight: 13
on_value:
- lambda: id(display_schedule_last) = -1;
- script.execute: apply_display_schedule
- platform: template
id: display_day_level
name: "Display In Daytime"
options:
- "Day"
- "Auto"
initial_option: "Day"
optimistic: true
restore_value: true
web_server:
sorting_group_id: group_clock
sorting_weight: 14
on_value:
- lambda: id(display_schedule_last) = -1;
- script.execute: apply_display_schedule
# Labels for these two come from Tuya's data model, not from testing.
# This MCU reports both DPs as enums, so values count from 0.
- platform: tuya
id: snooze_action
name: "Snooze Action"
enum_datapoint: 117
options:
0: "Sound + Radio"
1: "Light"
2: "Radio"
3: "Radio + Sound + Light"
web_server:
sorting_group_id: group_snooze
sorting_weight: 3
- platform: tuya
id: ambient_color
name: "Ambient Color"
enum_datapoint: 132
options:
0: "Cycle"
1: "Red"
2: "Orange"
3: "Yellow"
4: "Green"
5: "Cyan"
6: "Blue"
7: "Purple"
web_server:
sorting_group_id: group_light
sorting_weight: 3
###############################################################################
# Numbers
###############################################################################
number:
# Only meaningful after a Radio Scan has found stations.
- platform: template
id: radio_station
name: "Radio Station"
min_value: 1
max_value: 60
step: 1
update_interval: 2s
web_server:
sorting_group_id: group_radio
sorting_weight: 2.5
lambda: |-
if (!id(radio_valid) || id(radio_current) == 0) return {};
return static_cast<float>(id(radio_current));
set_action:
- lambda: |-
if (!id(radio_valid) || id(radio_total) == 0) {
ESP_LOGW("radio", "No stations found yet; run Radio Scan first");
return;
}
uint8_t station = static_cast<uint8_t>(x);
if (station > id(radio_total)) station = id(radio_total);
std::vector<uint8_t> data = {id(radio_total), station};
id(tuya_mcu).set_raw_datapoint_value(107, data);
- platform: template
id: sunset_volume
name: "Sunset Volume"
min_value: 1
max_value: 16
step: 1
update_interval: 2s
web_server:
sorting_group_id: group_sunset
sorting_weight: 4
lambda: |-
if (!id(sunset_raw_valid)) return {};
return static_cast<float>(id(sunset_raw)[3]);
set_action:
- lambda: |-
if (!id(sunset_raw_valid)) return;
id(sunset_raw)[3] = static_cast<uint8_t>(x);
- script.execute: send_sunset_settings
- platform: template
id: sunset_duration
name: "Sunset Duration"
unit_of_measurement: "min"
min_value: 10
max_value: 120
step: 5
update_interval: 2s
web_server:
sorting_group_id: group_sunset
sorting_weight: 5
lambda: |-
if (!id(sunset_raw_valid)) return {};
return static_cast<float>(id(sunset_raw)[8]);
set_action:
- lambda: |-
if (!id(sunset_raw_valid)) return;
uint8_t minutes = static_cast<uint8_t>(x);
id(sunset_raw)[4] = minutes;
id(sunset_raw)[6] = minutes;
id(sunset_raw)[8] = minutes;
- script.execute: send_sunset_settings
- platform: template
id: sunset_brightness
name: "Sunset Start Brightness"
min_value: 1
max_value: 20
step: 1
update_interval: 2s
web_server:
sorting_group_id: group_sunset
sorting_weight: 6
lambda: |-
if (!id(sunset_raw_valid)) return {};
return static_cast<float>(id(sunset_raw)[7]);
set_action:
- lambda: |-
if (!id(sunset_raw_valid)) return;
id(sunset_raw)[7] = static_cast<uint8_t>(x);
- script.execute: send_sunset_settings
- platform: template
id: sunset_radio_station
name: "Sunset Radio Station"
min_value: 1
max_value: 60
step: 1
update_interval: 2s
web_server:
sorting_group_id: group_sunset
sorting_weight: 7
lambda: |-
if (!id(sunset_raw_valid)) return {};
return static_cast<float>(id(sunset_raw)[5]);
set_action:
- lambda: |-
if (!id(sunset_raw_valid)) return;
id(sunset_raw)[5] = static_cast<uint8_t>(x);
- script.execute: send_sunset_settings
- platform: tuya
id: radio_volume
name: "Radio Volume"
number_datapoint: 106
min_value: 1
max_value: 16
step: 1
web_server:
sorting_group_id: group_radio
sorting_weight: 2
- platform: tuya
id: snooze_duration
name: "Snooze Duration"
number_datapoint: 116
unit_of_measurement: "min"
min_value: 8
max_value: 15
step: 1
web_server:
sorting_group_id: group_snooze
sorting_weight: 2
- platform: template
id: bedtime_fade_minutes
name: "Bedtime Fade Length"
unit_of_measurement: "min"
min_value: 1
max_value: 120
step: 1
initial_value: 30
optimistic: true
restore_value: true
web_server:
sorting_group_id: group_light
sorting_weight: 5
- platform: template
id: radio_sleep_minutes
name: "Radio Sleep Length"
unit_of_measurement: "min"
min_value: 5
max_value: 180
step: 5
initial_value: 30
optimistic: true
restore_value: true
web_server:
sorting_group_id: group_radio
sorting_weight: 5
###############################################################################
# Switches
###############################################################################
switch:
- platform: tuya
id: fm_radio
name: "FM Radio"
switch_datapoint: 105
web_server:
sorting_group_id: group_radio
sorting_weight: 1
- platform: tuya
id: sunset_mode
name: "Sunset / Sleep Mode"
switch_datapoint: 121
web_server:
sorting_group_id: group_sunset
sorting_weight: 1
- platform: tuya
id: ambient_light
name: "Ambient Light"
switch_datapoint: 129
web_server:
sorting_group_id: group_light
sorting_weight: 2
- platform: tuya
id: clock_24_hour
name: "24 Hour Clock"
switch_datapoint: 131
web_server:
sorting_group_id: group_clock
sorting_weight: 4
# Tuya's data model calls this online time synchronization.
- platform: tuya
id: network_time_switch
name: "Network Time"
switch_datapoint: 125
web_server:
sorting_group_id: group_clock
sorting_weight: 5
- platform: template
id: display_schedule_enabled
name: "Display Night Schedule"
optimistic: true
restore_mode: RESTORE_DEFAULT_OFF
web_server:
sorting_group_id: group_clock
sorting_weight: 10
on_turn_on:
- lambda: id(display_schedule_last) = -1;
- script.execute: apply_display_schedule
- platform: template
id: double_knock_light
name: "Double Knock Toggles Light"
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
web_server:
sorting_group_id: group_snooze
sorting_weight: 5
###############################################################################
# Diagnostics / still-undecoded payloads
###############################################################################
text_sensor:
- platform: template
id: clock_time_text
name: "Clock Time"
update_interval: never
web_server:
sorting_group_id: group_clock
sorting_weight: 2
- platform: template
id: radio_station_raw
name: "Radio Station Raw"
update_interval: never
web_server:
sorting_group_id: group_radio
sorting_weight: 9
- platform: template
id: alarm_settings_raw
name: "Alarm Settings Raw"
update_interval: never
web_server:
sorting_group_id: group_device
sorting_weight: 10
- platform: template
id: sunset_settings_raw
name: "Sunset Settings Raw"
update_interval: never
web_server:
sorting_group_id: group_device
sorting_weight: 11
- platform: wifi_info
ip_address:
name: "IP Address"
web_server:
sorting_group_id: group_device
sorting_weight: 1
- platform: debug
reset_reason:
name: "Last Restart Reason"
web_server:
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
###############################################################################
debug:
update_interval: 60s
sensor:
- platform: template
id: radio_stations_found
name: "Radio Stations Found"
accuracy_decimals: 0
update_interval: never
web_server:
sorting_group_id: group_radio
sorting_weight: 8
- platform: wifi_signal
name: "WiFi Signal"
update_interval: 60s
web_server:
sorting_group_id: group_device
sorting_weight: 2
- platform: uptime
name: "Uptime"
update_interval: 60s
web_server:
sorting_group_id: group_device
sorting_weight: 3
###############################################################################
# Knock sensor: switch between 3.3V and PA18, active high
# One knock snoozes straight away. Exactly two knocks also toggle the light.
###############################################################################
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"
pin:
number: PA18
mode:
input: true
pulldown: true
web_server:
sorting_group_id: group_snooze
sorting_weight: 4
filters:
# Stretch short hits so Home Assistant sees them, and merge bounces into
# one press.
- delayed_off: 200ms
on_press:
- script.execute: knock_snooze
- lambda: id(knock_count) += 1;
- script.execute: knock_pattern
###############################################################################
# Buttons
###############################################################################
button:
- platform: template
name: "Snooze"
icon: "mdi:alarm-snooze"
web_server:
sorting_group_id: group_snooze
sorting_weight: 1
on_press:
- script.execute: knock_snooze
- platform: template
name: "Start Bedtime Fade"
icon: "mdi:weather-night"
web_server:
sorting_group_id: group_light
sorting_weight: 6
on_press:
- script.execute: bedtime_fade
- platform: template
name: "Cancel Bedtime Fade"
icon: "mdi:cancel"
web_server:
sorting_group_id: group_light
sorting_weight: 7
on_press:
- script.stop: bedtime_fade
- platform: template
name: "Sync Lamp Clock Now"
icon: "mdi:clock-sync"
web_server:
sorting_group_id: group_clock
sorting_weight: 6
on_press:
- script.execute: sync_lamp_clock
# DP126: 1 starts a channel search, 0 ends it, per Tuya's data model.
- platform: template
name: "Radio Scan"
icon: "mdi:radio-tower"
web_server:
sorting_group_id: group_radio
sorting_weight: 3
on_press:
- lambda: id(tuya_mcu).force_set_boolean_datapoint_value(126, true);
- platform: template
name: "Radio Scan Stop"
icon: "mdi:stop"
web_server:
sorting_group_id: group_radio
sorting_weight: 4
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"
web_server:
sorting_group_id: group_radio
sorting_weight: 6
on_press:
- script.execute: radio_sleep
- platform: template
name: "Cancel Radio Sleep Timer"
icon: "mdi:timer-off-outline"
web_server:
sorting_group_id: group_radio
sorting_weight: 7
on_press:
- script.stop: radio_sleep
- platform: restart
name: "Restart Wake Up Light"
web_server:
sorting_group_id: group_device
sorting_weight: 5
###############################################################################
# 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: !secret ap_password
api:
encryption:
key: !secret api_encryption_key
web_server:
port: 80
version: 3
local: true
sorting_groups:
- id: group_light
name: "Light"
sorting_weight: 10
- id: group_clock
name: "Clock and Display"
sorting_weight: 20
- id: group_sunset
name: "Sunset"
sorting_weight: 25
- id: group_snooze
name: "Snooze and Knock"
sorting_weight: 40
- id: group_radio
name: "Radio"
sorting_weight: 50
- id: group_device
name: "Device"
sorting_weight: 90
# Recommended on a shared LAN:
# auth:
# username: admin
# password: !secret web_password
ota:
- platform: esphome
- platform: web_server