From 245bb7920384990259c05ecc7781e01654e86873 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Sun, 12 Jul 2026 12:59:34 -0700 Subject: [PATCH] feat: add HA weather bindings, Status tab, and Temp/Status toggle wiring --- firmware/components/display-classic.yaml | 362 ++++++++++++--------- firmware/components/weather.yaml | 84 +++++ firmware/waveshare-thermostat-classic.yaml | 1 + 3 files changed, 301 insertions(+), 146 deletions(-) create mode 100644 firmware/components/weather.yaml diff --git a/firmware/components/display-classic.yaml b/firmware/components/display-classic.yaml index 9889194..73c618d 100644 --- a/firmware/components/display-classic.yaml +++ b/firmware/components/display-classic.yaml @@ -41,20 +41,40 @@ lvgl: flex_align_main: space_evenly widgets: - button: + id: toggle_temp_btn width: 120px height: 36px radius: 18 bg_color: 0xFFFFFF + on_click: + - lvgl.widget.show: temp_view + - lvgl.widget.hide: status_view + - lvgl.widget.update: + id: toggle_temp_btn + bg_color: 0xFFFFFF + - lvgl.widget.update: + id: toggle_status_btn + bg_opa: transp widgets: - label: text: "Temp" text_color: 0x000000 text_font: montserrat_16 - button: + id: toggle_status_btn width: 120px height: 36px radius: 18 bg_opa: transp + on_click: + - lvgl.widget.show: status_view + - lvgl.widget.hide: temp_view + - lvgl.widget.update: + id: toggle_status_btn + bg_color: 0xFFFFFF + - lvgl.widget.update: + id: toggle_temp_btn + bg_opa: transp widgets: - label: text: "Status" @@ -74,101 +94,184 @@ lvgl: y: 20 text: "Firecrest" text_font: montserrat_16 - - arc: - id: dial_bg_arc - align: center - y: -20 - width: 400px - height: 400px - start_angle: 315 - end_angle: 225 - min_value: 0 - max_value: 1 - value: 0 - adjustable: false - # arc_color/arc_width are flattened to top-level (MAIN part) rather than nested - # under a "main:" key — an explicit "main:" sub-block silently suppresses the - # implicit top-level MAIN-part properties (align/width/height/x/y), verified via - # isolated test compile. indicator:/knob: below are unaffected since those are - # genuinely separate, non-MAIN parts that require nesting. - arc_color: 0x444444 - arc_width: 10 - indicator: - arc_opa: transp - knob: - arc_opa: transp - - arc: - id: heat_arc - align: center - y: -20 - width: 400px - height: 400px - start_angle: 315 - end_angle: 225 - min_value: 60 - max_value: 90 - value: 70 - adjustable: true - adv_hittest: true # LVGL's default touch hit-test just uses each widget's full - # bounding square (any topmost widget under the touch point - # wins), which is why the radius difference alone didn't fix - # touch disambiguation. adv_hittest switches to LVGL's precise - # ring-based hit test (see LV_EVENT_HIT_TEST in lv_arc.c), - # which actually respects each arc's own radius. - arc_opa: transp # flattened, see dial_bg_arc comment above - indicator: - arc_color: 0xFF3B30 - arc_width: 10 - knob: - bg_color: 0xFF3B30 - on_value: - then: - - climate.control: - id: main_thermostat - target_temperature_low: !lambda "return (x - 32.0f) * 5.0f / 9.0f;" - - lvgl.label.update: - id: heat_to_value_label - text: - format: "%.0f" - args: ["x"] - - arc: - id: cool_arc - align: center - y: -20 - width: 360px # smaller radius than heat_arc (400px) so the two rings are visually - height: 360px # distinct; touch disambiguation itself comes from adv_hittest below, - # not from this size difference alone (a same-radius pair would also - # work correctly with adv_hittest on both, but differing radii keeps - # the two rings visually separable too) - start_angle: 315 - end_angle: 225 - mode: REVERSE - min_value: 60 - max_value: 90 - value: 75 - adjustable: true - adv_hittest: true # see heat_arc comment above — precise ring-based hit test - arc_opa: transp # flattened, see dial_bg_arc comment above - indicator: - arc_color: 0x0A84FF - arc_width: 10 - knob: - bg_color: 0x0A84FF - on_value: - then: - - climate.control: - id: main_thermostat - target_temperature_high: !lambda "return (x - 32.0f) * 5.0f / 9.0f;" - - lvgl.label.update: - id: cool_to_value_label - text: - format: "%.0f" - args: ["x"] - obj: - align: center - y: -40 - width: 200px - height: size_content + id: temp_view + width: 100% + height: 100% + pad_all: 0 + border_width: 0 + outline_width: 0 + scrollable: false + widgets: + - arc: + id: dial_bg_arc + align: center + y: -20 + width: 400px + height: 400px + start_angle: 315 + end_angle: 225 + min_value: 0 + max_value: 1 + value: 0 + adjustable: false + # arc_color/arc_width are flattened to top-level (MAIN part) rather than nested + # under a "main:" key — an explicit "main:" sub-block silently suppresses the + # implicit top-level MAIN-part properties (align/width/height/x/y), verified via + # isolated test compile. indicator:/knob: below are unaffected since those are + # genuinely separate, non-MAIN parts that require nesting. + arc_color: 0x444444 + arc_width: 10 + indicator: + arc_opa: transp + knob: + arc_opa: transp + - arc: + id: heat_arc + align: center + y: -20 + width: 400px + height: 400px + start_angle: 315 + end_angle: 225 + min_value: 60 + max_value: 90 + value: 70 + adjustable: true + adv_hittest: true # LVGL's default touch hit-test just uses each widget's full + # bounding square (any topmost widget under the touch point + # wins), which is why the radius difference alone didn't fix + # touch disambiguation. adv_hittest switches to LVGL's precise + # ring-based hit test (see LV_EVENT_HIT_TEST in lv_arc.c), + # which actually respects each arc's own radius. + arc_opa: transp # flattened, see dial_bg_arc comment above + indicator: + arc_color: 0xFF3B30 + arc_width: 10 + knob: + bg_color: 0xFF3B30 + on_value: + then: + - climate.control: + id: main_thermostat + target_temperature_low: !lambda "return (x - 32.0f) * 5.0f / 9.0f;" + - lvgl.label.update: + id: heat_to_value_label + text: + format: "%.0f" + args: ["x"] + - arc: + id: cool_arc + align: center + y: -20 + width: 360px # smaller radius than heat_arc (400px) so the two rings are visually + height: 360px # distinct; touch disambiguation itself comes from adv_hittest below, + # not from this size difference alone (a same-radius pair would also + # work correctly with adv_hittest on both, but differing radii keeps + # the two rings visually separable too) + start_angle: 315 + end_angle: 225 + mode: REVERSE + min_value: 60 + max_value: 90 + value: 75 + adjustable: true + adv_hittest: true # see heat_arc comment above — precise ring-based hit test + arc_opa: transp # flattened, see dial_bg_arc comment above + indicator: + arc_color: 0x0A84FF + arc_width: 10 + knob: + bg_color: 0x0A84FF + on_value: + then: + - climate.control: + id: main_thermostat + target_temperature_high: !lambda "return (x - 32.0f) * 5.0f / 9.0f;" + - lvgl.label.update: + id: cool_to_value_label + text: + format: "%.0f" + args: ["x"] + - obj: + align: center + y: -40 + width: 200px + height: size_content + border_width: 0 + outline_width: 0 + scrollable: false + layout: + type: flex + flex_flow: column + flex_align_cross: center + flex_align_track: center + flex_align_main: center + widgets: + - label: + id: home_mode_label + text: "Mode: --" + text_font: montserrat_20 + - label: + id: home_temp_label + text: "-- °C" + text_font: montserrat_48 + - button: + align: bottom_left + x: 24 + y: -24 + width: 90px + height: 90px + radius: 45 + on_click: + climate.control: + id: main_thermostat + preset: away + widgets: + - label: + align: center + y: -10 + text: "Away" + text_font: montserrat_16 + - button: + align: bottom_mid + x: -110 + y: -24 + width: 90px + height: 60px + radius: 30 + widgets: + - label: + id: heat_to_value_label + align: center + text: "70" + text_font: montserrat_20 + - button: + align: bottom_mid + x: 110 + y: -24 + width: 90px + height: 60px + radius: 30 + widgets: + - label: + id: cool_to_value_label + align: center + text: "75" + text_font: montserrat_20 + - label: + id: outside_temp_label + align: bottom_right + x: -24 + y: -24 + text: "-- °" + text_font: montserrat_20 + - obj: + id: status_view + hidden: true + width: 100% + height: 100% + pad_all: 16 border_width: 0 outline_width: 0 scrollable: false @@ -177,65 +280,32 @@ lvgl: flex_flow: column flex_align_cross: center flex_align_track: center - flex_align_main: center + flex_align_main: space_evenly widgets: - label: - id: home_mode_label - text: "Mode: --" + id: status_condition_label + text: "--" + text_font: montserrat_32 + - label: + id: status_temp_label + text: "Outside: -- °F" + text_font: montserrat_24 + - label: + id: status_humidity_label + text: "Humidity: -- %" text_font: montserrat_20 - label: - id: home_temp_label - text: "-- °C" - text_font: montserrat_48 - - button: - align: bottom_left - x: 24 - y: -24 - width: 90px - height: 90px - radius: 45 - on_click: - climate.control: - id: main_thermostat - preset: away - widgets: - - label: - align: center - y: -10 - text: "Away" - text_font: montserrat_16 - - button: - align: bottom_mid - x: -110 - y: -24 - width: 90px - height: 60px - radius: 30 - widgets: - - label: - id: heat_to_value_label - align: center - text: "70" + id: status_high_label + text: "High: -- °F" text_font: montserrat_20 - - button: - align: bottom_mid - x: 110 - y: -24 - width: 90px - height: 60px - radius: 30 - widgets: - label: - id: cool_to_value_label - align: center - text: "75" + id: status_low_label + text: "Low: -- °F" + text_font: montserrat_20 + - label: + id: status_precip_label + text: "Precipitation: -- %" text_font: montserrat_20 - - label: - align: bottom_right - x: -24 - y: -24 - text: "-- °" # outside temp stub — weather data source deferred per design spec - text_font: montserrat_20 - id: page_sensors widgets: - obj: diff --git a/firmware/components/weather.yaml b/firmware/components/weather.yaml new file mode 100644 index 0000000..44128ff --- /dev/null +++ b/firmware/components/weather.yaml @@ -0,0 +1,84 @@ +text_sensor: + - platform: homeassistant + id: weather_condition + entity_id: weather.forecast_home + on_value: + then: + - lvgl.label.update: + id: status_condition_label + text: !lambda |- + if (x == "clear-night") return "Clear"; + if (x == "cloudy") return "Cloudy"; + if (x == "exceptional") return "Exceptional"; + if (x == "fog") return "Foggy"; + if (x == "hail") return "Hail"; + if (x == "lightning") return "Lightning"; + if (x == "lightning-rainy") return "Thunderstorms"; + if (x == "partlycloudy") return "Partly Cloudy"; + if (x == "pouring") return "Pouring Rain"; + if (x == "rainy") return "Rainy"; + if (x == "snowy") return "Snowy"; + if (x == "snowy-rainy") return "Snow/Rain"; + if (x == "sunny") return "Sunny"; + if (x == "windy") return "Windy"; + if (x == "windy-variant") return "Windy"; + return x; + +sensor: + - platform: homeassistant + id: weather_outside_temp + entity_id: weather.forecast_home + attribute: temperature + on_value: + then: + - lvgl.label.update: + id: outside_temp_label + text: + format: "%.0f °" + args: ["x"] + - lvgl.label.update: + id: status_temp_label + text: + format: "Outside: %.0f °F" + args: ["x"] + - platform: homeassistant + id: weather_humidity + entity_id: weather.forecast_home + attribute: humidity + on_value: + then: + - lvgl.label.update: + id: status_humidity_label + text: + format: "Humidity: %.0f%%" + args: ["x"] + - platform: homeassistant + id: weather_forecast_high + entity_id: sensor.firecrest_forecast_high + on_value: + then: + - lvgl.label.update: + id: status_high_label + text: + format: "High: %.0f °F" + args: ["x"] + - platform: homeassistant + id: weather_forecast_low + entity_id: sensor.firecrest_forecast_low + on_value: + then: + - lvgl.label.update: + id: status_low_label + text: + format: "Low: %.0f °F" + args: ["x"] + - platform: homeassistant + id: weather_precip_prob + entity_id: sensor.firecrest_precip_probability + on_value: + then: + - lvgl.label.update: + id: status_precip_label + text: + format: "Precipitation: %.0f%%" + args: ["x"] diff --git a/firmware/waveshare-thermostat-classic.yaml b/firmware/waveshare-thermostat-classic.yaml index ee07455..a0d294e 100644 --- a/firmware/waveshare-thermostat-classic.yaml +++ b/firmware/waveshare-thermostat-classic.yaml @@ -50,3 +50,4 @@ packages: relays: !include components/relays.yaml climate: !include components/climate.yaml diagnostics: !include components/diagnostics.yaml + weather: !include components/weather.yaml