Files
2026-07-19 16:49:55 +08:00

246 lines
10 KiB
YAML

# ─────────────────────────────────────────────────────────────────────────────
# NEON watchface (optional home face). Add to the thin config's files: list to
# enable; the picker (Settings > Home > Watchface) then offers it. Watchface id 1.
#
# A full-screen home page with huge two-tone digits, neon rim rings, weather +
# room/battery stats. It reuses core's clock/weather/room/battery sensors and the
# g_show_weather / g_show_climate display toggles - no extra entities needed.
#
# Contract (mirrors the screen carousel): self-registers in the watchface registry
# at boot, renders itself while active, and self-shows its page when core requests
# the home base (core can't reference an optional page id, hence the g_wf_req flag).
# ─────────────────────────────────────────────────────────────────────────────
esphome:
on_boot:
- priority: 655
then:
- lambda: 'id(g_wf_present)[1] = true; id(g_wf_name)[1] = "Neon";'
globals:
- { id: g_nf_last_min, type: int, restore_value: no, initial_value: '-1' } # perf: only redraw on minute change
- { id: g_nf_upd_min, type: bool, restore_value: no, initial_value: 'false' }
font:
# Huge bold digits - only 0-9 needed, so it stays tiny in flash
- file: { type: gfonts, family: "Roboto", weight: 900 }
id: font_neon
size: 108
glyphs: "0123456789"
lvgl:
pages:
- id: page_home_neon
bg_color: 0x000000
widgets:
- obj: # outer neon ring at the rim
id: nf_ring_out
align: CENTER
width: 352
height: 352
radius: 176
bg_opa: TRANSP
border_width: 4
border_color: 0xFF2E88
border_opa: 60%
clickable: false
scrollable: false
- obj: # second neon ring, also hugging the rim (interior left free for content)
id: nf_ring_in
align: CENTER
width: 320
height: 320
radius: 160
bg_opa: TRANSP
border_width: 4
border_color: 0x9A5BFF
border_opa: 55%
clickable: false
scrollable: false
- label: # weather icon
id: nf_wx_ic
align: CENTER
x: -34
y: -103
text: "󰖕"
text_font: icons34
text_color: 0xBFD2DA
- label: # outdoor temperature
id: nf_wx_temp
align: CENTER
x: 26
y: -106
text: "--°"
text_font: font_sel
text_color: 0xFFFFFF
- label: # condition text (UPPERCASE)
id: nf_wx_cond
align: CENTER
y: -76
text: ""
text_font: font_artist
text_color: 0x8A93A0
- label: # HOURS
id: nf_hh
align: CENTER
x: -70
y: -4
text: "00"
text_font: font_neon
text_color: 0xFF2E88
- label: # MINUTES
id: nf_mm
align: CENTER
x: 70
y: -4
text: "00"
text_font: font_neon
text_color: 0x9A5BFF
- label: # date
id: nf_date
align: CENTER
y: 70
text: ""
text_font: font_med
text_color: 0x6f9a92
- label: # room temperature icon
id: nf_ct_ic
align: CENTER
x: -60
y: 99
text: "󰔏"
text_font: icons18
text_color: 0xFF8A5C
- label:
id: nf_ct_val
align: CENTER
x: -32
y: 99
text: "--"
text_font: font_artist
text_color: 0x9AA3B0
- label: # room humidity icon
id: nf_hum_ic
align: CENTER
x: 16
y: 99
text: "󰖎"
text_font: icons18
text_color: 0x3FA9D6
- label:
id: nf_hum_val
align: CENTER
x: 42
y: 99
text: "--"
text_font: font_artist
text_color: 0x9AA3B0
- label: # battery (glyph + %), mirrored from core's lbl_batt (same LVGL symbols)
id: nf_bt
align: CENTER
y: 126
text: ""
text_font: montserrat_18
text_color: 0x6f9a92
- label: # timer badge (active timer - mins left), mirrors the classic face
id: nf_timer
align: TOP_MID
y: 22
text: ""
text_font: montserrat_18
text_color: 0xFFFFFF
bg_color: 0xFF3B30
bg_opa: COVER
radius: 14
pad_left: 12
pad_right: 12
pad_top: 5
pad_bottom: 5
hidden: true
interval:
# Self-show: core sets g_wf_req when it wants the home base shown; we own page_home_neon.
- interval: 50ms
then:
- if:
condition: { lambda: 'return id(g_wf_req) && id(g_watchface) == 1;' }
then:
- lambda: 'id(g_wf_req) = false; id(g_nf_last_min) = -1;' # force a full redraw on entry
# slide in matching the carousel direction (like Classic / the screens) - no laggy fade
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_home_neon, animation: move_left, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_home_neon, animation: move_right, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_home_neon, animation: move_top, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_home_neon, animation: move_bottom, time: 250ms }] }
- script.execute: neon_render
# Refresh while this is the active home face
- interval: 1s
then:
- if:
condition: { lambda: 'return id(g_watchface) == 1 && id(g_base) == 0;' }
then: [ script.execute: neon_render ]
script:
- id: neon_render
then:
# Cheap tick: figure out if the minute actually changed (heavy redraws only then)
- lambda: |-
id(g_nf_upd_min) = false;
auto t = id(ha_time).now();
if (!t.is_valid()) return;
int mn = t.hour * 60 + t.minute;
if (mn != id(g_nf_last_min)) { id(g_nf_last_min) = mn; id(g_nf_upd_min) = true; }
- if:
condition: { lambda: 'return id(g_nf_upd_min);' }
then:
- lvgl.label.update: { id: nf_hh, text: !lambda 'return id(ha_time).now().strftime("%H");' }
- lvgl.label.update: { id: nf_mm, text: !lambda 'return id(ha_time).now().strftime("%M");' }
- lvgl.label.update: { id: nf_wx_ic, text: !lambda 'return id(g_nf_glyph).empty() ? std::string("󰖕") : id(g_nf_glyph);' }
- lvgl.label.update: { id: nf_wx_cond, text: !lambda 'return id(g_nf_cond);' }
- lvgl.label.update:
id: nf_wx_temp
text: !lambda |-
if (!id(wx_temp).has_state()) return std::string("--°");
char b[10]; snprintf(b, sizeof(b), "%d°", (int) roundf(id(wx_temp).state)); return std::string(b);
- lvgl.label.update:
id: nf_date
text: !lambda |-
auto t = id(ha_time).now();
static const char* d[8] = {"","Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
std::string day = (t.day_of_week >= 1 && t.day_of_week <= 7) ? std::string(d[t.day_of_week]) : std::string("");
return t.strftime("%d %b") + " " + day;
- lvgl.label.update:
id: nf_ct_val
text: !lambda 'if (!id(room_temp).has_state()) return std::string("--"); char b[12]; snprintf(b, sizeof(b), "%.1f°", id(room_temp).state); return std::string(b);'
- lvgl.label.update:
id: nf_hum_val
text: !lambda 'if (!id(room_hum).has_state()) return std::string("--"); char b[8]; snprintf(b, sizeof(b), "%d%%", (int) roundf(id(room_hum).state)); return std::string(b);'
# Battery: mirror core's lbl_batt text + colour
- lambda: 'lv_label_set_text(id(nf_bt), lv_label_get_text(id(lbl_batt)));'
- if:
condition: { lambda: 'return id(g_charging);' }
then: [ lvgl.label.update: { id: nf_bt, text_color: 0x35c759 } ]
else: [ lvgl.label.update: { id: nf_bt, text_color: 0x6f9a92 } ]
# Weather / climate visibility obey the Display toggles
- if:
condition: { lambda: 'return id(g_show_weather);' }
then: [ lvgl.widget.show: nf_wx_ic, lvgl.widget.show: nf_wx_temp, lvgl.widget.show: nf_wx_cond ]
else: [ lvgl.widget.hide: nf_wx_ic, lvgl.widget.hide: nf_wx_temp, lvgl.widget.hide: nf_wx_cond ]
- if:
condition: { lambda: 'return id(g_show_climate);' }
then: [ lvgl.widget.show: nf_ct_ic, lvgl.widget.show: nf_ct_val, lvgl.widget.show: nf_hum_ic, lvgl.widget.show: nf_hum_val ]
else: [ lvgl.widget.hide: nf_ct_ic, lvgl.widget.hide: nf_ct_val, lvgl.widget.hide: nf_hum_ic, lvgl.widget.hide: nf_hum_val ]
# Timer badge - kept responsive (cheap) so it appears/updates promptly
- if:
condition: { lambda: 'return id(g_timer_active);' }
then:
- lvgl.widget.show: nf_timer
- lvgl.label.update:
id: nf_timer
text: !lambda |-
int s = id(g_timer_secs); if (s < 0) s = 0;
int m = (s + 59) / 60;
char b[24]; snprintf(b, sizeof(b), " %d min", m);
return std::string(b);
else:
- lvgl.widget.hide: nf_timer