#:########################################################################################:# # TITLE: ESP BEDSIDE PANEL # zorruno.com layout v1.1 2026 #:########################################################################################:# # REPO: # https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-bedside-panel.yaml #:########################################################################################:# # VERSIONS: # V1.15 2026-07-17 Changed Garage Lights to an imported controllable HA switch # V1.14 2026-07-17 Changed Garage Lights to a direct click-triggered HA toggle # V1.13 2026-07-17 Corrected Garage Lights switching and removed Atto value containers # V1.12 2026-07-17 Re-centred Atto tiles, swapped status rows and simplified value tiles # V1.11 2026-07-17 Refined Atto button centring and rearranged vehicle status tiles # V1.10a 2026-07-17 Compile fix: use C++ comments inside lambda blocks # V1.10 2026-07-17 Refined Atto status layout and updated Garage Lights entity # 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 # V1.5 2026-07-16 Added RX8130 battery-backed RTC time restore and synchronisation # V1.4 2026-07-16 Moved Security image right, added Driveway and four Future buttons # V1.3 2026-07-16 Added Security page with 640x360 Doorbell camera snapshot # V1.2 2026-07-16 Removed Climate title and aligned Heat Pump heading above Off # V1.1 2026-07-16 Increased Clock to 280px and date to 64px # V1.0 2026-07-16 First full Bedroom/Climate/Clock release; added 2-hour Sleep timer # V0.8u 2026-07-16 Enlarged Clock and date; moved environment readings lower # V0.8t 2026-07-16 Corrected Panasonic presets to quiet / powerful / none # V0.8s 2026-07-16 Added direct Panasonic preset control and optimistic Nanoe-X # V0.8r 2026-07-16 Added LVGL startup guards and reduced draw buffer to 6% # V0.8q 2026-07-16 Added direct profile helper selection, Nanoe-X and aligned Climate layout # V0.8p 2026-07-16 Renamed the Heat Pump high-output profile from Powerful to Boost # V0.8o 2026-07-16 Added reliable MDI setpoint arrows and optimistic profile controls # V0.8n 2026-07-16 Fixed setpoint arrow icons using LVGL built-in symbols # V0.8m 2026-07-16 Added Quiet / Boost profiles, Night Power toggle and improved highlighting # V0.8l 2026-07-16 Completed primary Climate controls, ceiling fan cycle and Air Filter Low # V0.8k 2026-07-16 Revised Bedroom header and added five-button navigation row # V0.8j 2026-07-16 Added delayed Home Assistant state synchronisation at startup # V0.8i 2026-07-16 Moved Clock date upward, added glyphs and increased Clock to 190px # V0.8h 2026-07-16 Updated local environment entities and added Moon / Outdoor readings # V0.8g 2026-07-16 Added working Climate mode buttons and retained placeholders # V0.8f 2026-07-15 Added minimal Climate page with two placeholders and Back button # V0.8e 2026-07-15 Centred Bedroom button captions only; no additional pages # V0.8b 2026-07-15 Changed proven V0.8a clock to 24-hour format and removed AM/PM # V0.8a 2026-07-14 Reduced LVGL memory load after V0.8 OTA rollback # Added lean large-clock idle page and Bedroom controls # V0.8 2026-07-14 Added large bedside clock, idle dimming and first Bedroom page # V0.7 2026-07-14 Added first landscape LVGL page and Home Assistant test button # V0.6 2026-07-14 Added GT911 touchscreen and touch-coordinate logging # V0.5 2026-07-14 Added corrected JC1060P470 MIPI display configuration and text test # V0.4 2026-07-14 Removed MIPI display test after setup watchdog boot loop # V0.3 2026-07-14 Added backlight control and first MIPI display text test # V0.2 2026-07-14 Added ESP32 hosted WiFi via ESP32-C6 co-processor # V0.1 2026-07-14 Initial ESPHome bring-up for Guition JC1060P470C_I_W_Y #:########################################################################################:# # HARDWARE: # - Guition JC1060P470C_I_W_Y # - 7 inch 1024x600 capacitive touch display # - ESP32-P4 main processor # - ESP32-C6 wireless co-processor # - 16MB flash # - 32MB PSRAM # - 100 Mbps Ethernet # - Dual USB-C ports # # Confirmed hardware details: # - Display model: JC1060P470 # - Display interface: MIPI-DSI # - Display reset: GPIO5 # - Display backlight PWM: GPIO23 # - Touch controller: GT911 # - Touch I2C SDA: GPIO7 # - Touch I2C SCL: GPIO8 # - Touch interrupt: GPIO21 # - Touch reset: GPIO22 # - Touch I2C address: 0x5D # - Battery-backed RTC: RX8130 # - RTC I2C address: 0x32 #:########################################################################################:# # OPERATION NOTES: # - This build returns to the proven V0.8a structure. # - The only functional clock changes are: # - 24-hour HH:MM display # - AM/PM label and update removed # - The 24-hour clock font is increased to 280px. # - The date uses a dedicated 64px font. # - Moon, Outdoor, Indoor and Humidity readings are positioned near the bottom. # - The display remains in its native 1024x600 landscape orientation. # - The default page contains nine Bedroom / nearby controls. # - The bottom navigation row contains Bedtime, Climate, Future1, Future2 and Clock. # - After the configured idle period: # - The large clock page is displayed. # - Backlight brightness is reduced. # - Touching the clock: # - Returns to the Bedroom controls. # - Restores normal brightness. # - The Climate page contains heat-pump, ceiling-fan, air-filter and Night Power controls. # - Sleep: 2 hrs starts a local 120-minute countdown and turns the heat pump off. # - Pressing the active Sleep button again cancels the countdown. #:########################################################################################:# # OFFLINE NOTES: # - LVGL, the clock, touch and backlight operate locally. # - Home Assistant control buttons require an API connection. # - At boot, system time is restored from the battery-backed RX8130 RTC. # - When Home Assistant time synchronises, the corrected time is written back to the RTC. # - A suitable backup cell must be fitted for the RTC to retain time without panel power. #:########################################################################################:# #:########################################################################################:# # SUBSTITUTIONS: # Specific device variable substitutions # If NOT using a secrets file, just replace these with the passwords etc (in quotes) #:########################################################################################:# 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. Garage Lights controlled through an imported Home Assistant switch. (Layout V1.1)" device_area: "Bedroom" # Project Naming project_name: "Guition.JC1060P470C_I_W_Y" project_version: "v1.15" # Passwords & Secrets api_key: !secret esp-api_key ota_pass: !secret esp-ota_pass # Device Settings log_level: "INFO" update_interval: "60s" local_timezone: "Pacific/Auckland" # Display Settings backlight_pin: "GPIO23" backlight_frequency: "1220Hz" # Touchscreen Settings touch_sda_pin: "GPIO7" touch_scl_pin: "GPIO8" touch_interrupt_pin: "GPIO21" touch_reset_pin: "GPIO22" # Clock / Screen Settings clock_idle_timeout: "60s" control_brightness: "80%" clock_brightness: "4%" # Security Camera Settings # Change this only if homeassistant.local is not reachable from the panel. home_assistant_internal_url: "http://homeassistant.local:8123" security_camera_refresh_interval: "5s" security_doorbell_camera_entity: "camera.view_road_front_door_fluent" security_driveway_camera_entity: "camera.camera5" # Home Assistant Entities - Status / Environment moon_phase_entity: "sensor.astroweather_moon_phase" outdoor_temperature_entity: "sensor.lounge_outside_temperature" bedroom_temperature_entity: "sensor.bedroom_1_environment_z19th_temperature" bedroom_humidity_entity: "sensor.bedroom_1_environment_z19th_humidity" # Home Assistant Entities - Bedroom / Nearby Controls north_lights_entity: "light.tasmo_ifan02_3793_bedrm1_1" south_lights_entity: "switch.master_bedroom_master_bedroom_main_lights_south_relay_2_master_bedroom_south_lights" bedside_lamp_entity: "switch.master_bedroom_bedside_lamp_arlec_bulb_master_bedroom_bedside_lamp_arlec_bulb_jump_fade_up_down" hall_floor_entity: "switch.main_hallway_nightlights_1_relay_1_main_hallway_nightlights" hall_lights_entity: "switch.main_hallway_lightswitch_relay_1_main_hallway_lights" bathroom_lights_entity: "switch.main_bathroom_lightswitch_3_2_all_main_bathroom_lights" patio_lights_entity: "switch.esp_garageentrylights_relay_2_patio_lights" garage_lights_entity: "switch.esp_garagelights_garage_lights" bathroom_fan_entity: "switch.esp_mainbathfancombo_relay_1_extract_fan" # Home Assistant Entities - Status garage_door_entity: "binary_sensor.garage_roller_door_x18rs_contact" quiet_time_entity: "input_boolean.quiet_time" # Home Assistant Entities - Actions bedtime_button_entity: "input_button.bedtime_mode" # Home Assistant Entities - Climate climate_entity: "climate.master_bedroom" climate_requested_mode_entity: "input_select.master_bedroom_hvac_requested_mode" climate_profile_entity: "input_select.master_bedroom_heat_pump_profile" climate_off_script: "script.master_bedroom_hvac_off" climate_heat_script: "script.master_bedroom_hvac_heat" climate_cool_script: "script.master_bedroom_hvac_cool" climate_dry_script: "script.master_bedroom_hvac_dry" climate_setpoint_adjust_script: "script.master_bedroom_setpoint_adjust" # Home Assistant Entities - Ceiling Fan / Air Filter / Night Power ceiling_fan_entity: "fan.tasmo_ifan02_3793_bedrm1_1" air_filter_level_entity: "number.bedroom_1_air_filter_bedroom_1_air_filter_speed_level" climate_night_power_entity: "input_boolean.master_bedroom_night_power_mode" nanoe_entity: "switch.master_bedroom_nanoe" # Home Assistant Entities - Vehicles / Atto 3 # 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" atto_charging_entity: "binary_sensor.atto_3_charging" atto_schedule_enabled_entity: "switch.atto_3_schedule_enabled" atto_range_entity: "sensor.atto_3_range" atto_battery_level_entity: "sensor.atto_3_battery_level" atto_cabin_temperature_entity: "sensor.atto_3_cabin_temperature" atto_climate_entity: "climate.atto_3_climate" #:########################################################################################:# # PACKAGES: # Included Common Packages # https://esphome.io/components/packages.html #:########################################################################################:# packages: #### WIFI, Network DHCP, Fallback AP, Safemode #### common_wifi: !include file: common/network_common_dhcp.yaml vars: local_device_name: "${device_name}" local_ota_pass: "${ota_pass}" #### HOME ASSISTANT API #### common_api: !include file: common/api_common.yaml #file: common/api_common_noencryption.yaml vars: local_api_key: "${api_key}" #### MQTT #### #common_mqtt: !include # file: common/mqtt_common.yaml # vars: # local_device_name: "${device_name}" #### WEB PORTAL #### #common_webportal: !include common/webportal_common_nopass.yaml #### SNTP #### #common_sntp: !include common/sntp_common.yaml #### DIAGNOSTICS Sensors #### #diag_basic: !include common/include_basic_diag_sensors.yaml #diag_more: !include common/include_more_diag_sensors.yaml #diag_debug: !include common/include_debug_diag_sensors.yaml #diag_resetcount: !include common/include_resetcount_diag_sensors.yaml #:########################################################################################:# # ESPHOME: # https://esphome.io/components/esphome.html #:########################################################################################:# esphome: name: "${device_name}" friendly_name: "${friendly_name}" comment: "${description_comment}" area: "${device_area}" project: name: "${project_name}" version: "${project_version}" # Do not update LVGL widgets until page construction has completed. on_boot: priority: -100 then: # Restore system time from the battery-backed hardware RTC. - rx8130.read_time: id: rtc_time - delay: 2s - lambda: |- id(lvgl_ready) = true; - script.execute: refresh_api_indicator - script.execute: refresh_button_states - script.execute: refresh_sleep_timer #:########################################################################################:# # ESP PLATFORM AND FRAMEWORK: # https://esphome.io/components/esp32.html #:########################################################################################:# esp32: variant: ESP32P4 engineering_sample: true cpu_frequency: 360MHz flash_size: 16MB framework: type: esp-idf advanced: enable_idf_experimental_features: true disable_vfs_support_termios: false disable_vfs_support_select: false disable_vfs_support_dir: false sdkconfig_options: CONFIG_SPIRAM_XIP_FROM_PSRAM: "y" CONFIG_SDMMC_HOST_DEFAULT: "y" #:########################################################################################:# # PSRAM: # https://esphome.io/components/psram.html #:########################################################################################:# psram: mode: hex speed: 200MHz #:########################################################################################:# # ESP32 HOSTED WIFI COPROCESSOR: # https://esphome.io/components/esp32_hosted.html #:########################################################################################:# esp32_hosted: variant: ESP32C6 reset_pin: GPIO54 cmd_pin: GPIO19 clk_pin: GPIO18 d0_pin: GPIO14 d1_pin: GPIO15 d2_pin: GPIO16 d3_pin: GPIO17 active_high: true use_psram: true #:########################################################################################:# # ESP LDO: # Supplies the ESP32-P4 MIPI-DSI PHY #:########################################################################################:# esp_ldo: - channel: 3 id: dsi_phy_enable voltage: 2.5V adjustable: true #:########################################################################################:# # LOGGER: # https://esphome.io/components/logger.html #:########################################################################################:# logger: level: "${log_level}" baud_rate: 115200 hardware_uart: USB_SERIAL_JTAG #:########################################################################################:# # HOME ASSISTANT API: #:########################################################################################:# api: on_client_connected: then: - logger.log: level: INFO format: "API connected: ESP Bedside Panel is online" - 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. - script.execute: startup_button_state_sync on_client_disconnected: then: - logger.log: level: WARN format: "API disconnected: ESP Bedside Panel is offline from Home Assistant" # Allow another API client to remain connected before showing offline. - delay: 500ms - script.execute: refresh_api_indicator #:########################################################################################:# # OTA: # https://esphome.io/components/ota/esphome.html #:########################################################################################:# ota: - platform: esphome password: "${ota_pass}" #:########################################################################################:# # TIME: # RX8130 restores time at boot; Home Assistant corrects it when available # https://esphome.io/components/time/rx8130/ # https://esphome.io/components/time/homeassistant.html #:########################################################################################:# time: # Battery-backed onboard hardware RTC. - platform: rx8130 id: rtc_time i2c_id: touch_i2c address: 0x32 timezone: "${local_timezone}" update_interval: never # Home Assistant remains the authoritative network time source. - platform: homeassistant id: ha_time timezone: "${local_timezone}" on_time_sync: then: # Save corrected HA time into the hardware RTC. - rx8130.write_time: id: rtc_time #:########################################################################################:# # I2C: # https://esphome.io/components/i2c.html #:########################################################################################:# i2c: - id: touch_i2c sda: "${touch_sda_pin}" scl: "${touch_scl_pin}" frequency: 400kHz scan: true #:########################################################################################:# # OUTPUT: # https://esphome.io/components/output/ledc.html #:########################################################################################:# output: - platform: ledc pin: "${backlight_pin}" id: backlight_output frequency: "${backlight_frequency}" #:########################################################################################:# # LIGHT: # https://esphome.io/components/light/monochromatic.html #:########################################################################################:# light: - platform: monochromatic output: backlight_output name: "Backlight" id: display_backlight icon: "mdi:brightness-6" restore_mode: RESTORE_DEFAULT_ON default_transition_length: 500ms gamma_correct: 1.0 #:########################################################################################:# # DISPLAY: # https://esphome.io/components/display/mipi_dsi.html #:########################################################################################:# display: - platform: mipi_dsi model: JC1060P470 id: main_display color_order: RGB reset_pin: number: GPIO5 hsync_pulse_width: 20 update_interval: never auto_clear_enabled: false #:########################################################################################:# # TOUCHSCREEN: # https://esphome.io/components/touchscreen/gt911.html #:########################################################################################:# touchscreen: - platform: gt911 id: main_touchscreen display: main_display i2c_id: touch_i2c address: 0x5D interrupt_pin: number: "${touch_interrupt_pin}" mode: input: true reset_pin: number: "${touch_reset_pin}" mode: output: true #:########################################################################################:# # FONTS: # Reduced font set for lower startup memory use # https://esphome.io/components/font.html #:########################################################################################:# font: - file: "gfonts://Roboto" id: font_clock size: 280 glyphs: - "0123456789:" - file: "gfonts://Roboto" id: font_date size: 64 glyphs: - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789, " - file: "gfonts://Roboto" id: font_title size: 36 glyphs: - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789, .:/-%°+()" - file: "gfonts://Roboto" id: font_button size: 24 glyphs: - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789 .:/-%°+()" - file: "gfonts://Roboto" id: font_small size: 20 glyphs: - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789, .:/-%°+()" # Dedicated MDI font for reliable setpoint chevron icons. - file: "https://raw.githubusercontent.com/Templarian/MaterialDesign-Webfont/master/fonts/materialdesignicons-webfont.ttf" id: font_mdi_setpoint size: 48 bpp: 4 glyphs: - "\U000F0140" # mdi-chevron-down - "\U000F0143" # mdi-chevron-up # Small Bedroom-control icon font. Only the required glyphs are compiled. - file: "https://raw.githubusercontent.com/Templarian/MaterialDesign-Webfont/master/fonts/materialdesignicons-webfont.ttf" id: font_mdi_button size: 30 bpp: 4 glyphs: - "\U000F1253" # mdi-lightbulb-group - "\U000F08DD" # mdi-floor-lamp - "\U000F06B5" # mdi-lamp - "\U000F0210" # mdi-fan - "\U000F0769" # mdi-ceiling-light - "\U000F0335" # mdi-lightbulb - "\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 # Bold Atto status/value font used for prominent second-line values. - file: type: gfonts family: Roboto weight: 700 id: font_vehicle_bold size: 27 bpp: 4 glyphs: - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789 .:/-%°()" #:########################################################################################:# # HOME ASSISTANT NUMERIC SENSORS: # https://esphome.io/components/sensor/homeassistant.html #:########################################################################################:# sensor: - platform: uptime name: "${friendly_name} Uptime" id: uptime_sensor update_interval: "${update_interval}" - platform: wifi_signal name: "${friendly_name} WiFi Signal" id: wifi_signal_sensor update_interval: "${update_interval}" - platform: homeassistant id: moon_phase entity_id: "${moon_phase_entity}" internal: true on_value: then: - lvgl.label.update: id: bedroom_moon_value_label text: format: "%.1f%%" args: [x] - lvgl.label.update: id: clock_moon_value_label text: format: "%.1f%%" args: [x] - platform: homeassistant id: outdoor_temperature entity_id: "${outdoor_temperature_entity}" internal: true on_value: then: - lvgl.label.update: id: bedroom_outdoor_value_label text: format: "%.1f°C" args: [x] - lvgl.label.update: id: clock_outdoor_value_label text: format: "%.1f°C" args: [x] - platform: homeassistant id: bedroom_temperature entity_id: "${bedroom_temperature_entity}" internal: true on_value: then: - lvgl.label.update: id: bedroom_temperature_label text: format: "%.1f°C" args: [x] - lvgl.label.update: id: clock_temperature_label text: format: "%.1f°C" args: [x] - platform: homeassistant id: bedroom_humidity entity_id: "${bedroom_humidity_entity}" internal: true on_value: then: - lvgl.label.update: id: bedroom_humidity_label text: format: "%.0f%%" args: [x] - lvgl.label.update: id: clock_humidity_label text: format: "%.0f%%" args: [x] # Current heat-pump target temperature. - platform: homeassistant id: ha_climate_setpoint entity_id: "${climate_entity}" attribute: temperature internal: true on_value: then: - script.execute: refresh_climate_controls # Current ceiling-fan percentage, normally 33 / 66 / 100. - platform: homeassistant id: ha_ceiling_fan_percentage entity_id: "${ceiling_fan_entity}" attribute: percentage internal: true on_value: then: - script.execute: refresh_climate_controls # Current bedroom air-filter speed level. - platform: homeassistant id: ha_air_filter_level entity_id: "${air_filter_level_entity}" internal: true on_value: then: - script.execute: refresh_climate_controls #:######################################################################################:# # ATTO 3 NUMERIC VALUES #:######################################################################################:# - platform: homeassistant id: ha_atto_range entity_id: "${atto_range_entity}" internal: true on_value: then: - script.execute: refresh_vehicle_controls - platform: homeassistant id: ha_atto_battery_level entity_id: "${atto_battery_level_entity}" internal: true on_value: then: - script.execute: refresh_vehicle_controls - platform: homeassistant id: ha_atto_cabin_temperature entity_id: "${atto_cabin_temperature_entity}" internal: true on_value: then: - script.execute: refresh_vehicle_controls - platform: homeassistant id: ha_atto_setpoint entity_id: "${atto_climate_entity}" attribute: temperature internal: true on_value: then: - lambda: |- if (isnan(x)) { return; } // BYD cloud polling can briefly return the previous target after // a command. Do not let that stale value reverse the display. if ( !id(atto_setpoint_optimistic) || fabsf(x - id(atto_setpoint_local)) < 0.1f ) { id(atto_setpoint_local) = x; id(atto_setpoint_optimistic) = false; } - script.execute: refresh_vehicle_controls #:########################################################################################:# # HOME ASSISTANT BINARY SENSORS: # Imported states used to colour LVGL buttons #:########################################################################################:# binary_sensor: #:######################################################################################:# # ATTO 3 BINARY STATES #:######################################################################################:# - platform: homeassistant id: ha_atto_charging entity_id: "${atto_charging_entity}" internal: true on_state: then: - script.execute: refresh_vehicle_controls - platform: homeassistant id: ha_atto_schedule_enabled entity_id: "${atto_schedule_enabled_entity}" internal: true on_state: then: - script.execute: refresh_vehicle_controls - platform: homeassistant id: ha_north_lights entity_id: "${north_lights_entity}" internal: true on_state: then: - lvgl.widget.update: id: north_lights_button bg_color: !lambda |- return x ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - platform: homeassistant id: ha_south_lights entity_id: "${south_lights_entity}" internal: true on_state: then: - lvgl.widget.update: id: south_lights_button bg_color: !lambda |- return x ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - platform: homeassistant id: ha_bedside_lamp entity_id: "${bedside_lamp_entity}" internal: true on_state: then: - lvgl.widget.update: id: bedside_lamp_button bg_color: !lambda |- return x ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - platform: homeassistant id: ha_hall_floor entity_id: "${hall_floor_entity}" internal: true on_state: then: - lvgl.widget.update: id: hall_floor_button bg_color: !lambda |- return x ? lv_color_hex(0x92702D) : lv_color_hex(0x28313D); - platform: homeassistant id: ha_hall_lights entity_id: "${hall_lights_entity}" internal: true on_state: then: - lvgl.widget.update: id: hall_lights_button bg_color: !lambda |- return x ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - platform: homeassistant id: ha_bathroom_lights entity_id: "${bathroom_lights_entity}" internal: true on_state: then: - lvgl.widget.update: id: bathroom_lights_button bg_color: !lambda |- return x ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - platform: homeassistant id: ha_patio_lights entity_id: "${patio_lights_entity}" internal: true on_state: then: - lvgl.widget.update: id: patio_lights_button bg_color: !lambda |- return x ? lv_color_hex(0xA87500) : lv_color_hex(0x28313D); - platform: homeassistant id: ha_garage_door entity_id: "${garage_door_entity}" internal: true on_state: then: - lvgl.widget.update: id: garage_lights_button bg_color: !lambda |- if (x) { return lv_color_hex(0xC25B13); } return id(ha_garage_lights).state ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - platform: homeassistant id: ha_bathroom_fan entity_id: "${bathroom_fan_entity}" internal: true on_state: then: - lvgl.widget.update: id: bathroom_fan_button bg_color: !lambda |- return x ? lv_color_hex(0x1668A8) : lv_color_hex(0x28313D); - platform: homeassistant id: ha_ceiling_fan_on entity_id: "${ceiling_fan_entity}" internal: true on_state: then: - script.execute: refresh_climate_controls - platform: homeassistant id: ha_climate_night_power entity_id: "${climate_night_power_entity}" internal: true on_state: then: - script.execute: refresh_climate_controls - platform: homeassistant id: ha_nanoe entity_id: "${nanoe_entity}" internal: true on_state: then: - lambda: |- id(nanoe_optimistic) = -1; - script.execute: refresh_climate_controls - platform: homeassistant id: ha_quiet_time entity_id: "${quiet_time_entity}" internal: true on_state: then: - lvgl.widget.update: id: bedtime_button bg_color: !lambda |- return x ? lv_color_hex(0x705295) : lv_color_hex(0x3B3048); - lvgl.label.update: id: clock_status_label text: !lambda |- return x ? std::string("Quiet Time active") : std::string(""); #:########################################################################################:# # GLOBALS: # 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" # -1 = follow HA, 0 = Normal, 1 = Quiet, 2 = Boost. - id: climate_profile_optimistic type: int restore_value: false initial_value: "-1" # -1 = follow HA, 0 = off, 1 = on. - id: nanoe_optimistic type: int 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 type: uint32_t restore_value: true initial_value: "0" # True only while the Security page is visible. - id: security_page_active type: bool restore_value: false initial_value: "false" # 1 = Doorbell, 2 = Driveway. Camera3 and Camera4 remain placeholders. - id: security_camera_selection type: int restore_value: false initial_value: "1" #:########################################################################################:# # SCRIPTS: # 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 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 - lvgl.widget.update: id: north_lights_button bg_color: !lambda |- return id(ha_north_lights).state ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - lvgl.widget.update: id: south_lights_button bg_color: !lambda |- return id(ha_south_lights).state ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - lvgl.widget.update: id: bedside_lamp_button bg_color: !lambda |- return id(ha_bedside_lamp).state ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - lvgl.widget.update: id: hall_floor_button bg_color: !lambda |- return id(ha_hall_floor).state ? lv_color_hex(0x92702D) : lv_color_hex(0x28313D); - lvgl.widget.update: id: hall_lights_button bg_color: !lambda |- return id(ha_hall_lights).state ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - lvgl.widget.update: id: bathroom_lights_button bg_color: !lambda |- return id(ha_bathroom_lights).state ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - lvgl.widget.update: id: patio_lights_button bg_color: !lambda |- return id(ha_patio_lights).state ? lv_color_hex(0xA87500) : lv_color_hex(0x28313D); - lvgl.widget.update: id: garage_lights_button bg_color: !lambda |- if (id(ha_garage_door).state) { return lv_color_hex(0xC25B13); } return id(ha_garage_lights).state ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - lvgl.widget.update: id: bathroom_fan_button bg_color: !lambda |- return id(ha_bathroom_fan).state ? lv_color_hex(0x1668A8) : lv_color_hex(0x28313D); - lvgl.widget.update: id: bedtime_button bg_color: !lambda |- return id(ha_quiet_time).state ? lv_color_hex(0x705295) : lv_color_hex(0x3B3048); - lvgl.label.update: id: clock_status_label text: !lambda |- return id(ha_quiet_time).state ? std::string("Quiet Time active") : std::string(""); - script.execute: refresh_climate_controls - script.execute: refresh_vehicle_controls #:######################################################################################:# # VEHICLES PAGE STATE REFRESH #:######################################################################################:# - id: refresh_vehicle_controls mode: restart then: - if: condition: lambda: |- return id(lvgl_ready); then: # Lock state and optimistic lock/unlock feedback. - lvgl.label.update: id: atto_lock_button_label text: !lambda |- if (id(atto_lock_optimistic) == 1) { return std::string("Locked\n(Hold)"); } if (id(atto_lock_optimistic) == 0) { return std::string("Unlocked\n(Hold)"); } const auto state = id(ha_atto_lock_state).state; if (state == "locked") { return std::string("Locked\n(Hold)"); } if (state == "locking") { return std::string("Locking..."); } if (state == "unlocking") { return std::string("Unlocking..."); } return std::string("Unlocked\n(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 |- 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: prominent status and bright red active button. - lvgl.label.update: id: atto_charging_state_label text: !lambda |- return id(ha_atto_charging).state ? std::string("CHARGING") : std::string("Off"); - lvgl.widget.update: id: atto_charging_status bg_color: !lambda |- return id(ha_atto_charging).state ? lv_color_hex(0xD32F2F) : lv_color_hex(0x28313D); # Charging schedule state. - lvgl.label.update: id: atto_schedule_state_label text: !lambda |- return id(ha_atto_schedule_enabled).state ? std::string("ENABLED") : std::string("NOT ENABLED"); - lvgl.widget.update: id: atto_schedule_button bg_color: !lambda |- return id(ha_atto_schedule_enabled).state ? lv_color_hex(0x315E82) : lv_color_hex(0x28313D); # Vehicle values. - lvgl.label.update: id: atto_range_value_label text: !lambda |- const float value = id(ha_atto_range).state; if (isnan(value)) { return std::string("-- km"); } char buffer[20]; snprintf(buffer, sizeof(buffer), "%.0f km", value); return std::string(buffer); - lvgl.label.update: id: atto_battery_value_label text: !lambda |- const float value = id(ha_atto_battery_level).state; if (isnan(value)) { return std::string("--%"); } char buffer[20]; snprintf(buffer, sizeof(buffer), "%.0f%%", value); return std::string(buffer); - lvgl.label.update: id: atto_cabin_temperature_value_label text: !lambda |- const float value = id(ha_atto_cabin_temperature).state; if (isnan(value)) { return std::string("--.-°C"); } char buffer[20]; snprintf(buffer, sizeof(buffer), "%.1f°C", value); return std::string(buffer); # Seat-heating colour only; the label remains clean and fixed. - lvgl.widget.update: id: atto_passenger_seat_button bg_color: !lambda |- const auto value = id(ha_atto_passenger_seat_heating).state; if (value == "high" || value == "High") { return lv_color_hex(0xB76822); } if (value == "low" || value == "Low") { return lv_color_hex(0x7A4A22); } return lv_color_hex(0x28313D); - lvgl.widget.update: id: atto_driver_seat_button bg_color: !lambda |- const auto value = id(ha_atto_driver_seat_heating).state; if (value == "high" || value == "High") { return lv_color_hex(0xB76822); } if (value == "low" || value == "Low") { return lv_color_hex(0x7A4A22); } return lv_color_hex(0x28313D); # BYD heating/cooling intent is represented by max presets. - lvgl.widget.update: id: atto_heat_button bg_color: !lambda |- 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 |- 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 |- 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), "%.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: 30s - 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 then: - if: condition: lambda: |- return id(lvgl_ready); 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 requested_none = ( requested.empty() || requested == "none" || requested == "None" || requested == "unknown" || requested == "unavailable" ); const bool active = ( requested == "off" || requested == "Off" || (requested_none && current == "off") ); return active ? lv_color_hex(0x548C6C) : lv_color_hex(0x28313D); - 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 requested_none = ( requested.empty() || requested == "none" || requested == "None" || requested == "unknown" || requested == "unavailable" ); const bool active = ( requested == "heat" || requested == "Heat" || (requested_none && current == "heat") ); return active ? lv_color_hex(0xB76822) : lv_color_hex(0x28313D); - 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 requested_none = ( requested.empty() || requested == "none" || requested == "None" || requested == "unknown" || requested == "unavailable" ); const bool active = ( requested == "cool" || requested == "Cool" || (requested_none && current == "cool") ); return active ? lv_color_hex(0x2E6A96) : lv_color_hex(0x28313D); - 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 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. Optimistic state wins # immediately; actual Panasonic preset is authoritative afterwards. - lvgl.widget.update: id: climate_quiet_button bg_color: !lambda |- int selected = id(climate_profile_optimistic); if (selected < 0) { const auto preset = id(ha_climate_preset_mode).state; const auto helper = id(ha_climate_profile).state; if ( preset == "quiet" || preset == "Quiet" || helper == "Quiet" || helper == "quiet" ) { selected = 1; } else if ( preset == "powerful" || preset == "Powerful" || helper == "Boost" || helper == "boost" ) { selected = 2; } else { selected = 0; } } return selected == 1 ? lv_color_hex(0x315843) : lv_color_hex(0x28313D); - lvgl.widget.update: id: climate_boost_button bg_color: !lambda |- int selected = id(climate_profile_optimistic); if (selected < 0) { const auto preset = id(ha_climate_preset_mode).state; const auto helper = id(ha_climate_profile).state; if ( preset == "quiet" || preset == "Quiet" || helper == "Quiet" || helper == "quiet" ) { selected = 1; } else if ( preset == "powerful" || preset == "Powerful" || helper == "Boost" || helper == "boost" ) { selected = 2; } else { selected = 0; } } return selected == 2 ? lv_color_hex(0x6B4A86) : lv_color_hex(0x28313D); - lvgl.widget.update: id: climate_nanoe_button bg_color: !lambda |- bool active = id(ha_nanoe).state; if (id(nanoe_optimistic) >= 0) { active = id(nanoe_optimistic) == 1; } return active ? 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 -> Low -> Medium -> High -> 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: Low"); } if (percentage < 85.0f) { return std::string("Ceiling Fan: Medium"); } return std::string("Ceiling Fan: High"); - 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); # Update the Sleep timer label and active button colour. - id: refresh_sleep_timer mode: restart then: - if: condition: lambda: |- return id(lvgl_ready); then: - lvgl.label.update: id: sleep_timer_button_label text: !lambda |- const auto now = id(ha_time).now(); if ( id(sleep_timer_end_epoch) == 0 || !now.is_valid() || now.timestamp >= id(sleep_timer_end_epoch) ) { return std::string("Sleep: 2 hrs (0)"); } const uint32_t seconds_remaining = id(sleep_timer_end_epoch) - now.timestamp; const uint32_t minutes_remaining = (seconds_remaining + 59) / 60; char buffer[32]; snprintf( buffer, sizeof(buffer), "Sleep: 2 hrs (%u)", static_cast(minutes_remaining) ); return std::string(buffer); - lvgl.widget.update: id: sleep_timer_button bg_color: !lambda |- const auto now = id(ha_time).now(); const bool active = ( id(sleep_timer_end_epoch) > 0 && now.is_valid() && now.timestamp < id(sleep_timer_end_epoch) ); return active ? lv_color_hex(0x6B4A86) : lv_color_hex(0x28313D); # Highlight the selected Security camera. - id: refresh_security_camera_buttons mode: restart then: - if: condition: lambda: |- return ( id(lvgl_ready) && id(security_page_active) ); then: - lvgl.widget.update: id: security_doorbell_button bg_color: !lambda |- return id(security_camera_selection) == 1 ? lv_color_hex(0x493535) : lv_color_hex(0x28313D); - lvgl.widget.update: id: security_driveway_button bg_color: !lambda |- return id(security_camera_selection) == 2 ? lv_color_hex(0x493535) : lv_color_hex(0x28313D); # Download the selected Security camera image through the authenticated # proxy path supplied by Home Assistant. - id: refresh_security_camera mode: restart then: - if: condition: lambda: |- return ( id(lvgl_ready) && id(security_page_active) ); then: - script.execute: refresh_security_camera_buttons - if: condition: lambda: |- std::string path; if (id(security_camera_selection) == 2) { path = id(ha_driveway_entity_picture).state; } else { path = id(ha_doorbell_entity_picture).state; } return ( !path.empty() && path != "unknown" && path != "unavailable" ); then: - lvgl.label.update: id: security_camera_status_label text: !lambda |- return id(security_camera_selection) == 2 ? std::string("Loading Driveway...") : std::string("Loading Doorbell..."); - online_image.set_url: id: security_camera_image url: !lambda |- std::string path; if (id(security_camera_selection) == 2) { path = id(ha_driveway_entity_picture).state; } else { path = id(ha_doorbell_entity_picture).state; } std::string url; if ( path.rfind("http://", 0) == 0 || path.rfind("https://", 0) == 0 ) { url = path; } else { url = std::string( "${home_assistant_internal_url}" ) + path; } char suffix[32]; snprintf( suffix, sizeof(suffix), "%ctime=%lu", url.find('?') == std::string::npos ? '?' : '&', static_cast(millis()) ); return url + suffix; else: - lvgl.label.update: id: security_camera_status_label text: !lambda |- return id(security_camera_selection) == 2 ? std::string("Waiting for Driveway camera") : std::string("Waiting for Doorbell camera"); - id: release_climate_profile_optimistic mode: restart then: - delay: 5s - lambda: |- id(climate_profile_optimistic) = -1; - script.execute: refresh_climate_controls - id: release_nanoe_optimistic mode: restart then: - delay: 5s - lambda: |- id(nanoe_optimistic) = -1; - script.execute: refresh_climate_controls # Two passes allow time for LVGL creation and Home Assistant state delivery. - id: startup_button_state_sync mode: restart then: - delay: 1500ms - script.execute: refresh_button_states - delay: 3500ms - script.execute: refresh_button_states - script.execute: refresh_sleep_timer #:########################################################################################:# # INTERVAL: # Update minute-resolution clock values locally #:########################################################################################:# interval: - interval: 10s then: - if: condition: lambda: |- return id(ha_time).now().is_valid(); then: - lvgl.label.update: id: clock_time_label text: !lambda |- auto now = id(ha_time).now(); char buffer[8]; now.strftime(buffer, sizeof(buffer), "%H:%M"); return std::string(buffer); - lvgl.label.update: id: clock_date_label text: !lambda |- auto now = id(ha_time).now(); char weekday[16]; char month[16]; now.strftime(weekday, sizeof(weekday), "%A"); now.strftime(month, sizeof(month), "%B"); char buffer[48]; snprintf( buffer, sizeof(buffer), "%s, %d %s", weekday, now.day_of_month, month ); return std::string(buffer); # Maintain the local two-hour Sleep countdown. - if: condition: lambda: |- const auto now = id(ha_time).now(); return ( id(sleep_timer_end_epoch) > 0 && now.is_valid() && now.timestamp >= id(sleep_timer_end_epoch) ); then: - lambda: |- id(sleep_timer_end_epoch) = 0; - homeassistant.action: action: script.turn_on data: entity_id: "${climate_off_script}" - script.execute: refresh_sleep_timer else: - script.execute: refresh_sleep_timer - interval: "${security_camera_refresh_interval}" then: - if: condition: lambda: |- return id(security_page_active); then: - script.execute: refresh_security_camera #:########################################################################################:# # HOME ASSISTANT SWITCH COMPONENT: # https://esphome.io/components/switch/homeassistant/ #:########################################################################################:# switch: - platform: homeassistant id: ha_garage_lights entity_id: "${garage_lights_entity}" internal: true on_turn_on: then: - logger.log: level: INFO format: "Garage Lights HA switch reports ON" - lvgl.widget.update: id: garage_lights_button bg_color: !lambda |- if (id(ha_garage_door).state) { return lv_color_hex(0xC25B13); } return lv_color_hex(0xB88918); on_turn_off: then: - logger.log: level: INFO format: "Garage Lights HA switch reports OFF" - lvgl.widget.update: id: garage_lights_button bg_color: !lambda |- if (id(ha_garage_door).state) { return lv_color_hex(0xC25B13); } return lv_color_hex(0x28313D); #:########################################################################################:# # TEXT SENSOR COMPONENT: # https://esphome.io/components/text_sensor/ #:########################################################################################:# 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}" internal: true on_value: then: - script.execute: refresh_vehicle_controls - platform: homeassistant id: ha_atto_passenger_seat_heating entity_id: "${atto_passenger_seat_heating_entity}" internal: true on_value: then: - script.execute: refresh_vehicle_controls - platform: homeassistant id: ha_atto_climate_mode entity_id: "${atto_climate_entity}" internal: true on_value: 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 entity_id: "${climate_requested_mode_entity}" internal: true on_value: then: - script.execute: refresh_climate_controls # Actual current HVAC mode provides the fallback when requested mode is none. - platform: homeassistant id: ha_climate_current_mode entity_id: "${climate_entity}" internal: true on_value: then: - script.execute: refresh_climate_controls # User-facing profile helper: Boost / Quiet / Normal. - platform: homeassistant id: ha_climate_profile entity_id: "${climate_profile_entity}" internal: true on_value: then: - script.execute: refresh_climate_controls # Actual Panasonic Comfort Cloud preset for this installation: # quiet = Quiet, powerful = Boost, none = Normal. - platform: homeassistant id: ha_climate_preset_mode entity_id: "${climate_entity}" attribute: preset_mode internal: true on_value: then: - script.execute: refresh_climate_controls # Home Assistant supplies short-lived authenticated camera proxy URLs in # each entity_picture attribute. This avoids storing an HA token in firmware. - platform: homeassistant id: ha_doorbell_entity_picture entity_id: "${security_doorbell_camera_entity}" attribute: entity_picture internal: true on_value: then: - if: condition: lambda: |- return ( id(security_page_active) && id(security_camera_selection) == 1 ); then: - script.execute: refresh_security_camera - platform: homeassistant id: ha_driveway_entity_picture entity_id: "${security_driveway_camera_entity}" attribute: entity_picture internal: true on_value: then: - if: condition: lambda: |- return ( id(security_page_active) && id(security_camera_selection) == 2 ); then: - script.execute: refresh_security_camera - platform: version name: "${friendly_name} ESPHome Version" hide_timestamp: true - platform: wifi_info ip_address: name: "${friendly_name} IP Address" ssid: name: "${friendly_name} Connected SSID" bssid: name: "${friendly_name} Connected BSSID" mac_address: name: "${friendly_name} MAC Address" #:########################################################################################:# # HTTP REQUEST: # Used by the online Doorbell image # https://esphome.io/components/http_request/ #:########################################################################################:# http_request: timeout: 10s follow_redirects: true #:########################################################################################:# # ONLINE SECURITY CAMERA IMAGE: # Home Assistant camera proxy image, resized for the Security page # https://esphome.io/components/image/online_image/ #:########################################################################################:# image: - platform: online_image id: security_camera_image url: "${home_assistant_internal_url}/" format: JPEG type: RGB565 resize: 640x360 update_interval: never buffer_size: 32768 on_download_finished: then: - if: condition: lambda: |- return ( id(lvgl_ready) && id(security_page_active) ); then: - lvgl.image.update: id: security_camera_widget src: security_camera_image - lvgl.label.update: id: security_camera_status_label text: "" on_error: then: - if: condition: lambda: |- return ( id(lvgl_ready) && id(security_page_active) ); then: - lvgl.label.update: id: security_camera_status_label text: "Camera image unavailable" #:########################################################################################:# # LVGL: # https://esphome.io/components/lvgl/ #:########################################################################################:# lvgl: id: panel_lvgl displays: - main_display touchscreens: - touchscreen_id: main_touchscreen long_press_time: 1200ms long_press_repeat_time: 500ms rotation: 0 buffer_size: 6% log_level: WARN default_font: font_button page_wrap: false theme: dark_mode: true on_idle: - timeout: "${clock_idle_timeout}" then: - lambda: |- id(security_page_active) = false; - online_image.release: security_camera_image - lvgl.page.show: id: clock_page animation: FADE_IN time: 250ms - light.turn_on: id: display_backlight brightness: "${clock_brightness}" transition_length: 1500ms pages: #:####################################################################################:# # BEDROOM PAGE #:####################################################################################:# # Row 1: Bathroom Lights | Hall Floor | Bedside Lamp # Row 2: Bathroom Fan | Hall Lights | South Lights # Row 3: Garage Lights | Patio Lights | North Lights - id: bedroom_page bg_color: 0x10151D bg_opa: COVER widgets: # Home Assistant API status: # Green = connected, red = disconnected. - obj: id: bedroom_connection_indicator x: 30 y: 25 width: 18 height: 18 radius: 9 bg_color: 0xF06A6A bg_opa: COVER border_width: 0 shadow_width: 0 clickable: false # Four status readings across the top - label: x: 355 y: 12 width: 150 text: "Moon" text_font: font_small text_align: CENTER text_color: 0xA9A0D4 - label: id: bedroom_moon_value_label x: 355 y: 39 width: 150 text: "--.-%" text_font: font_button text_align: CENTER text_color: 0xC6BDEC - label: x: 510 y: 12 width: 150 text: "Outdoor" text_font: font_small text_align: CENTER text_color: 0x84B5D8 - label: id: bedroom_outdoor_value_label x: 510 y: 39 width: 150 text: "--.-°C" text_font: font_button text_align: CENTER text_color: 0x9CCBEA - label: x: 665 y: 12 width: 150 text: "Indoor" text_font: font_small text_align: CENTER text_color: 0xD6B78F - label: id: bedroom_temperature_label x: 665 y: 39 width: 150 text: "--.-°C" text_font: font_button text_align: CENTER text_color: 0xE8C79D - label: x: 820 y: 12 width: 165 text: "Humidity" text_font: font_small text_align: CENTER text_color: 0x80BDA4 - label: id: bedroom_humidity_label x: 820 y: 39 width: 165 text: "--%" text_font: font_button text_align: CENTER text_color: 0x97D2B7 - button: id: north_lights_button x: 689 y: 341 width: 300 height: 105 widgets: - label: align: LEFT_MID x: 24 text: "\U000F0335" text_font: font_mdi_button text_color: 0xFFFFFF - label: id: north_lights_button_label align: CENTER x: 18 text: "North Lights" text_font: font_button text_color: 0xFFFFFF bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x445365 radius: 18 pressed: bg_color: 0x425166 on_click: - homeassistant.action: action: light.toggle data: entity_id: "${north_lights_entity}" - button: id: south_lights_button x: 689 y: 218 width: 300 height: 105 widgets: - label: align: LEFT_MID x: 24 text: "\U000F0335" text_font: font_mdi_button text_color: 0xFFFFFF - label: id: south_lights_button_label align: CENTER x: 18 text: "South Lights" text_font: font_button text_color: 0xFFFFFF bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x445365 radius: 18 pressed: bg_color: 0x425166 on_click: - homeassistant.action: action: switch.toggle data: entity_id: "${south_lights_entity}" - button: id: bedside_lamp_button x: 689 y: 95 width: 300 height: 105 widgets: - label: align: LEFT_MID x: 24 text: "\U000F06B5" text_font: font_mdi_button text_color: 0xFFFFFF - label: id: bedside_lamp_button_label align: CENTER x: 18 text: "Bedside Lamp" text_font: font_button text_color: 0xFFFFFF bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x445365 radius: 18 pressed: bg_color: 0x425166 on_click: - homeassistant.action: action: switch.toggle data: entity_id: "${bedside_lamp_entity}" - button: id: hall_floor_button x: 362 y: 95 width: 300 height: 105 widgets: - label: align: LEFT_MID x: 24 text: "\U000F08DD" text_font: font_mdi_button text_color: 0xFFFFFF - label: id: hall_floor_button_label align: CENTER x: 18 text: "Hall Floor" text_font: font_button text_color: 0xFFFFFF bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x445365 radius: 18 pressed: bg_color: 0x425166 on_click: - homeassistant.action: action: switch.toggle data: entity_id: "${hall_floor_entity}" - button: id: hall_lights_button x: 362 y: 218 width: 300 height: 105 widgets: - label: align: LEFT_MID x: 24 text: "\U000F0769" text_font: font_mdi_button text_color: 0xFFFFFF - label: id: hall_lights_button_label align: CENTER x: 18 text: "Hall Lights" text_font: font_button text_color: 0xFFFFFF bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x445365 radius: 18 pressed: bg_color: 0x425166 on_click: - homeassistant.action: action: switch.toggle data: entity_id: "${hall_lights_entity}" - button: id: bathroom_lights_button x: 35 y: 95 width: 300 height: 105 widgets: - label: align: LEFT_MID x: 24 text: "\U000F1253" text_font: font_mdi_button text_color: 0xFFFFFF - label: id: bathroom_lights_button_label align: CENTER x: 18 text: "Bathroom Lights" text_font: font_button text_color: 0xFFFFFF bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x445365 radius: 18 pressed: bg_color: 0x425166 on_click: - homeassistant.action: action: switch.toggle data: entity_id: "${bathroom_lights_entity}" - button: id: patio_lights_button x: 362 y: 341 width: 300 height: 105 widgets: - label: align: LEFT_MID x: 24 text: "\U000F1054" text_font: font_mdi_button text_color: 0xFFFFFF - label: id: patio_lights_button_label align: CENTER x: 18 text: "Patio Lights" text_font: font_button text_color: 0xFFFFFF bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x445365 radius: 18 pressed: bg_color: 0x425166 on_click: - homeassistant.action: action: switch.toggle data: entity_id: "${patio_lights_entity}" - button: id: garage_lights_button x: 35 y: 341 width: 300 height: 105 widgets: - label: align: LEFT_MID x: 24 text: "\U000F06D9" text_font: font_mdi_button text_color: 0xFFFFFF - label: id: garage_lights_button_label align: CENTER x: 18 text: "Garage Lights" text_font: font_button text_color: 0xFFFFFF bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x445365 radius: 18 pressed: bg_color: 0x425166 on_click: - logger.log: level: INFO format: "Garage Lights button clicked; toggling imported HA switch" - switch.toggle: ha_garage_lights - button: id: bathroom_fan_button x: 35 y: 218 width: 300 height: 105 widgets: - label: align: LEFT_MID x: 24 text: "\U000F0210" text_font: font_mdi_button text_color: 0xFFFFFF - label: id: bathroom_fan_button_label align: CENTER x: 18 text: "Bathroom Fan" text_font: font_button text_color: 0xFFFFFF bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x445365 radius: 18 pressed: bg_color: 0x425166 on_click: - homeassistant.action: action: switch.toggle data: entity_id: "${bathroom_fan_entity}" #:################################################################################:# # BEDROOM PAGE BOTTOM NAVIGATION #:################################################################################:# - button: id: bedtime_button x: 18 y: 475 width: 190 height: 92 widgets: - label: id: bedtime_button_label align: CENTER text: "Bedtime (Hold)" text_font: font_button text_color: 0xFFFFFF bg_color: 0x3B3048 bg_opa: COVER border_width: 2 border_color: 0x735E88 radius: 16 pressed: bg_color: 0x584668 on_long_press: then: - homeassistant.action: action: input_button.press data: entity_id: "${bedtime_button_entity}" - lvgl.label.update: id: bedtime_button_label text: "Activated" - delay: 2s - lvgl.label.update: id: bedtime_button_label text: "Bedtime (Hold)" - button: id: climate_navigation_button x: 216 y: 475 width: 190 height: 92 widgets: - label: id: climate_navigation_button_label align: CENTER text: "Climate" text_font: font_button text_color: 0xFFFFFF bg_color: 0x315843 bg_opa: COVER border_width: 2 border_color: 0x548C6C radius: 16 pressed: bg_color: 0x416F55 on_click: - lvgl.page.show: id: climate_page animation: MOVE_LEFT time: 220ms - button: id: atto_navigation_button x: 414 y: 475 width: 190 height: 92 widgets: - label: id: atto_navigation_button_label align: CENTER text: "Atto (Hold)" text_font: font_button text_color: 0xFFFFFF bg_color: 0x3E4652 bg_opa: COVER border_width: 2 border_color: 0x687587 radius: 16 pressed: bg_color: 0x566170 on_long_press: then: - lvgl.page.show: id: vehicles_page animation: MOVE_LEFT time: 220ms - light.turn_on: id: display_backlight brightness: "${control_brightness}" transition_length: 250ms - script.execute: refresh_vehicle_controls - button: id: security_navigation_button x: 612 y: 475 width: 190 height: 92 widgets: - label: id: security_navigation_button_label align: CENTER text: "Security" text_font: font_button text_color: 0xFFFFFF bg_color: 0x493535 bg_opa: COVER border_width: 2 border_color: 0x805858 radius: 16 pressed: bg_color: 0x654949 on_click: - lambda: |- id(security_page_active) = true; - lvgl.page.show: id: security_page animation: MOVE_LEFT time: 220ms - light.turn_on: id: display_backlight brightness: "${control_brightness}" transition_length: 250ms - script.execute: refresh_security_camera_buttons - script.execute: refresh_security_camera - button: id: clock_now_button x: 810 y: 475 width: 190 height: 92 widgets: - label: id: clock_now_button_label align: CENTER text: "Clock" text_font: font_button text_color: 0xFFFFFF bg_color: 0x214F72 bg_opa: COVER border_width: 2 border_color: 0x3E7FA7 radius: 16 pressed: bg_color: 0x2E6A96 on_click: - lvgl.page.show: id: clock_page animation: FADE_IN time: 250ms - light.turn_on: id: display_backlight brightness: "${clock_brightness}" transition_length: 1500ms #:####################################################################################:# # VEHICLES PAGE #:####################################################################################:# - id: vehicles_page skip: true bg_color: 0x111923 bg_opa: COVER widgets: #:################################################################################:# # VEHICLE STATUS / PRIMARY CONTROLS #:################################################################################:# - button: id: atto_lock_button x: 25 y: 130 width: 220 height: 92 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x548C6C radius: 17 pressed: bg_color: 0x416F55 widgets: - label: id: atto_lock_icon_label align: LEFT_MID x: 10 y: 0 text: "\U000F033E" text_font: font_mdi_vehicle text_color: 0xFFFFFF - label: id: atto_lock_button_label x: 44 y: 19 width: 166 text: "Locked\n(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(atto_lock_optimistic) == 1; then: - homeassistant.action: action: lock.lock data: entity_id: "${atto_lock_entity}" else: - homeassistant.action: action: lock.unlock data: entity_id: "${atto_lock_entity}" - script.execute: release_atto_lock_optimistic - button: id: atto_charging_status x: 272 y: 130 width: 220 height: 92 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0xA96363 radius: 17 widgets: - label: align: LEFT_MID x: 10 y: 0 text: "\U000F0084" text_font: font_mdi_vehicle text_color: 0xFFFFFF - label: x: 44 y: 11 width: 166 text: "Atto Charging" text_font: font_small text_align: CENTER text_color: 0xFFFFFF - label: id: atto_charging_state_label x: 44 y: 43 width: 166 text: "Off" text_font: font_vehicle_bold text_align: CENTER text_color: 0xFFFFFF - button: id: atto_schedule_button x: 519 y: 130 width: 220 height: 92 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x47789B radius: 17 pressed: bg_color: 0x315E82 widgets: - label: align: LEFT_MID x: 10 y: 0 text: "\U000F00F0" text_font: font_mdi_vehicle text_color: 0xFFFFFF - label: x: 44 y: 11 width: 166 text: "Schedule" text_font: font_small text_align: CENTER text_color: 0xFFFFFF - label: id: atto_schedule_state_label x: 44 y: 43 width: 166 text: "NOT ENABLED" text_font: font_vehicle_bold text_align: CENTER text_color: 0xFFFFFF on_click: - homeassistant.action: action: switch.toggle data: entity_id: "${atto_schedule_enabled_entity}" - button: id: atto_driver_seat_button x: 766 y: 22 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: LEFT_MID x: 10 y: 0 text: "\U000F0FA6" text_font: font_mdi_vehicle text_color: 0xFFFFFF - label: id: atto_driver_seat_button_label x: 44 y: 19 width: 166 text: "Driver Seat\n(Hold)" 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" #:################################################################################:# # VEHICLE VALUES / SEAT HEATING #:################################################################################:# # Cabin, battery level and range are display-only labels. # No parent object is used, so there is no border, outline or clipping. - label: x: 42 y: 53 text: "\U000F050F" text_font: font_mdi_vehicle text_color: 0xFFFFFF - label: x: 82 y: 31 width: 145 text: "Cabin" text_font: font_small text_align: CENTER text_color: 0xAEB8C4 - label: id: atto_cabin_temperature_value_label x: 82 y: 60 width: 145 text: "--.-°C" text_font: font_vehicle_bold text_align: CENTER text_color: 0xFFFFFF - label: x: 289 y: 53 text: "\U000F0079" text_font: font_mdi_vehicle text_color: 0xFFFFFF - label: x: 329 y: 31 width: 145 text: "Level" text_font: font_small text_align: CENTER text_color: 0xAEB8C4 - label: id: atto_battery_value_label x: 329 y: 60 width: 145 text: "--%" text_font: font_vehicle_bold text_align: CENTER text_color: 0xFFFFFF - label: x: 536 y: 53 text: "\U000F08F0" text_font: font_mdi_vehicle text_color: 0xFFFFFF - label: x: 576 y: 31 width: 145 text: "Range" text_font: font_small text_align: CENTER text_color: 0xAEB8C4 - label: id: atto_range_value_label x: 576 y: 60 width: 145 text: "-- km" text_font: font_vehicle_bold text_align: CENTER text_color: 0xFFFFFF - 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: align: LEFT_MID x: 10 y: 0 text: "\U000F0FA6" text_font: font_mdi_vehicle text_color: 0xFFFFFF - label: id: atto_passenger_seat_button_label x: 44 y: 19 width: 166 text: "Passenger Seat\n(Hold)" text_font: font_small text_align: CENTER text_color: 0xFFFFFF on_long_press: then: - if: condition: lambda: |- const auto value = id(ha_atto_passenger_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_passenger_seat_heating_entity}" option: "low" else: - if: condition: lambda: |- const auto value = id(ha_atto_passenger_seat_heating).state; return value == "low" || value == "Low"; then: - homeassistant.action: action: select.select_option data: entity_id: "${atto_passenger_seat_heating_entity}" option: "high" else: - homeassistant.action: action: select.select_option data: entity_id: "${atto_passenger_seat_heating_entity}" option: "off" #:################################################################################:# # ATTO CLIMATE / SETPOINT #:################################################################################:# - label: x: 639 y: 230 width: 227 text: "Setpoint" text_font: font_small text_align: CENTER text_color: 0x8FA0B3 - button: id: atto_heat_button x: 25 y: 260 width: 220 height: 105 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0xA6672B radius: 18 pressed: bg_color: 0xB76822 widgets: - label: align: LEFT_MID x: 10 text: "\U000F0238" text_font: font_mdi_vehicle text_color: 0xFFFFFF - label: align: CENTER x: 28 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_preset_mode data: entity_id: "${atto_climate_entity}" preset_mode: "max_heat" - script.execute: release_atto_climate_optimistic - button: id: atto_cool_button x: 272 y: 260 width: 220 height: 105 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x3E7FA7 radius: 18 pressed: bg_color: 0x2E6A96 widgets: - label: align: LEFT_MID x: 10 text: "\U000F0717" text_font: font_mdi_vehicle text_color: 0xFFFFFF - label: align: CENTER x: 28 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_preset_mode data: entity_id: "${atto_climate_entity}" preset_mode: "max_cool" - script.execute: release_atto_climate_optimistic - button: id: atto_setpoint_down_button x: 519 y: 260 width: 110 height: 105 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x526276 radius: 18 pressed: bg_color: 0x38485A widgets: - label: align: CENTER text: "\U000F0140" 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: entity_id: "${atto_climate_entity}" data_template: temperature: "{{ target_temperature }}" variables: target_temperature: |- return id(atto_setpoint_local); - script.execute: release_atto_setpoint_optimistic - button: id: atto_setpoint_display x: 639 y: 260 width: 227 height: 105 bg_color: 0x1C2632 bg_opa: COVER border_width: 2 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 x: 15 text: "--°C" text_font: font_title text_color: 0xFFFFFF - button: id: atto_setpoint_up_button x: 876 y: 260 width: 110 height: 105 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x526276 radius: 18 pressed: bg_color: 0x38485A widgets: - label: align: CENTER text: "\U000F0143" 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: entity_id: "${atto_climate_entity}" data_template: temperature: "{{ target_temperature }}" variables: target_temperature: |- return id(atto_setpoint_local); - script.execute: release_atto_setpoint_optimistic #:################################################################################:# # BOTTOM NAVIGATION #:################################################################################:# - button: id: vehicles_back_button x: 612 y: 475 width: 190 height: 92 bg_color: 0x315843 bg_opa: COVER border_width: 2 border_color: 0x548C6C radius: 16 pressed: bg_color: 0x416F55 widgets: - label: align: CENTER text: "Back" text_font: font_button text_color: 0xFFFFFF on_click: - lvgl.page.show: id: bedroom_page animation: MOVE_RIGHT time: 220ms - button: id: vehicles_clock_button x: 810 y: 475 width: 190 height: 92 bg_color: 0x214F72 bg_opa: COVER border_width: 2 border_color: 0x3E7FA7 radius: 16 pressed: bg_color: 0x2E6A96 widgets: - label: align: CENTER text: "Clock" text_font: font_button text_color: 0xFFFFFF on_click: - lvgl.page.show: id: clock_page animation: FADE_IN time: 250ms - light.turn_on: id: display_backlight brightness: "${clock_brightness}" transition_length: 1500ms #:####################################################################################:# # CLIMATE PAGE #:####################################################################################:# - id: climate_page bg_color: 0x111923 bg_opa: COVER widgets: - label: x: 25 y: 41 width: 220 text: "Heat Pump" text_font: font_small text_align: CENTER text_color: 0x8FA0B3 #:################################################################################:# # HEAT-PUMP MODES #:################################################################################:# - button: id: climate_off_button x: 25 y: 72 width: 220 height: 96 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x5A7A68 radius: 17 pressed: bg_color: 0x548C6C widgets: - label: align: CENTER text: "Off" text_font: font_button text_color: 0xFFFFFF on_click: - homeassistant.action: action: script.turn_on data: entity_id: "${climate_off_script}" - button: id: climate_heat_button x: 272 y: 72 width: 220 height: 96 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0xA6672B radius: 17 pressed: bg_color: 0xB76822 widgets: - label: align: CENTER text: "Heat" text_font: font_button text_color: 0xFFFFFF on_click: - homeassistant.action: action: script.turn_on data: entity_id: "${climate_heat_script}" - button: id: climate_cool_button x: 519 y: 72 width: 220 height: 96 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x3E7FA7 radius: 17 pressed: bg_color: 0x2E6A96 widgets: - label: align: CENTER text: "Cool" text_font: font_button text_color: 0xFFFFFF on_click: - homeassistant.action: action: script.turn_on data: entity_id: "${climate_cool_script}" - button: id: climate_dry_button x: 766 y: 72 width: 220 height: 96 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x427B96 radius: 17 pressed: bg_color: 0x32677F widgets: - label: align: CENTER text: "Dehumid" text_font: font_button text_color: 0xFFFFFF on_click: - homeassistant.action: action: script.turn_on data: entity_id: "${climate_dry_script}" #:################################################################################:# # HEAT-PUMP PROFILE / NANO-X / SETPOINT #:################################################################################:# - label: x: 625 y: 187 width: 230 text: "Setpoint" text_font: font_small text_align: CENTER text_color: 0x8FA0B3 - button: id: climate_quiet_button x: 25 y: 218 width: 145 height: 105 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x548C6C radius: 18 pressed: bg_color: 0x315843 widgets: - label: align: CENTER text: "Quiet" text_font: font_button text_color: 0xFFFFFF on_click: # Quiet toggles between Quiet and Normal. - lambda: |- int selected = id(climate_profile_optimistic); if (selected < 0) { const auto preset = id(ha_climate_preset_mode).state; const auto helper = id(ha_climate_profile).state; if ( preset == "quiet" || preset == "Quiet" || helper == "Quiet" || helper == "quiet" ) { selected = 1; } else if ( preset == "powerful" || preset == "Powerful" || helper == "Boost" || helper == "boost" ) { selected = 2; } else { selected = 0; } } id(climate_profile_optimistic) = selected == 1 ? 0 : 1; - script.execute: refresh_climate_controls - if: condition: lambda: |- return id(climate_profile_optimistic) == 1; then: - homeassistant.action: action: input_select.select_option data: entity_id: "${climate_profile_entity}" option: "Quiet" - homeassistant.action: action: climate.set_preset_mode data: entity_id: "${climate_entity}" preset_mode: "quiet" else: - homeassistant.action: action: input_select.select_option data: entity_id: "${climate_profile_entity}" option: "Normal" - homeassistant.action: action: climate.set_preset_mode data: entity_id: "${climate_entity}" preset_mode: "none" - script.execute: release_climate_profile_optimistic - button: id: climate_boost_button x: 180 y: 218 width: 145 height: 105 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x80619B radius: 18 pressed: bg_color: 0x6B4A86 widgets: - label: align: CENTER text: "Boost" text_font: font_button text_color: 0xFFFFFF on_click: # Boost toggles between Boost and Normal. - lambda: |- int selected = id(climate_profile_optimistic); if (selected < 0) { const auto preset = id(ha_climate_preset_mode).state; const auto helper = id(ha_climate_profile).state; if ( preset == "quiet" || preset == "Quiet" || helper == "Quiet" || helper == "quiet" ) { selected = 1; } else if ( preset == "powerful" || preset == "Powerful" || helper == "Boost" || helper == "boost" ) { selected = 2; } else { selected = 0; } } id(climate_profile_optimistic) = selected == 2 ? 0 : 2; - script.execute: refresh_climate_controls - if: condition: lambda: |- return id(climate_profile_optimistic) == 2; then: - homeassistant.action: action: input_select.select_option data: entity_id: "${climate_profile_entity}" option: "Boost" - homeassistant.action: action: climate.set_preset_mode data: entity_id: "${climate_entity}" preset_mode: "powerful" else: - homeassistant.action: action: input_select.select_option data: entity_id: "${climate_profile_entity}" option: "Normal" - homeassistant.action: action: climate.set_preset_mode data: entity_id: "${climate_entity}" preset_mode: "none" - script.execute: release_climate_profile_optimistic - button: id: climate_nanoe_button x: 335 y: 218 width: 145 height: 105 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x47789B radius: 18 pressed: bg_color: 0x315E82 widgets: - label: align: CENTER text: "Nanoe-X" text_font: font_button text_color: 0xFFFFFF on_click: - lambda: |- bool active = id(ha_nanoe).state; if (id(nanoe_optimistic) >= 0) { active = id(nanoe_optimistic) == 1; } id(nanoe_optimistic) = active ? 0 : 1; - script.execute: refresh_climate_controls - if: condition: lambda: |- return id(nanoe_optimistic) == 1; then: - homeassistant.action: action: switch.turn_on data: entity_id: "${nanoe_entity}" else: - homeassistant.action: action: switch.turn_off data: entity_id: "${nanoe_entity}" - script.execute: release_nanoe_optimistic - button: id: climate_setpoint_down_button x: 505 y: 218 width: 110 height: 105 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x526276 radius: 18 pressed: bg_color: 0x38485A widgets: - label: align: CENTER text: "\U000F0140" text_font: font_mdi_setpoint text_color: 0xFFFFFF on_click: - homeassistant.action: action: "${climate_setpoint_adjust_script}" data: step: "-0.5" - button: id: climate_setpoint_display x: 625 y: 218 width: 230 height: 105 bg_color: 0x1C2632 bg_opa: COVER border_width: 2 border_color: 0x5A6C82 radius: 18 widgets: - label: id: climate_setpoint_value_label align: CENTER text: "--.-°C" text_font: font_title text_color: 0xFFFFFF - button: id: climate_setpoint_up_button x: 865 y: 218 width: 120 height: 105 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x526276 radius: 18 pressed: bg_color: 0x38485A widgets: - label: align: CENTER text: "\U000F0143" text_font: font_mdi_setpoint text_color: 0xFFFFFF on_click: - homeassistant.action: action: "${climate_setpoint_adjust_script}" data: step: "0.5" #:################################################################################:# # CEILING FAN / AIR FILTER / NIGHT POWER / SLEEP TIMER #:################################################################################:# - button: id: ceiling_fan_cycle_button x: 25 y: 356 width: 220 height: 100 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x47789B radius: 18 pressed: bg_color: 0x315E82 widgets: - label: id: ceiling_fan_cycle_label align: CENTER text: "Ceiling Fan: Off" text_font: font_small text_color: 0xFFFFFF on_click: # Off -> Low - if: condition: lambda: |- return !id(ha_ceiling_fan_on).state; then: - homeassistant.action: action: fan.set_percentage data: entity_id: "${ceiling_fan_entity}" percentage: "33" else: # Low -> Medium - if: condition: lambda: |- const float percentage = id(ha_ceiling_fan_percentage).state; return ( isnan(percentage) || percentage < 50.0f ); then: - homeassistant.action: action: fan.set_percentage data: entity_id: "${ceiling_fan_entity}" percentage: "66" else: # Medium -> High - if: condition: lambda: |- return ( id(ha_ceiling_fan_percentage).state < 85.0f ); then: - homeassistant.action: action: fan.set_percentage data: entity_id: "${ceiling_fan_entity}" percentage: "100" # High -> Off else: - homeassistant.action: action: fan.turn_off data: entity_id: "${ceiling_fan_entity}" - button: id: air_filter_quiet_button x: 272 y: 356 width: 220 height: 100 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x548C6C radius: 18 pressed: bg_color: 0x315843 widgets: - label: align: CENTER text: "Air Filter: Quiet" text_font: font_small text_color: 0xFFFFFF on_click: - homeassistant.action: action: number.set_value data: entity_id: "${air_filter_level_entity}" value: "2" - button: id: climate_night_power_button x: 519 y: 356 width: 220 height: 100 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x80619B radius: 18 pressed: bg_color: 0x5A4778 widgets: - label: align: CENTER text: "Climate: 9pm-12am" text_font: font_small text_color: 0xFFFFFF on_click: - homeassistant.action: action: input_boolean.toggle data: entity_id: "${climate_night_power_entity}" - button: id: sleep_timer_button x: 766 y: 356 width: 220 height: 100 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x80619B radius: 18 pressed: bg_color: 0x6B4A86 widgets: - label: id: sleep_timer_button_label align: CENTER text: "Sleep: 2 hrs (0)" text_font: font_small text_color: 0xFFFFFF on_click: # Pressing an active Sleep timer cancels it. Otherwise, start # a new 120-minute countdown. - if: condition: lambda: |- const auto now = id(ha_time).now(); return ( id(sleep_timer_end_epoch) > 0 && now.is_valid() && now.timestamp < id(sleep_timer_end_epoch) ); then: - lambda: |- id(sleep_timer_end_epoch) = 0; else: - if: condition: lambda: |- return id(ha_time).now().is_valid(); then: - lambda: |- const auto now = id(ha_time).now(); id(sleep_timer_end_epoch) = now.timestamp + (120 * 60); - script.execute: refresh_sleep_timer #:################################################################################:# # BOTTOM NAVIGATION #:################################################################################:# - button: id: climate_back_button x: 612 y: 475 width: 190 height: 92 bg_color: 0x315843 bg_opa: COVER border_width: 2 border_color: 0x548C6C radius: 16 pressed: bg_color: 0x416F55 widgets: - label: align: CENTER text: "Back" text_font: font_button text_color: 0xFFFFFF on_click: - lvgl.page.show: id: bedroom_page animation: MOVE_RIGHT time: 220ms - button: id: climate_clock_button x: 810 y: 475 width: 190 height: 92 bg_color: 0x214F72 bg_opa: COVER border_width: 2 border_color: 0x3E7FA7 radius: 16 pressed: bg_color: 0x2E6A96 widgets: - label: align: CENTER text: "Clock" text_font: font_button text_color: 0xFFFFFF on_click: - lvgl.page.show: id: clock_page animation: FADE_IN time: 250ms - light.turn_on: id: display_backlight brightness: "${clock_brightness}" transition_length: 1500ms #:####################################################################################:# # SECURITY PAGE #:####################################################################################:# - id: security_page skip: true bg_color: 0x0D1218 bg_opa: COVER widgets: #:################################################################################:# # CAMERA SELECTION #:################################################################################:# - button: id: security_doorbell_button x: 25 y: 18 width: 220 height: 70 bg_color: 0x493535 bg_opa: COVER border_width: 2 border_color: 0x9A6666 radius: 16 pressed: bg_color: 0x654949 widgets: - label: align: CENTER text: "Doorbell" text_font: font_button text_color: 0xFFFFFF on_click: - lambda: |- id(security_camera_selection) = 1; - script.execute: refresh_security_camera_buttons - script.execute: refresh_security_camera - button: id: security_driveway_button x: 272 y: 18 width: 220 height: 70 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x9A6666 radius: 16 pressed: bg_color: 0x654949 widgets: - label: align: CENTER text: "Driveway" text_font: font_button text_color: 0xFFFFFF on_click: - lambda: |- id(security_camera_selection) = 2; - script.execute: refresh_security_camera_buttons - script.execute: refresh_security_camera - button: id: security_camera_3_button x: 519 y: 18 width: 220 height: 70 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x445365 radius: 16 widgets: - label: align: CENTER text: "Camera3" text_font: font_button text_color: 0x8D98A6 - button: id: security_camera_4_button x: 766 y: 18 width: 220 height: 70 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x445365 radius: 16 widgets: - label: align: CENTER text: "Camera4" text_font: font_button text_color: 0x8D98A6 #:################################################################################:# # FUTURE SECURITY CONTROLS #:################################################################################:# - button: id: security_future_1_button x: 25 y: 105 width: 290 height: 70 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x445365 radius: 16 widgets: - label: align: CENTER text: "Future1" text_font: font_button text_color: 0x8D98A6 - button: id: security_future_2_button x: 25 y: 190 width: 290 height: 70 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x445365 radius: 16 widgets: - label: align: CENTER text: "Future2" text_font: font_button text_color: 0x8D98A6 - button: id: security_future_3_button x: 25 y: 275 width: 290 height: 70 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x445365 radius: 16 widgets: - label: align: CENTER text: "Future3" text_font: font_button text_color: 0x8D98A6 - button: id: security_future_4_button x: 25 y: 360 width: 290 height: 70 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x445365 radius: 16 widgets: - label: align: CENTER text: "Future4" text_font: font_button text_color: 0x8D98A6 #:################################################################################:# # 640x360 CAMERA IMAGE - RIGHT SIDE #:################################################################################:# - obj: x: 348 y: 98 width: 644 height: 364 bg_color: 0x020304 bg_opa: COVER border_width: 2 border_color: 0x445365 radius: 13 shadow_width: 0 clickable: false - image: id: security_camera_widget x: 350 y: 100 width: 640 height: 360 src: security_camera_image radius: 11 clip_corner: true clickable: false - label: id: security_camera_status_label x: 350 y: 260 width: 640 text: "Waiting for Doorbell camera" text_font: font_button text_align: CENTER text_color: 0xD4D8DE clickable: false #:################################################################################:# # BOTTOM NAVIGATION #:################################################################################:# - button: id: security_back_button x: 612 y: 475 width: 190 height: 92 bg_color: 0x315843 bg_opa: COVER border_width: 2 border_color: 0x548C6C radius: 16 pressed: bg_color: 0x416F55 widgets: - label: align: CENTER text: "Back" text_font: font_button text_color: 0xFFFFFF on_click: - lambda: |- id(security_page_active) = false; - online_image.release: security_camera_image - lvgl.page.show: id: bedroom_page animation: MOVE_RIGHT time: 220ms - button: id: security_clock_button x: 810 y: 475 width: 190 height: 92 bg_color: 0x214F72 bg_opa: COVER border_width: 2 border_color: 0x3E7FA7 radius: 16 pressed: bg_color: 0x2E6A96 widgets: - label: align: CENTER text: "Clock" text_font: font_button text_color: 0xFFFFFF on_click: - lambda: |- id(security_page_active) = false; - online_image.release: security_camera_image - lvgl.page.show: id: clock_page animation: FADE_IN time: 250ms - light.turn_on: id: display_backlight brightness: "${clock_brightness}" transition_length: 1500ms #:####################################################################################:# # CLOCK PAGE #:####################################################################################:# - id: clock_page skip: true bg_color: 0x020304 bg_opa: COVER widgets: - button: id: clock_wake_button x: 0 y: 0 width: 1024 height: 600 bg_opa: TRANSP border_width: 0 shadow_width: 0 radius: 0 text: "" on_click: then: - lvgl.page.show: id: bedroom_page animation: FADE_OUT time: 250ms - light.turn_on: id: display_backlight brightness: "${control_brightness}" transition_length: 500ms - label: id: clock_time_label align: TOP_MID y: -5 text: "--:--" text_font: font_clock text_color: 0xE4DDD2 clickable: false - label: id: clock_date_label align: TOP_MID y: 315 text: "Waiting for time" text_font: font_date text_color: 0xA8A198 clickable: false # Four status readings - label: x: 80 y: 455 width: 205 text: "Moon" text_font: font_small text_align: CENTER text_color: 0xA9A0D4 clickable: false - label: id: clock_moon_value_label x: 80 y: 490 width: 205 text: "--.-%" text_font: font_title text_align: CENTER text_color: 0xC6BDEC clickable: false - label: x: 285 y: 455 width: 205 text: "Outdoor" text_font: font_small text_align: CENTER text_color: 0x84B5D8 clickable: false - label: id: clock_outdoor_value_label x: 285 y: 490 width: 205 text: "--.-°C" text_font: font_title text_align: CENTER text_color: 0x9CCBEA clickable: false - label: x: 490 y: 455 width: 205 text: "Indoor" text_font: font_small text_align: CENTER text_color: 0xD6B78F clickable: false - label: id: clock_temperature_label x: 490 y: 490 width: 205 text: "--.-°C" text_font: font_title text_align: CENTER text_color: 0xE8C79D clickable: false - label: x: 695 y: 455 width: 245 text: "Humidity" text_font: font_small text_align: CENTER text_color: 0x80BDA4 clickable: false - label: id: clock_humidity_label x: 695 y: 490 width: 245 text: "--%" text_font: font_title text_align: CENTER text_color: 0x97D2B7 clickable: false - label: id: clock_status_label align: BOTTOM_MID y: -8 text: "" text_font: font_small text_color: 0x887799 clickable: false