Files
esphome-wake-up-light-ii/wake-up-light-ii.yaml
T
mangavalkandClaude Opus 5.5 e6f004e811 Correct alarm data findings from byte tests
The alarm station byte is shared and follows the current radio station, so
remove the per-alarm control. Remove the FM sound option, as writing it just
turns on the radio mode. Record how the MCU treats each DP127 byte.

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

1215 lines
35 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.2
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
###############################################################################
# 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. 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