From 034c87317fb3439f8e381aa3251a476bef2c60c5 Mon Sep 17 00:00:00 2001 From: ESPHome Device Builder Date: Fri, 17 Jul 2026 22:17:32 +1200 Subject: [PATCH] Edit esp-bedside-panel.yaml --- esphome/esp-bedside-panel.yaml | 593 +++++++++++++++++++++++++-------- 1 file changed, 462 insertions(+), 131 deletions(-) diff --git a/esphome/esp-bedside-panel.yaml b/esphome/esp-bedside-panel.yaml index 0f4bfa4..fdf23de 100644 --- a/esphome/esp-bedside-panel.yaml +++ b/esphome/esp-bedside-panel.yaml @@ -6,6 +6,7 @@ # https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-bedside-panel.yaml #:########################################################################################:# # VERSIONS: +# V1.9 2026-07-17 Corrected Atto lock/climate/setpoint controls and added vehicle icons # V1.8 2026-07-17 Added hold-to-open Atto Vehicles page and vehicle controls # V1.7 2026-07-17 Added Bedroom button icons and corrected Panasonic Nanoe-X naming # V1.6 2026-07-17 Reordered the nine Bedroom control buttons @@ -106,12 +107,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. Bedroom, Vehicles, Climate, Security and Clock pages. (Layout V1.1)" + description_comment: "Guition JC1060P470C_I_W_Y ESP32-P4 bedside control panel. Vehicles page with corrected BYD lock, climate and setpoint controls. (Layout V1.1)" device_area: "Bedroom" # Project Naming project_name: "Guition.JC1060P470C_I_W_Y" - project_version: "v1.8" + project_version: "v1.9" # Passwords & Secrets api_key: !secret esp-api_key @@ -185,9 +186,7 @@ substitutions: nanoe_entity: "switch.master_bedroom_nanoe" # Home Assistant Entities - Vehicles / Atto 3 - atto_locked_sensor_entity: "binary_sensor.atto_3_locked" - # BYD Vehicle integration lock entity. Adjust only if HA generated a - # different entity_id for the Atto 3 lock control. + # BYD Vehicle integration lock entity; used for both status and control. atto_lock_entity: "lock.atto_3_lock" atto_driver_seat_heating_entity: "select.atto_3_driver_seat_heating" atto_passenger_seat_heating_entity: "select.atto_3_passenger_seat_heating" @@ -529,6 +528,23 @@ font: - "\U000F06D9" # mdi-garage - "\U000F1054" # mdi-outdoor-lamp + # Compact Vehicles-page icon font. + - file: "https://raw.githubusercontent.com/Templarian/MaterialDesign-Webfont/master/fonts/materialdesignicons-webfont.ttf" + id: font_mdi_vehicle + size: 27 + bpp: 4 + glyphs: + - "\U000F033E" # mdi-lock + - "\U000F0FC6" # mdi-lock-open-variant + - "\U000F0084" # mdi-battery-charging + - "\U000F00F0" # mdi-calendar-clock + - "\U000F08F0" # mdi-map-marker-distance + - "\U000F0079" # mdi-battery + - "\U000F050F" # mdi-thermometer + - "\U000F0FA6" # mdi-car-seat-heater + - "\U000F0238" # mdi-fire + - "\U000F0717" # mdi-snowflake + #:########################################################################################:# # HOME ASSISTANT NUMERIC SENSORS: # https://esphome.io/components/sensor/homeassistant.html @@ -679,6 +695,12 @@ sensor: internal: true on_value: then: + - lambda: |- + if (!isnan(x)) { + id(atto_setpoint_local) = x; + id(atto_setpoint_optimistic) = false; + } + - script.execute: refresh_vehicle_controls #:########################################################################################:# @@ -689,14 +711,6 @@ binary_sensor: #:######################################################################################:# # ATTO 3 BINARY STATES #:######################################################################################:# - - platform: homeassistant - id: ha_atto_locked - entity_id: "${atto_locked_sensor_entity}" - internal: true - on_state: - then: - - script.execute: refresh_vehicle_controls - - platform: homeassistant id: ha_atto_charging entity_id: "${atto_charging_entity}" @@ -921,6 +935,29 @@ globals: restore_value: false initial_value: "-1" + # Atto lock optimistic state: -1 follows HA, 0 unlocked, 1 locked. + - id: atto_lock_optimistic + type: int + restore_value: false + initial_value: "-1" + + # Atto climate direction: -1 follows HA preset, 1 max heat, 2 max cool. + - id: atto_climate_optimistic + type: int + restore_value: false + initial_value: "-1" + + # Local target is used so repeated presses do not work from stale cloud data. + - id: atto_setpoint_local + type: float + restore_value: false + initial_value: "21.0" + + - id: atto_setpoint_optimistic + type: bool + restore_value: false + initial_value: "false" + # Unix timestamp at which the 2-hour Sleep timer expires. # A value of 0 means that the timer is inactive. - id: sleep_timer_end_epoch @@ -1082,20 +1119,63 @@ script: return id(lvgl_ready); then: + # Lock state and optimistic lock/unlock feedback. - lvgl.label.update: id: atto_lock_button_label text: !lambda |- - return id(ha_atto_locked).state - ? std::string("Locked (Hold)") - : std::string("Unlocked (Hold)"); + if (id(atto_lock_optimistic) == 1) { + return std::string("Locked (Hold)"); + } + + if (id(atto_lock_optimistic) == 0) { + return std::string("Unlocked (Hold)"); + } + + const auto state = id(ha_atto_lock_state).state; + + if (state == "locked") { + return std::string("Locked (Hold)"); + } + + if (state == "locking") { + return std::string("Locking..."); + } + + if (state == "unlocking") { + return std::string("Unlocking..."); + } + + return std::string("Unlocked (Hold)"); + + - lvgl.label.update: + id: atto_lock_icon_label + text: !lambda |- + bool locked = id(atto_lock_optimistic) == 1; + + if (id(atto_lock_optimistic) < 0) { + const auto state = id(ha_atto_lock_state).state; + locked = state == "locked" || state == "locking"; + } + + return locked + ? std::string("\U000F033E") + : std::string("\U000F0FC6"); - lvgl.widget.update: id: atto_lock_button bg_color: !lambda |- - return id(ha_atto_locked).state + bool locked = id(atto_lock_optimistic) == 1; + + if (id(atto_lock_optimistic) < 0) { + const auto state = id(ha_atto_lock_state).state; + locked = state == "locked" || state == "locking"; + } + + return locked ? lv_color_hex(0x315843) : lv_color_hex(0x6B3A3A); + # Charging and schedule status. - lvgl.label.update: id: atto_charging_label text: !lambda |- @@ -1117,6 +1197,7 @@ script: ? lv_color_hex(0x315E82) : lv_color_hex(0x28313D); + # Vehicle values. - lvgl.label.update: id: atto_range_label text: !lambda |- @@ -1156,49 +1237,21 @@ script: snprintf(buffer, sizeof(buffer), "Cabin Temp\n%.1f°C", value); return std::string(buffer); - - lvgl.label.update: - id: atto_driver_seat_button_label - text: !lambda |- - const auto value = id(ha_atto_driver_seat_heating).state; - - if (value == "low" || value == "Low") { - return std::string("Drive Seat Heat (Hold)\nLow"); - } - - if (value == "high" || value == "High") { - return std::string("Drive Seat Heat (Hold)\nHigh"); - } - - return std::string("Drive Seat Heat (Hold)\nOff"); - - - lvgl.widget.update: - id: atto_driver_seat_button - bg_color: !lambda |- - const auto value = id(ha_atto_driver_seat_heating).state; - - return ( - value == "low" || - value == "Low" || - value == "high" || - value == "High" - ) - ? lv_color_hex(0x7A4A22) - : lv_color_hex(0x28313D); - + # Seat-heating states. - lvgl.label.update: id: atto_passenger_seat_button_label text: !lambda |- const auto value = id(ha_atto_passenger_seat_heating).state; if (value == "low" || value == "Low") { - return std::string("Passenger Seat Heat (Hold)\nLow"); + return std::string("Passenger Seat (Hold)\nLow"); } if (value == "high" || value == "High") { - return std::string("Passenger Seat Heat (Hold)\nHigh"); + return std::string("Passenger Seat (Hold)\nHigh"); } - return std::string("Passenger Seat Heat (Hold)\nOff"); + return std::string("Passenger Seat (Hold)\nOff"); - lvgl.widget.update: id: atto_passenger_seat_button @@ -1214,33 +1267,124 @@ script: ? lv_color_hex(0x7A4A22) : lv_color_hex(0x28313D); + - lvgl.label.update: + id: atto_driver_seat_button_label + text: !lambda |- + const auto value = id(ha_atto_driver_seat_heating).state; + + if (value == "low" || value == "Low") { + return std::string("Driver Seat (Hold)\nLow"); + } + + if (value == "high" || value == "High") { + return std::string("Driver Seat (Hold)\nHigh"); + } + + return std::string("Driver Seat (Hold)\nOff"); + + - lvgl.widget.update: + id: atto_driver_seat_button + bg_color: !lambda |- + const auto value = id(ha_atto_driver_seat_heating).state; + + return ( + value == "low" || + value == "Low" || + value == "high" || + value == "High" + ) + ? lv_color_hex(0x7A4A22) + : lv_color_hex(0x28313D); + + # BYD heating/cooling intent is represented by max presets. - lvgl.widget.update: id: atto_heat_button bg_color: !lambda |- - return id(ha_atto_climate_mode).state == "heat" + int selected = id(atto_climate_optimistic); + + if (selected < 0) { + selected = + id(ha_atto_climate_preset).state == "max_heat" + ? 1 + : ( + id(ha_atto_climate_preset).state == "max_cool" + ? 2 + : 0 + ); + } + + return selected == 1 ? lv_color_hex(0xB76822) : lv_color_hex(0x28313D); - lvgl.widget.update: id: atto_cool_button bg_color: !lambda |- - return id(ha_atto_climate_mode).state == "cool" + int selected = id(atto_climate_optimistic); + + if (selected < 0) { + selected = + id(ha_atto_climate_preset).state == "max_heat" + ? 1 + : ( + id(ha_atto_climate_preset).state == "max_cool" + ? 2 + : 0 + ); + } + + return selected == 2 ? lv_color_hex(0x2E6A96) : lv_color_hex(0x28313D); + # Use the local value while a command is pending, then HA again. - lvgl.label.update: id: atto_setpoint_value_label text: !lambda |- - const float value = id(ha_atto_setpoint).state; + float value = id(atto_setpoint_local); + + if (!id(atto_setpoint_optimistic)) { + value = id(ha_atto_setpoint).state; + } if (isnan(value)) { return std::string("--.-°C"); } char buffer[16]; - snprintf(buffer, sizeof(buffer), "%.1f°C", value); + snprintf(buffer, sizeof(buffer), "%.0f°C", value); return std::string(buffer); + - id: release_atto_lock_optimistic + mode: restart + then: + - delay: 8s + - lambda: |- + id(atto_lock_optimistic) = -1; + - script.execute: refresh_vehicle_controls + + - id: release_atto_climate_optimistic + mode: restart + then: + - delay: 10s + - lambda: |- + id(atto_climate_optimistic) = -1; + - script.execute: refresh_vehicle_controls + + - id: release_atto_setpoint_optimistic + mode: restart + then: + - delay: 12s + - lambda: |- + id(atto_setpoint_optimistic) = false; + + const float actual = id(ha_atto_setpoint).state; + if (!isnan(actual)) { + id(atto_setpoint_local) = actual; + } + + - script.execute: refresh_vehicle_controls + # Update Climate-page labels and highlight the currently selected settings. - id: refresh_climate_controls mode: restart @@ -1788,6 +1932,18 @@ text_sensor: #:######################################################################################:# # ATTO 3 TEXT STATES #:######################################################################################:# + # The lock entity state is locked/unlocked/locking/unlocking. + - platform: homeassistant + id: ha_atto_lock_state + entity_id: "${atto_lock_entity}" + internal: true + on_value: + then: + - lambda: |- + id(atto_lock_optimistic) = -1; + + - script.execute: refresh_vehicle_controls + - platform: homeassistant id: ha_atto_driver_seat_heating entity_id: "${atto_driver_seat_heating_entity}" @@ -1812,6 +1968,19 @@ text_sensor: then: - script.execute: refresh_vehicle_controls + # BYD exposes heating/cooling intent as max_heat / max_cool presets. + - platform: homeassistant + id: ha_atto_climate_preset + entity_id: "${atto_climate_entity}" + attribute: preset_mode + internal: true + on_value: + then: + - lambda: |- + id(atto_climate_optimistic) = -1; + + - script.execute: refresh_vehicle_controls + # Requested heat-pump mode helper used while a mode change is pending. - platform: homeassistant id: ha_climate_requested_mode @@ -2615,32 +2784,58 @@ lvgl: pressed: bg_color: 0x416F55 widgets: + - label: + id: atto_lock_icon_label + align: TOP_LEFT + x: 10 + y: 8 + text: "\U000F033E" + text_font: font_mdi_vehicle + text_color: 0xFFFFFF + - label: id: atto_lock_button_label align: CENTER + x: 10 text: "Locked (Hold)" text_font: font_small text_align: CENTER text_color: 0xFFFFFF on_long_press: then: + - lambda: |- + bool locked = false; + + if (id(atto_lock_optimistic) >= 0) { + locked = id(atto_lock_optimistic) == 1; + } else { + const auto state = id(ha_atto_lock_state).state; + locked = state == "locked" || state == "locking"; + } + + id(atto_lock_optimistic) = locked ? 0 : 1; + + - script.execute: refresh_vehicle_controls + - if: condition: lambda: |- - return id(ha_atto_locked).state; + return id(atto_lock_optimistic) == 1; then: - homeassistant.action: - action: lock.unlock + action: lock.lock data: entity_id: "${atto_lock_entity}" else: - homeassistant.action: - action: lock.lock + action: lock.unlock data: entity_id: "${atto_lock_entity}" + - script.execute: release_atto_lock_optimistic + - button: id: atto_charging_status x: 272 @@ -2653,9 +2848,18 @@ lvgl: border_color: 0x548C6C radius: 17 widgets: + - label: + align: TOP_LEFT + x: 10 + y: 8 + text: "\U000F0084" + text_font: font_mdi_vehicle + text_color: 0xFFFFFF + - label: id: atto_charging_label align: CENTER + x: 10 text: "Atto Charging\nNot Charging" text_font: font_small text_align: CENTER @@ -2675,8 +2879,17 @@ lvgl: pressed: bg_color: 0x315E82 widgets: + - label: + align: TOP_LEFT + x: 10 + y: 8 + text: "\U000F00F0" + text_font: font_mdi_vehicle + text_color: 0xFFFFFF + - label: align: CENTER + x: 10 text: "Schedule Enabled" text_font: font_small text_align: CENTER @@ -2699,9 +2912,18 @@ lvgl: border_color: 0x526276 radius: 17 widgets: + - label: + align: TOP_LEFT + x: 10 + y: 8 + text: "\U000F08F0" + text_font: font_mdi_vehicle + text_color: 0xFFFFFF + - label: id: atto_range_label align: CENTER + x: 10 text: "Atto Range\n-- km" text_font: font_small text_align: CENTER @@ -2722,9 +2944,18 @@ lvgl: border_color: 0x526276 radius: 17 widgets: + - label: + align: TOP_LEFT + x: 10 + y: 8 + text: "\U000F0079" + text_font: font_mdi_vehicle + text_color: 0xFFFFFF + - label: id: atto_battery_label align: CENTER + x: 10 text: "Battery Level\n--%" text_font: font_small text_align: CENTER @@ -2742,16 +2973,26 @@ lvgl: border_color: 0x526276 radius: 17 widgets: + - label: + align: TOP_LEFT + x: 10 + y: 8 + text: "\U000F050F" + text_font: font_mdi_vehicle + text_color: 0xFFFFFF + - label: id: atto_cabin_temperature_label align: CENTER + x: 10 text: "Cabin Temp\n--.-°C" text_font: font_small text_align: CENTER text_color: 0xFFFFFF + # Passenger seat is intentionally before Driver seat. - button: - id: atto_driver_seat_button + id: atto_passenger_seat_button x: 519 y: 130 width: 220 @@ -2765,76 +3006,18 @@ lvgl: bg_color: 0x7A4A22 widgets: - label: - id: atto_driver_seat_button_label - align: CENTER - text: "Drive Seat Heat (Hold)\nOff" - text_font: font_small - text_align: CENTER + align: TOP_LEFT + x: 10 + y: 8 + text: "\U000F0FA6" + text_font: font_mdi_vehicle text_color: 0xFFFFFF - on_long_press: - then: - - if: - condition: - lambda: |- - const auto value = - id(ha_atto_driver_seat_heating).state; - return ( - value == "off" || - value == "Off" || - value == "unknown" || - value == "unavailable" || - value.empty() - ); - - then: - - homeassistant.action: - action: select.select_option - data: - entity_id: "${atto_driver_seat_heating_entity}" - option: "low" - - else: - - if: - condition: - lambda: |- - const auto value = - id(ha_atto_driver_seat_heating).state; - - return value == "low" || value == "Low"; - - then: - - homeassistant.action: - action: select.select_option - data: - entity_id: "${atto_driver_seat_heating_entity}" - option: "high" - - else: - - homeassistant.action: - action: select.select_option - data: - entity_id: "${atto_driver_seat_heating_entity}" - option: "off" - - - button: - id: atto_passenger_seat_button - x: 766 - y: 130 - width: 220 - height: 92 - bg_color: 0x28313D - bg_opa: COVER - border_width: 2 - border_color: 0xA6672B - radius: 17 - pressed: - bg_color: 0x7A4A22 - widgets: - label: id: atto_passenger_seat_button_label align: CENTER - text: "Passenger Seat Heat (Hold)\nOff" + x: 10 + text: "Passenger Seat (Hold)\nOff" text_font: font_small text_align: CENTER text_color: 0xFFFFFF @@ -2884,6 +3067,82 @@ lvgl: entity_id: "${atto_passenger_seat_heating_entity}" option: "off" + - button: + id: atto_driver_seat_button + x: 766 + y: 130 + width: 220 + height: 92 + bg_color: 0x28313D + bg_opa: COVER + border_width: 2 + border_color: 0xA6672B + radius: 17 + pressed: + bg_color: 0x7A4A22 + widgets: + - label: + align: TOP_LEFT + x: 10 + y: 8 + text: "\U000F0FA6" + text_font: font_mdi_vehicle + text_color: 0xFFFFFF + + - label: + id: atto_driver_seat_button_label + align: CENTER + x: 10 + text: "Driver Seat (Hold)\nOff" + text_font: font_small + text_align: CENTER + text_color: 0xFFFFFF + on_long_press: + then: + - if: + condition: + lambda: |- + const auto value = + id(ha_atto_driver_seat_heating).state; + + return ( + value == "off" || + value == "Off" || + value == "unknown" || + value == "unavailable" || + value.empty() + ); + + then: + - homeassistant.action: + action: select.select_option + data: + entity_id: "${atto_driver_seat_heating_entity}" + option: "low" + + else: + - if: + condition: + lambda: |- + const auto value = + id(ha_atto_driver_seat_heating).state; + + return value == "low" || value == "Low"; + + then: + - homeassistant.action: + action: select.select_option + data: + entity_id: "${atto_driver_seat_heating_entity}" + option: "high" + + else: + - homeassistant.action: + action: select.select_option + data: + entity_id: "${atto_driver_seat_heating_entity}" + option: "off" + #:################################################################################:# # ATTO CLIMATE / SETPOINT #:################################################################################:# @@ -2910,18 +3169,33 @@ lvgl: pressed: bg_color: 0xB76822 widgets: + - label: + align: LEFT_MID + x: 18 + text: "\U000F0238" + text_font: font_mdi_vehicle + text_color: 0xFFFFFF + - label: align: CENTER + x: 15 text: "Heat (Hold)" text_font: font_button text_color: 0xFFFFFF on_long_press: then: + - lambda: |- + id(atto_climate_optimistic) = 1; + + - script.execute: refresh_vehicle_controls + - homeassistant.action: - action: climate.set_hvac_mode + action: climate.set_preset_mode data: entity_id: "${atto_climate_entity}" - hvac_mode: "heat" + preset_mode: "max_heat" + + - script.execute: release_atto_climate_optimistic - button: id: atto_cool_button @@ -2937,18 +3211,33 @@ lvgl: pressed: bg_color: 0x2E6A96 widgets: + - label: + align: LEFT_MID + x: 18 + text: "\U000F0717" + text_font: font_mdi_vehicle + text_color: 0xFFFFFF + - label: align: CENTER + x: 15 text: "Cool (Hold)" text_font: font_button text_color: 0xFFFFFF on_long_press: then: + - lambda: |- + id(atto_climate_optimistic) = 2; + + - script.execute: refresh_vehicle_controls + - homeassistant.action: - action: climate.set_hvac_mode + action: climate.set_preset_mode data: entity_id: "${atto_climate_entity}" - hvac_mode: "cool" + preset_mode: "max_cool" + + - script.execute: release_atto_climate_optimistic - button: id: atto_setpoint_down_button @@ -2970,6 +3259,22 @@ lvgl: text_font: font_mdi_setpoint text_color: 0xFFFFFF on_click: + - lambda: |- + float current = id(atto_setpoint_local); + + if (!id(atto_setpoint_optimistic)) { + const float actual = id(ha_atto_setpoint).state; + if (!isnan(actual)) { + current = actual; + } + } + + id(atto_setpoint_local) = + std::max(15.0f, current - 1.0f); + id(atto_setpoint_optimistic) = true; + + - script.execute: refresh_vehicle_controls + - homeassistant.action: action: climate.set_temperature data: @@ -2978,8 +3283,9 @@ lvgl: temperature: "{{ target_temperature }}" variables: target_temperature: |- - const float current = id(ha_atto_setpoint).state; - return isnan(current) ? 22.0f : current - 0.5f; + return id(atto_setpoint_local); + + - script.execute: release_atto_setpoint_optimistic - button: id: atto_setpoint_display @@ -2993,10 +3299,18 @@ lvgl: border_color: 0x5A6C82 radius: 18 widgets: + - label: + align: LEFT_MID + x: 18 + text: "\U000F050F" + text_font: font_mdi_vehicle + text_color: 0xFFFFFF + - label: id: atto_setpoint_value_label align: CENTER - text: "--.-°C" + x: 15 + text: "--°C" text_font: font_title text_color: 0xFFFFFF @@ -3020,6 +3334,22 @@ lvgl: text_font: font_mdi_setpoint text_color: 0xFFFFFF on_click: + - lambda: |- + float current = id(atto_setpoint_local); + + if (!id(atto_setpoint_optimistic)) { + const float actual = id(ha_atto_setpoint).state; + if (!isnan(actual)) { + current = actual; + } + } + + id(atto_setpoint_local) = + std::min(31.0f, current + 1.0f); + id(atto_setpoint_optimistic) = true; + + - script.execute: refresh_vehicle_controls + - homeassistant.action: action: climate.set_temperature data: @@ -3028,8 +3358,9 @@ lvgl: temperature: "{{ target_temperature }}" variables: target_temperature: |- - const float current = id(ha_atto_setpoint).state; - return isnan(current) ? 22.0f : current + 0.5f; + return id(atto_setpoint_local); + + - script.execute: release_atto_setpoint_optimistic #:################################################################################:# # BOTTOM NAVIGATION