2026-10-08 10:05:56 +03:00
|
|
|
###############################################################################
|
|
|
|
|
# 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)
|
|
|
|
|
#
|
2026-11-05 12:27:54 +01:00
|
|
|
# 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).
|
2026-10-08 10:05:56 +03:00
|
|
|
###############################################################################
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
2026-11-05 12:27:54 +01:00
|
|
|
# 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.0
|
|
|
|
|
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" } }
|
|
|
|
|
|
2026-10-08 10:05:56 +03:00
|
|
|
esphome:
|
|
|
|
|
name: ${device_name}
|
|
|
|
|
friendly_name: ${friendly_name}
|
|
|
|
|
|
|
|
|
|
rtl87xx:
|
|
|
|
|
board: wbr3
|
2026-11-05 12:27:54 +01:00
|
|
|
# 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
|
2026-10-08 10:05:56 +03:00
|
|
|
|
|
|
|
|
# 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:
|
2026-11-05 12:27:54 +01:00
|
|
|
# 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.
|
2026-10-08 10:05:56 +03:00
|
|
|
- id: alarm_raw
|
2026-11-05 12:27:54 +01:00
|
|
|
type: uint8_t[48]
|
2026-10-08 10:05:56 +03:00
|
|
|
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'
|
|
|
|
|
|
2026-11-05 12:27:54 +01:00
|
|
|
# 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'
|
|
|
|
|
|
|
|
|
|
# Last display state the schedule applied: -1 none, 0 day, 1 night.
|
|
|
|
|
- id: display_schedule_last
|
|
|
|
|
type: int
|
|
|
|
|
restore_value: no
|
|
|
|
|
initial_value: '-1'
|
|
|
|
|
|
2026-10-08 10:05:56 +03:00
|
|
|
###############################################################################
|
|
|
|
|
# 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
|
2026-11-05 12:27:54 +01:00
|
|
|
- script.execute: apply_display_schedule
|
2026-10-08 10:05:56 +03:00
|
|
|
|
|
|
|
|
on_time:
|
|
|
|
|
- seconds: 0
|
|
|
|
|
minutes: 0
|
|
|
|
|
then:
|
|
|
|
|
- script.execute: sync_lamp_clock
|
2026-11-05 12:27:54 +01:00
|
|
|
- seconds: 0
|
|
|
|
|
then:
|
|
|
|
|
- script.execute: apply_display_schedule
|
2026-10-08 10:05:56 +03:00
|
|
|
|
|
|
|
|
###############################################################################
|
|
|
|
|
# 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);
|
|
|
|
|
|
2026-11-05 12:27:54 +01:00
|
|
|
# DP107: [0] stations found by a scan (0 = none yet), [1] current station.
|
2026-10-08 10:05:56 +03:00
|
|
|
- sensor_datapoint: 107
|
|
|
|
|
datapoint_type: raw
|
|
|
|
|
then:
|
|
|
|
|
- lambda: |-
|
|
|
|
|
id(radio_station_raw).publish_state(format_hex_pretty(x));
|
2026-11-05 12:27:54 +01:00
|
|
|
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]);
|
2026-10-08 10:05:56 +03:00
|
|
|
|
|
|
|
|
# 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;
|
2026-11-05 12:27:54 +01:00
|
|
|
id(publish_alarm_summaries).execute();
|
2026-10-08 10:05:56 +03:00
|
|
|
|
|
|
|
|
- sensor_datapoint: 122
|
|
|
|
|
datapoint_type: bool
|
|
|
|
|
then:
|
|
|
|
|
- lambda: |-
|
|
|
|
|
if (id(alarm_raw_valid)) id(alarm_raw)[12] = x ? 1 : 0;
|
2026-11-05 12:27:54 +01:00
|
|
|
id(publish_alarm_summaries).execute();
|
2026-10-08 10:05:56 +03:00
|
|
|
|
2026-11-05 12:27:54 +01:00
|
|
|
- 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)
|
|
|
|
|
# [05] mode: 0=sound, 1=sound+light, 2=radio, 3=radio+light
|
|
|
|
|
# [06] sound (this project also observed 8=FM)
|
|
|
|
|
# [07] sound volume 1..16
|
|
|
|
|
# [08] radio station 1..60
|
|
|
|
|
# [09] sunrise lead time in minutes, 10..60
|
|
|
|
|
# [10] brightness after sunrise, 1..20
|
|
|
|
|
# [11] snooze: 0=allowed, 1=not allowed
|
2026-10-08 10:05:56 +03:00
|
|
|
- sensor_datapoint: 127
|
|
|
|
|
datapoint_type: raw
|
|
|
|
|
then:
|
|
|
|
|
- lambda: |-
|
|
|
|
|
id(alarm_settings_raw).publish_state(format_hex_pretty(x));
|
2026-11-05 12:27:54 +01:00
|
|
|
if (x.size() != 48) {
|
2026-10-08 10:05:56 +03:00
|
|
|
ESP_LOGW("alarm", "Unexpected DP127 size: %u", (unsigned) x.size());
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-11-05 12:27:54 +01:00
|
|
|
for (size_t i = 0; i < 48; i++) id(alarm_raw)[i] = x[i];
|
2026-10-08 10:05:56 +03:00
|
|
|
id(alarm_raw_valid) = true;
|
2026-11-05 12:27:54 +01:00
|
|
|
id(publish_alarm_summaries).execute();
|
2026-10-08 10:05:56 +03:00
|
|
|
|
2026-11-05 12:27:54 +01:00
|
|
|
# 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.
|
2026-10-08 10:05:56 +03:00
|
|
|
- sensor_datapoint: 128
|
|
|
|
|
datapoint_type: raw
|
|
|
|
|
then:
|
|
|
|
|
- lambda: |-
|
|
|
|
|
id(sunset_settings_raw).publish_state(format_hex_pretty(x));
|
2026-11-05 12:27:54 +01:00
|
|
|
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;
|
2026-10-08 10:05:56 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
###############################################################################
|
|
|
|
|
# Helper scripts
|
|
|
|
|
###############################################################################
|
|
|
|
|
|
|
|
|
|
script:
|
2026-11-05 12:27:54 +01:00
|
|
|
# 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(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_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);
|
|
|
|
|
|
2026-10-08 10:05:56 +03:00
|
|
|
- id: send_alarm_settings
|
|
|
|
|
mode: restart
|
|
|
|
|
then:
|
2026-11-05 12:27:54 +01:00
|
|
|
- delay: 500ms
|
2026-10-08 10:05:56 +03:00
|
|
|
- lambda: |-
|
|
|
|
|
if (!id(alarm_raw_valid)) {
|
|
|
|
|
ESP_LOGW("alarm", "DP127 has not been received yet; refusing to overwrite it");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-11-05 12:27:54 +01:00
|
|
|
std::vector<uint8_t> data(id(alarm_raw), id(alarm_raw) + 48);
|
2026-10-08 10:05:56 +03:00
|
|
|
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
|
2026-11-05 12:27:54 +01:00
|
|
|
# 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.
|
2026-10-08 10:05:56 +03:00
|
|
|
default_transition_length: 0s
|
2026-11-05 12:27:54 +01:00
|
|
|
web_server:
|
|
|
|
|
sorting_group_id: group_light
|
|
|
|
|
sorting_weight: 1
|
2026-10-08 10:05:56 +03:00
|
|
|
|
|
|
|
|
###############################################################################
|
2026-11-05 12:27:54 +01:00
|
|
|
# Clock and display schedule times
|
2026-10-08 10:05:56 +03:00
|
|
|
###############################################################################
|
|
|
|
|
|
|
|
|
|
datetime:
|
|
|
|
|
- platform: template
|
|
|
|
|
id: lamp_clock
|
|
|
|
|
name: "Lamp Clock"
|
|
|
|
|
type: time
|
|
|
|
|
update_interval: 2s
|
2026-11-05 12:27:54 +01:00
|
|
|
web_server:
|
|
|
|
|
sorting_group_id: group_clock
|
|
|
|
|
sorting_weight: 1
|
2026-10-08 10:05:56 +03:00
|
|
|
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
|
2026-11-05 12:27:54 +01:00
|
|
|
id: display_night_start
|
|
|
|
|
name: "Display Night Start"
|
2026-10-08 10:05:56 +03:00
|
|
|
type: time
|
2026-11-05 12:27:54 +01:00
|
|
|
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
|
2026-10-08 10:05:56 +03:00
|
|
|
|
|
|
|
|
- platform: template
|
2026-11-05 12:27:54 +01:00
|
|
|
id: display_night_end
|
|
|
|
|
name: "Display Night End"
|
2026-10-08 10:05:56 +03:00
|
|
|
type: time
|
2026-11-05 12:27:54 +01:00
|
|
|
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
|
2026-10-08 10:05:56 +03:00
|
|
|
|
|
|
|
|
###############################################################################
|
|
|
|
|
# Selects
|
|
|
|
|
###############################################################################
|
|
|
|
|
|
|
|
|
|
select:
|
2026-11-05 12:27:54 +01:00
|
|
|
- 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
|
|
|
|
|
|
2026-10-08 10:05:56 +03:00
|
|
|
- platform: tuya
|
|
|
|
|
id: clock_display_mode
|
|
|
|
|
name: "Clock Display"
|
|
|
|
|
int_datapoint: 104
|
|
|
|
|
options:
|
|
|
|
|
0: "Off"
|
|
|
|
|
1: "Day"
|
|
|
|
|
2: "Night"
|
|
|
|
|
3: "Auto"
|
2026-11-05 12:27:54 +01:00
|
|
|
web_server:
|
|
|
|
|
sorting_group_id: group_clock
|
|
|
|
|
sorting_weight: 3
|
2026-10-08 10:05:56 +03:00
|
|
|
|
|
|
|
|
- platform: template
|
2026-11-05 12:27:54 +01:00
|
|
|
id: display_night_level
|
|
|
|
|
name: "Display At Night"
|
2026-10-08 10:05:56 +03:00
|
|
|
options:
|
|
|
|
|
- "Off"
|
2026-11-05 12:27:54 +01:00
|
|
|
- "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
|
2026-10-08 10:05:56 +03:00
|
|
|
|
|
|
|
|
###############################################################################
|
|
|
|
|
# Numbers
|
|
|
|
|
###############################################################################
|
|
|
|
|
|
|
|
|
|
number:
|
2026-11-05 12:27:54 +01:00
|
|
|
# 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
|
|
|
|
|
|
2026-10-08 10:05:56 +03:00
|
|
|
- platform: tuya
|
|
|
|
|
id: radio_volume
|
|
|
|
|
name: "Radio Volume"
|
|
|
|
|
number_datapoint: 106
|
|
|
|
|
min_value: 1
|
|
|
|
|
max_value: 16
|
|
|
|
|
step: 1
|
2026-11-05 12:27:54 +01:00
|
|
|
web_server:
|
|
|
|
|
sorting_group_id: group_radio
|
|
|
|
|
sorting_weight: 2
|
2026-10-08 10:05:56 +03:00
|
|
|
|
2026-11-05 12:27:54 +01:00
|
|
|
- platform: tuya
|
|
|
|
|
id: snooze_duration
|
|
|
|
|
name: "Snooze Duration"
|
|
|
|
|
number_datapoint: 116
|
2026-10-08 10:05:56 +03:00
|
|
|
unit_of_measurement: "min"
|
2026-11-05 12:27:54 +01:00
|
|
|
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
|
2026-10-08 10:05:56 +03:00
|
|
|
|
|
|
|
|
###############################################################################
|
|
|
|
|
# Switches
|
|
|
|
|
###############################################################################
|
|
|
|
|
|
|
|
|
|
switch:
|
|
|
|
|
- platform: tuya
|
|
|
|
|
id: fm_radio
|
|
|
|
|
name: "FM Radio"
|
|
|
|
|
switch_datapoint: 105
|
2026-11-05 12:27:54 +01:00
|
|
|
web_server:
|
|
|
|
|
sorting_group_id: group_radio
|
|
|
|
|
sorting_weight: 1
|
2026-10-08 10:05:56 +03:00
|
|
|
|
|
|
|
|
- platform: tuya
|
|
|
|
|
id: sunset_mode
|
|
|
|
|
name: "Sunset / Sleep Mode"
|
|
|
|
|
switch_datapoint: 121
|
2026-11-05 12:27:54 +01:00
|
|
|
web_server:
|
|
|
|
|
sorting_group_id: group_sunset
|
|
|
|
|
sorting_weight: 1
|
2026-10-08 10:05:56 +03:00
|
|
|
|
|
|
|
|
- platform: tuya
|
|
|
|
|
id: ambient_light
|
|
|
|
|
name: "Ambient Light"
|
|
|
|
|
switch_datapoint: 129
|
2026-11-05 12:27:54 +01:00
|
|
|
web_server:
|
|
|
|
|
sorting_group_id: group_light
|
|
|
|
|
sorting_weight: 2
|
2026-10-08 10:05:56 +03:00
|
|
|
|
|
|
|
|
- platform: tuya
|
|
|
|
|
id: clock_24_hour
|
|
|
|
|
name: "24 Hour Clock"
|
|
|
|
|
switch_datapoint: 131
|
2026-11-05 12:27:54 +01:00
|
|
|
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
|
2026-10-08 10:05:56 +03:00
|
|
|
|
|
|
|
|
###############################################################################
|
|
|
|
|
# Diagnostics / still-undecoded payloads
|
|
|
|
|
###############################################################################
|
|
|
|
|
|
|
|
|
|
text_sensor:
|
|
|
|
|
- platform: template
|
|
|
|
|
id: clock_time_text
|
|
|
|
|
name: "Clock Time"
|
|
|
|
|
update_interval: never
|
2026-11-05 12:27:54 +01:00
|
|
|
web_server:
|
|
|
|
|
sorting_group_id: group_clock
|
|
|
|
|
sorting_weight: 2
|
2026-10-08 10:05:56 +03:00
|
|
|
|
|
|
|
|
- platform: template
|
|
|
|
|
id: radio_station_raw
|
|
|
|
|
name: "Radio Station Raw"
|
|
|
|
|
update_interval: never
|
2026-11-05 12:27:54 +01:00
|
|
|
web_server:
|
|
|
|
|
sorting_group_id: group_radio
|
|
|
|
|
sorting_weight: 9
|
2026-10-08 10:05:56 +03:00
|
|
|
|
|
|
|
|
- platform: template
|
|
|
|
|
id: alarm_settings_raw
|
|
|
|
|
name: "Alarm Settings Raw"
|
|
|
|
|
update_interval: never
|
2026-11-05 12:27:54 +01:00
|
|
|
web_server:
|
|
|
|
|
sorting_group_id: group_device
|
|
|
|
|
sorting_weight: 10
|
2026-10-08 10:05:56 +03:00
|
|
|
|
|
|
|
|
- platform: template
|
|
|
|
|
id: sunset_settings_raw
|
|
|
|
|
name: "Sunset Settings Raw"
|
|
|
|
|
update_interval: never
|
2026-11-05 12:27:54 +01:00
|
|
|
web_server:
|
|
|
|
|
sorting_group_id: group_device
|
|
|
|
|
sorting_weight: 11
|
2026-10-08 10:05:56 +03:00
|
|
|
|
2026-11-05 12:27:54 +01:00
|
|
|
- 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
|
|
|
|
|
|
|
|
|
|
###############################################################################
|
|
|
|
|
# Device health
|
|
|
|
|
###############################################################################
|
|
|
|
|
|
|
|
|
|
debug:
|
|
|
|
|
update_interval: 60s
|
|
|
|
|
|
|
|
|
|
sensor:
|
2026-10-08 10:05:56 +03:00
|
|
|
- platform: template
|
2026-11-05 12:27:54 +01:00
|
|
|
id: radio_stations_found
|
|
|
|
|
name: "Radio Stations Found"
|
|
|
|
|
accuracy_decimals: 0
|
2026-10-08 10:05:56 +03:00
|
|
|
update_interval: never
|
2026-11-05 12:27:54 +01:00
|
|
|
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:
|
|
|
|
|
- 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
|
2026-10-08 10:05:56 +03:00
|
|
|
|
|
|
|
|
###############################################################################
|
|
|
|
|
# Buttons
|
|
|
|
|
###############################################################################
|
|
|
|
|
|
|
|
|
|
button:
|
2026-11-05 12:27:54 +01:00
|
|
|
- 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
|
|
|
|
|
|
2026-10-08 10:05:56 +03:00
|
|
|
- platform: template
|
|
|
|
|
name: "Sync Lamp Clock Now"
|
|
|
|
|
icon: "mdi:clock-sync"
|
2026-11-05 12:27:54 +01:00
|
|
|
web_server:
|
|
|
|
|
sorting_group_id: group_clock
|
|
|
|
|
sorting_weight: 6
|
2026-10-08 10:05:56 +03:00
|
|
|
on_press:
|
|
|
|
|
- script.execute: sync_lamp_clock
|
|
|
|
|
|
2026-11-05 12:27:54 +01:00
|
|
|
# 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: "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
|
|
|
|
|
|
2026-10-08 10:05:56 +03:00
|
|
|
- platform: restart
|
|
|
|
|
name: "Restart Wake Up Light"
|
2026-11-05 12:27:54 +01:00
|
|
|
web_server:
|
|
|
|
|
sorting_group_id: group_device
|
|
|
|
|
sorting_weight: 5
|
2026-10-08 10:05:56 +03:00
|
|
|
|
|
|
|
|
###############################################################################
|
|
|
|
|
# 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:
|
2026-11-05 12:27:54 +01:00
|
|
|
ssid: !secret wifi_ssid
|
|
|
|
|
password: !secret wifi_password
|
2026-10-08 10:05:56 +03:00
|
|
|
ap:
|
|
|
|
|
ssid: ${ap_ssid}
|
2026-11-05 12:27:54 +01:00
|
|
|
password: !secret ap_password
|
|
|
|
|
|
|
|
|
|
api:
|
|
|
|
|
encryption:
|
|
|
|
|
key: !secret api_encryption_key
|
2026-10-08 10:05:56 +03:00
|
|
|
|
|
|
|
|
web_server:
|
|
|
|
|
port: 80
|
2026-11-05 12:27:54 +01:00
|
|
|
version: 3
|
2026-10-08 10:05:56 +03:00
|
|
|
local: true
|
2026-11-05 12:27:54 +01:00
|
|
|
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
|
2026-10-08 10:05:56 +03:00
|
|
|
# Recommended on a shared LAN:
|
|
|
|
|
# auth:
|
|
|
|
|
# username: admin
|
|
|
|
|
# password: !secret web_password
|
|
|
|
|
|
|
|
|
|
ota:
|
|
|
|
|
- platform: esphome
|
|
|
|
|
- platform: web_server
|