From 5f540de6af4fc04ed383814061bb82b99b2b8bde Mon Sep 17 00:00:00 2001 From: ESPHome Device Builder Date: Thu, 16 Jul 2026 21:00:16 +1200 Subject: [PATCH] Edit esp-bedside-panel.yaml --- esphome/esp-bedside-panel.yaml | 419 ++++++++++++++++++--------------- 1 file changed, 227 insertions(+), 192 deletions(-) diff --git a/esphome/esp-bedside-panel.yaml b/esphome/esp-bedside-panel.yaml index 7041c33..69f0943 100644 --- a/esphome/esp-bedside-panel.yaml +++ b/esphome/esp-bedside-panel.yaml @@ -84,12 +84,12 @@ substitutions: # Device Naming device_name: "esp-bedside-panel" friendly_name: "ESP Bedside Panel" - description_comment: "Guition JC1060P470C_I_W_Y ESP32-P4 bedside control panel. Direct Quiet / Boost / Normal selection, Nano-X and aligned Climate controls. (Layout V1.1)" + description_comment: "Guition JC1060P470C_I_W_Y ESP32-P4 bedside control panel. Climate layout with guarded LVGL startup and additional memory headroom. (Layout V1.1)" device_area: "Bedroom" # Project Naming project_name: "Guition.JC1060P470C_I_W_Y" - project_version: "v0.8q" + project_version: "v0.8r" # Passwords & Secrets api_key: !secret esp-api_key @@ -205,6 +205,18 @@ esphome: name: "${project_name}" version: "${project_version}" + # Do not update LVGL widgets until page construction has completed. + on_boot: + priority: -100 + then: + - delay: 2s + + - lambda: |- + id(lvgl_ready) = true; + + - script.execute: refresh_api_indicator + - script.execute: refresh_button_states + #:########################################################################################:# # ESP PLATFORM AND FRAMEWORK: # https://esphome.io/components/esp32.html @@ -278,9 +290,7 @@ api: level: INFO format: "API connected: ESP Bedside Panel is online" - - lvgl.widget.update: - id: bedroom_connection_indicator - bg_color: 0x65D483 + - script.execute: refresh_api_indicator # HA may deliver imported entity states before all LVGL widgets are ready. # Run a delayed two-pass refresh so startup colours always match HA. @@ -295,18 +305,7 @@ api: # Allow another API client to remain connected before showing offline. - delay: 500ms - - if: - condition: - api.connected: - then: - - lvgl.widget.update: - id: bedroom_connection_indicator - bg_color: 0x65D483 - - else: - - lvgl.widget.update: - id: bedroom_connection_indicator - bg_color: 0xF06A6A + - script.execute: refresh_api_indicator #:########################################################################################:# # OTA: @@ -748,6 +747,12 @@ binary_sensor: # Local optimistic Heat Pump profile state #:########################################################################################:# globals: + # Prevent state callbacks from touching widgets while LVGL creates pages. + - id: lvgl_ready + type: bool + restore_value: false + initial_value: "false" + # 0 = Normal, 1 = Quiet, 2 = Boost. - id: climate_profile_optimistic type: int @@ -759,6 +764,30 @@ globals: # Refresh all state-dependent LVGL button colours #:########################################################################################:# script: + # Update the API status indicator only after LVGL has finished constructing. + - id: refresh_api_indicator + mode: restart + then: + - if: + condition: + lambda: |- + return id(lvgl_ready); + + then: + - if: + condition: + api.connected: + + then: + - lvgl.widget.update: + id: bedroom_connection_indicator + bg_color: 0x65D483 + + else: + - lvgl.widget.update: + id: bedroom_connection_indicator + bg_color: 0xF06A6A + # Immediate refresh using the latest imported Home Assistant states. - id: refresh_button_states mode: restart @@ -863,196 +892,202 @@ script: - id: refresh_climate_controls mode: restart then: - # Heat-pump mode buttons. The requested mode wins while a change is - # pending; when it is "none", the actual climate state is used. - - lvgl.widget.update: - id: climate_off_button - bg_color: !lambda |- - const auto requested = id(ha_climate_requested_mode).state; - const auto current = id(ha_climate_current_mode).state; + - if: + condition: + lambda: |- + return id(lvgl_ready); - const bool requested_none = ( - requested.empty() || - requested == "none" || - requested == "None" || - requested == "unknown" || - requested == "unavailable" - ); + then: + # Heat-pump mode buttons. The requested mode wins while a change is + # pending; when it is "none", the actual climate state is used. + - lvgl.widget.update: + id: climate_off_button + bg_color: !lambda |- + const auto requested = id(ha_climate_requested_mode).state; + const auto current = id(ha_climate_current_mode).state; - const bool active = ( - requested == "off" || - requested == "Off" || - (requested_none && current == "off") - ); + const bool requested_none = ( + requested.empty() || + requested == "none" || + requested == "None" || + requested == "unknown" || + requested == "unavailable" + ); - return active - ? lv_color_hex(0x548C6C) - : lv_color_hex(0x28313D); + const bool active = ( + requested == "off" || + requested == "Off" || + (requested_none && current == "off") + ); - - lvgl.widget.update: - id: climate_heat_button - bg_color: !lambda |- - const auto requested = id(ha_climate_requested_mode).state; - const auto current = id(ha_climate_current_mode).state; + return active + ? lv_color_hex(0x548C6C) + : lv_color_hex(0x28313D); - const bool requested_none = ( - requested.empty() || - requested == "none" || - requested == "None" || - requested == "unknown" || - requested == "unavailable" - ); + - lvgl.widget.update: + id: climate_heat_button + bg_color: !lambda |- + const auto requested = id(ha_climate_requested_mode).state; + const auto current = id(ha_climate_current_mode).state; - const bool active = ( - requested == "heat" || - requested == "Heat" || - (requested_none && current == "heat") - ); + const bool requested_none = ( + requested.empty() || + requested == "none" || + requested == "None" || + requested == "unknown" || + requested == "unavailable" + ); - return active - ? lv_color_hex(0xB76822) - : lv_color_hex(0x28313D); + const bool active = ( + requested == "heat" || + requested == "Heat" || + (requested_none && current == "heat") + ); - - lvgl.widget.update: - id: climate_cool_button - bg_color: !lambda |- - const auto requested = id(ha_climate_requested_mode).state; - const auto current = id(ha_climate_current_mode).state; + return active + ? lv_color_hex(0xB76822) + : lv_color_hex(0x28313D); - const bool requested_none = ( - requested.empty() || - requested == "none" || - requested == "None" || - requested == "unknown" || - requested == "unavailable" - ); + - lvgl.widget.update: + id: climate_cool_button + bg_color: !lambda |- + const auto requested = id(ha_climate_requested_mode).state; + const auto current = id(ha_climate_current_mode).state; - const bool active = ( - requested == "cool" || - requested == "Cool" || - (requested_none && current == "cool") - ); + const bool requested_none = ( + requested.empty() || + requested == "none" || + requested == "None" || + requested == "unknown" || + requested == "unavailable" + ); - return active - ? lv_color_hex(0x2E6A96) - : lv_color_hex(0x28313D); + const bool active = ( + requested == "cool" || + requested == "Cool" || + (requested_none && current == "cool") + ); - - lvgl.widget.update: - id: climate_dry_button - bg_color: !lambda |- - const auto requested = id(ha_climate_requested_mode).state; - const auto current = id(ha_climate_current_mode).state; + return active + ? lv_color_hex(0x2E6A96) + : lv_color_hex(0x28313D); - const bool requested_none = ( - requested.empty() || - requested == "none" || - requested == "None" || - requested == "unknown" || - requested == "unavailable" - ); + - lvgl.widget.update: + id: climate_dry_button + bg_color: !lambda |- + const auto requested = id(ha_climate_requested_mode).state; + const auto current = id(ha_climate_current_mode).state; - const bool active = ( - requested == "dry" || - requested == "Dry" || - requested == "dehumidify" || - requested == "Dehumidify" || - ( - requested_none && - ( - current == "dry" || - current == "dehumidify" + const bool requested_none = ( + requested.empty() || + requested == "none" || + requested == "None" || + requested == "unknown" || + requested == "unavailable" + ); + + const bool active = ( + requested == "dry" || + requested == "Dry" || + requested == "dehumidify" || + requested == "Dehumidify" || + ( + requested_none && + ( + current == "dry" || + current == "dehumidify" + ) + ) + ); + + return active + ? lv_color_hex(0x32677F) + : lv_color_hex(0x28313D); + + # Quiet / Boost are mutually exclusive. Normal means neither is lit. + - lvgl.widget.update: + id: climate_quiet_button + bg_color: !lambda |- + return id(climate_profile_optimistic) == 1 + ? lv_color_hex(0x315843) + : lv_color_hex(0x28313D); + + - lvgl.widget.update: + id: climate_boost_button + bg_color: !lambda |- + return id(climate_profile_optimistic) == 2 + ? lv_color_hex(0x6B4A86) + : lv_color_hex(0x28313D); + + - lvgl.widget.update: + id: climate_nanoe_button + bg_color: !lambda |- + return id(ha_nanoe).state + ? lv_color_hex(0x315E82) + : lv_color_hex(0x28313D); + + # Heat-pump target setpoint. + - lvgl.label.update: + id: climate_setpoint_value_label + text: !lambda |- + const float value = id(ha_climate_setpoint).state; + + if (isnan(value)) { + return std::string("--.-°C"); + } + + char buffer[16]; + snprintf(buffer, sizeof(buffer), "%.1f°C", value); + return std::string(buffer); + + # One-button ceiling-fan cycle: Off -> 1 -> 2 -> 3 -> Off. + - lvgl.label.update: + id: ceiling_fan_cycle_label + text: !lambda |- + if (!id(ha_ceiling_fan_on).state) { + return std::string("Ceiling Fan: Off"); + } + + const float percentage = id(ha_ceiling_fan_percentage).state; + + if (isnan(percentage) || percentage < 50.0f) { + return std::string("Ceiling Fan: 1"); + } + + if (percentage < 85.0f) { + return std::string("Ceiling Fan: 2"); + } + + return std::string("Ceiling Fan: 3"); + + - lvgl.widget.update: + id: ceiling_fan_cycle_button + bg_color: !lambda |- + return id(ha_ceiling_fan_on).state + ? lv_color_hex(0x315E82) + : lv_color_hex(0x28313D); + + # Air Filter Quiet is highlighted for filter levels 0, 1 or 2. + - lvgl.widget.update: + id: air_filter_quiet_button + bg_color: !lambda |- + const float level = id(ha_air_filter_level).state; + + return ( + !isnan(level) && + level >= 0.0f && + level <= 2.0f ) - ) - ); + ? lv_color_hex(0x315843) + : lv_color_hex(0x28313D); - return active - ? lv_color_hex(0x32677F) - : lv_color_hex(0x28313D); - - # Quiet / Boost are mutually exclusive. Normal means neither is lit. - - lvgl.widget.update: - id: climate_quiet_button - bg_color: !lambda |- - return id(climate_profile_optimistic) == 1 - ? lv_color_hex(0x315843) - : lv_color_hex(0x28313D); - - - lvgl.widget.update: - id: climate_boost_button - bg_color: !lambda |- - return id(climate_profile_optimistic) == 2 - ? lv_color_hex(0x6B4A86) - : lv_color_hex(0x28313D); - - - lvgl.widget.update: - id: climate_nanoe_button - bg_color: !lambda |- - return id(ha_nanoe).state - ? lv_color_hex(0x315E82) - : lv_color_hex(0x28313D); - - # Heat-pump target setpoint. - - lvgl.label.update: - id: climate_setpoint_value_label - text: !lambda |- - const float value = id(ha_climate_setpoint).state; - - if (isnan(value)) { - return std::string("--.-°C"); - } - - char buffer[16]; - snprintf(buffer, sizeof(buffer), "%.1f°C", value); - return std::string(buffer); - - # One-button ceiling-fan cycle: Off -> 1 -> 2 -> 3 -> Off. - - lvgl.label.update: - id: ceiling_fan_cycle_label - text: !lambda |- - if (!id(ha_ceiling_fan_on).state) { - return std::string("Ceiling Fan: Off"); - } - - const float percentage = id(ha_ceiling_fan_percentage).state; - - if (isnan(percentage) || percentage < 50.0f) { - return std::string("Ceiling Fan: 1"); - } - - if (percentage < 85.0f) { - return std::string("Ceiling Fan: 2"); - } - - return std::string("Ceiling Fan: 3"); - - - lvgl.widget.update: - id: ceiling_fan_cycle_button - bg_color: !lambda |- - return id(ha_ceiling_fan_on).state - ? lv_color_hex(0x315E82) - : lv_color_hex(0x28313D); - - # Air Filter Quiet is highlighted for filter levels 0, 1 or 2. - - lvgl.widget.update: - id: air_filter_quiet_button - bg_color: !lambda |- - const float level = id(ha_air_filter_level).state; - - return ( - !isnan(level) && - level >= 0.0f && - level <= 2.0f - ) - ? lv_color_hex(0x315843) - : lv_color_hex(0x28313D); - - # Climate 9pm-12 follows the Night Power helper state. - - lvgl.widget.update: - id: climate_night_power_button - bg_color: !lambda |- - return id(ha_climate_night_power).state - ? lv_color_hex(0x5A4778) - : lv_color_hex(0x28313D); + # Climate 9pm-12 follows the Night Power helper state. + - lvgl.widget.update: + id: climate_night_power_button + bg_color: !lambda |- + return id(ha_climate_night_power).state + ? lv_color_hex(0x5A4778) + : lv_color_hex(0x28313D); # Two passes allow time for LVGL creation and Home Assistant state delivery. - id: startup_button_state_sync @@ -1184,7 +1219,7 @@ lvgl: long_press_repeat_time: 500ms rotation: 0 - buffer_size: 8% + buffer_size: 6% log_level: WARN default_font: font_button page_wrap: false