lvgl: id: lvgl_comp displays: - lcd_display touchscreens: - touch_gt911 pages: - id: page_home widgets: - obj: width: 100% height: 100% pad_all: 0 border_width: 0 outline_width: 0 scrollable: false widgets: - label: align: top_left x: 70 y: 20 text: "-- --" # static placeholder clock — live time is a fast-follow, no time: component this pass text_font: montserrat_16 - obj: id: mode_toggle align: top_mid y: 12 width: 400px height: 44px radius: 22 bg_color: 0x333333 border_width: 0 outline_width: 0 pad_all: 4 scrollable: false layout: type: flex flex_flow: row flex_align_cross: center flex_align_track: center 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" text_font: montserrat_16 - button: width: 120px height: 36px radius: 18 bg_opa: transp widgets: - label: text: "My Home" text_font: montserrat_16 - label: align: top_right x: -16 y: 20 text: "Firecrest" text_font: montserrat_16 - obj: id: temp_view width: 100% height: 100% pad_all: 0 border_width: 0 outline_width: 0 scrollable: false bg_opa: transp # LVGL's base obj is clickable by default (obj->flags = LV_OBJ_FLAG_CLICKABLE in # lv_obj_create) and lv_obj_hit_test() requires that flag before checking geometry. # Being a full-screen, topmost sibling of mode_toggle, this container was silently # swallowing every tap over the toggle bar (no on_click, so nothing visibly happened) # before the touch search could reach mode_toggle underneath — same z-order class of # bug as the bg_opa fix above, but for touch capture instead of rendering. clickable: 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 physically height: 360px # distinct. LVGL's adv_hittest arc hit test only accepts touches within # the arc's own ring band (radius - arc_width to radius, in its angular # span) — confirmed via lv_arc.c LV_EVENT_HIT_TEST. Same-radius arcs # occupy an identical ring band, so adv_hittest alone CANNOT disambiguate # them (verified on hardware: making both 400px left only the topmost, # cool_arc, draggable). Differing radii are required, not optional. 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 on_click: lvgl.widget.show: mode_select_overlay - label: id: home_temp_label text: "-- °F" 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 bg_opa: transp clickable: false # see temp_view comment above — same touch-capture fix layout: type: flex flex_flow: column flex_align_cross: center flex_align_track: center flex_align_main: space_evenly widgets: - label: 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: status_high_label text: "High: -- °F" text_font: montserrat_20 - label: id: status_low_label text: "Low: -- °F" text_font: montserrat_20 - label: id: status_precip_label text: "Precipitation: -- %" text_font: montserrat_20 - id: page_sensors widgets: - obj: width: 100% height: 510px pad_all: 16 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: space_evenly widgets: - label: id: sensors_temp_label text: "Temperature: -- °F" text_font: montserrat_28 - label: id: sensors_humidity_label text: "Humidity: -- %" text_font: montserrat_28 - label: id: sensors_pressure_label text: "Pressure: -- hPa" text_font: montserrat_28 - id: page_settings widgets: - obj: width: 100% height: 510px pad_all: 16 border_width: 0 outline_width: 0 scrollable: false layout: type: flex flex_flow: column flex_align_cross: center flex_align_track: start flex_align_main: start widgets: - label: id: settings_ip_label text: "IP: --" text_font: montserrat_20 - label: id: settings_uptime_label text: "Uptime: -- min" text_font: montserrat_20 - label: text: "Manual relay override (bypasses thermostat):" text_font: montserrat_16 - obj: width: 100% height: size_content border_width: 0 outline_width: 0 scrollable: false layout: type: flex flex_flow: row_wrap flex_align_cross: start flex_align_track: start flex_align_main: start widgets: - checkbox: text: "Fan" on_click: switch.toggle: relay_fan - checkbox: text: "Cool 1" on_click: switch.toggle: relay_cool1 - checkbox: text: "Cool 2" on_click: switch.toggle: relay_cool2 - checkbox: text: "Heat 1" on_click: switch.toggle: relay_heat1 - checkbox: text: "Heat 2" on_click: switch.toggle: relay_heat2 - id: page_schedule widgets: - obj: width: 100% height: 510px pad_all: 16 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: space_evenly widgets: - label: text: "Preset" text_font: montserrat_28 - obj: width: 100% height: size_content border_width: 0 outline_width: 0 scrollable: false layout: type: flex flex_flow: row flex_align_cross: center flex_align_track: center flex_align_main: space_evenly widgets: - button: width: 300px height: 100px on_click: climate.control: id: main_thermostat preset: home widgets: - label: text: "Home" text_font: montserrat_28 - button: width: 300px height: 100px on_click: climate.control: id: main_thermostat preset: away widgets: - label: text: "Away" text_font: montserrat_28 - label: text: "Time-based scheduling runs in Home Assistant, not on this device." text_font: montserrat_16 top_layer: widgets: - button: id: hamburger_btn align: top_left x: 16 y: 16 width: 60px height: 60px radius: 8 on_click: lvgl.widget.show: nav_menu_overlay widgets: - label: text: "=" text_font: montserrat_28 - obj: id: nav_menu_overlay hidden: true align: top_left x: 16 y: 90px width: 280px height: size_content pad_all: 8 radius: 8 layout: type: flex flex_flow: column flex_align_cross: stretch flex_align_track: start flex_align_main: start widgets: - button: on_click: - lvgl.page.show: page_home - lvgl.widget.hide: nav_menu_overlay widgets: - label: text: "Home" text_font: montserrat_20 - button: on_click: - lvgl.page.show: page_sensors - lvgl.widget.hide: nav_menu_overlay widgets: - label: text: "Sensors" text_font: montserrat_20 - button: on_click: - lvgl.page.show: page_settings - lvgl.widget.hide: nav_menu_overlay widgets: - label: text: "Settings" text_font: montserrat_20 - button: on_click: - lvgl.page.show: page_schedule - lvgl.widget.hide: nav_menu_overlay widgets: - label: text: "Schedule" text_font: montserrat_20 - obj: id: mode_select_overlay hidden: true align: center width: 280px height: size_content pad_all: 8 radius: 8 layout: type: flex flex_flow: column flex_align_cross: stretch flex_align_track: start flex_align_main: start widgets: - button: on_click: - climate.control: id: main_thermostat mode: HEAT - lvgl.widget.hide: mode_select_overlay widgets: - label: text: "Heat" text_font: montserrat_20 - button: on_click: - climate.control: id: main_thermostat mode: COOL - lvgl.widget.hide: mode_select_overlay widgets: - label: text: "Cool" text_font: montserrat_20 - button: on_click: - climate.control: id: main_thermostat mode: "OFF" - lvgl.widget.hide: mode_select_overlay widgets: - label: text: "Off" text_font: montserrat_20