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.
528 lines
20 KiB
YAML
528 lines
20 KiB
YAML
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 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: "-- °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
|