# ───────────────────────────────────────────────────────────────────────────── # 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