The thermostat had no way to actually set its operating mode: home_mode_label
was read-only, and no widget anywhere called climate.control with a mode.
Tapping the mode label now shows a modal (mirrors the existing
nav_menu_overlay pattern) with explicit Heat/Cool/Off choices.
Making both arcs 400px (same-size rings, requested for visual alignment)
broke independent dragging: LVGL's adv_hittest arc hit test only accepts
touches within an arc's own ring band (radius - arc_width to radius, within
its angular span), confirmed via lv_arc.c LV_EVENT_HIT_TEST. Two identical
rings occupy the same band, so only the topmost (cool_arc) ever won the
touch, confirmed on hardware. Reverting to 400px/360px nested rings restores
independent heat/cool dragging; same-size same-position arcs are not
achievable with LVGL's current hit-testing without a larger redesign.
cool_arc was 360px against heat_arc's 400px, nested rather than aligned.
Touch disambiguation already comes entirely from adv_hittest, not radius
difference, so matching them to 400px doesn't affect touch targeting.
sensors.yaml formatted the raw BME280 reading directly with no unit
conversion, inconsistent with the dial setpoints and weather widget, which
are already Fahrenheit. Converts at the only point this sensor's value
reaches the UI (shared by every display variant); climate.yaml's internal
Celsius values are untouched since ESPHome's climate component always
operates in Celsius regardless of display unit.
LVGL's base obj is clickable by default (lv_obj_create sets
LV_OBJ_FLAG_CLICKABLE unconditionally). As full-screen, topmost siblings of
mode_toggle, temp_view and status_view were silently capturing every tap
over the toggle bar via lv_obj_hit_test before the touch search could reach
mode_toggle underneath, even though they have no on_click of their own.
Confirmed via generated C++ (lv_obj_remove_flag ... LV_OBJ_FLAG_CLICKABLE)
and hardware testing.
Root cause of 'only the cool/blue knob drags, regardless of where you
tap': LVGL's default arc hit-testing just uses each widget's full
bounding square (whichever topmost widget's box contains the touch
point wins), not the precise ring geometry. Since cool_arc's 360x360
box is entirely contained within heat_arc's 400x400 box, cool_arc (on
top in z-order) claimed every touch inside its own square, including
touches physically on heat_arc's outer ring, which sits outside
cool_arc's box but still within heat_arc's simple bounding-box test.
Confirmed via lv_arc.c: LVGL has a precise ring-based hit test
(LV_EVENT_HIT_TEST, respects each widget's own radius and angular
span) but it's opt-in via the ADV_HITTEST flag; without it, LVGL
falls back to the simple bounding-box test. Verified the adv_hittest:
true YAML property generates lv_obj_add_flag(..., LV_OBJ_FLAG_ADV_HITTEST)
via an isolated test compile before applying to both arcs.
The differing radii (400/360) from the previous fix are still useful
for visual separation but were never sufficient on their own — this
flag is what actually makes hit-testing respect the radius difference.
Root cause of the Home screen dial rendering tiny and pinned to the
top-left, confirmed via isolated test compile and direct inspection
of generated C++: an explicit main: sub-block on an LVGL arc widget
silently suppresses the implicit top-level MAIN-part style properties
(align, width, height, x, y) instead of merging with them. All three
arcs (dial_bg_arc, heat_arc, cool_arc) used a main: block for
arc_color/arc_width/arc_opa, which wiped out their align/width/height
in the exact same commit that set them (my earlier size/wrapper fix
never had a chance to take effect).
Verified via a standalone test config: an arc with main: nesting
generates zero lv_obj_set_style_width/height/align calls; the
identical arc with the same properties flattened to top-level
generates them correctly. indicator:/knob: are unaffected since those
are genuinely separate LVGL parts that require explicit nesting.
Two bugs found on first hardware test:
1. page_home placed all widgets directly on the page with no sized
wrapper container, unlike every other page in this codebase (which
all wrap content in an explicit width/height obj before using
align:). The dial and its siblings rendered tiny and pinned to the
top-left instead of filling/centering on the real 1024x600 screen.
Wrapped all of page_home's content in a width:100%/height:100% obj,
matching the established pattern.
2. heat_arc and cool_arc were both adjustable:true at the identical
400x400 size, so their rings occupied the same physical pixels.
LVGL hit-tests arcs by distance from center, so a shared radius is
touch-ambiguous, and the topmost widget (cool_arc, declared last)
won every touch regardless of where on the ring you tapped. Shrunk
cool_arc to 360x360 so the two rings are physically distinguishable
by touch position.
The dual-arc setpoint dial uses a Fahrenheit-range scale (60-90) to
match the Lennox reference UI, but climate.control expects Celsius
throughout this codebase. Dragging the dial was sending raw
Fahrenheit values straight into target_temperature_low/high, which
would have commanded wildly wrong setpoints on live hardware.
Also fixes a stale comment pointing at the wrong file after the
panel.yaml extraction.
Waveshare's board wires log output through the same UART USB-C port
used for flashing. ESPHome's default hardware_uart for ESP32-S3 is
USB_SERIAL_JTAG, which is a separate physical port on this board,
so log output was going nowhere the flashing tool could see it.