#:########################################################################################:# # 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.34 2026-07-19 Added three ambient-light ranges and periodic backlight rechecking # V1.33 2026-07-19 Raised the default night lux threshold after TEMT6000 testing # V1.32 2026-07-19 Added persistent ambient-light thresholds, brightness levels and idle delays # V1.31 2026-07-19 Added TEMT6000 ambient-light voltage and illuminance sensors # V1.30 2026-07-18 Made Downstairs Guests red when occupied and removed duplicate OTA config # V1.29 2026-07-18 Perfected Atto charging alignment and expanded Security states # V1.28 2026-07-18 Restyled Atto Charging as top-row data with active red background # V1.27 2026-07-18 Added Open-Meteo rain sensors and rearranged Atto status/controls # V1.26 2026-07-18 Added Bedtime icon, snappier Security navigation and Atto text refinements # V1.25 2026-07-18 Reduced Jobs button line spacing to a balanced gap # V1.24 2026-07-18 Enlarged and separated Jobs button text # V1.23 2026-07-18 Centred Jobs text and used direct Jobs Tracker completion script # V1.22 2026-07-18 Added 15-slot Jobs page with status colours and hold completion # V1.21 2026-07-18 Replaced bottom navigation text with icons and live time # V1.20 2026-07-18 Swapped Bedtime/Jobs, defaulted Doorbell and refined Climate icons # V1.18 2026-07-18 Added Lounge Fire, six Security cameras, Jobs page and weather/tide header # V1.17 2026-07-18 Swapped Bedroom Climate/Atto buttons and removed Garage test logs # V1.16 2026-07-17 Removed garage-door influence from Garage Lights button state # 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 # - TEMT6000 ambient-light sensor signal: GPIO20 / ADC1 #:########################################################################################:# # 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. # - Compact Bedtime control is positioned beside the API connection indicator. # - The bottom navigation row contains Jobs Summary, Atto, Climate, Security and Clock. # - Jobs Summary opens a reserved Jobs page with Back and Clock navigation. # - The Security page supports six selectable camera views. # - 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. # - TEMT6000 voltage and approximate lux are exposed for calibration. # - Ambient light is divided into Dark, Normal and Bright ranges. # - Touch immediately restores the active brightness for the current range. # - The current page dims after a configurable idle delay. # - A second configurable delay changes to the large Clock page. # - Clock/idle brightness is rechecked periodically as ambient light changes. # - Calibration, thresholds, brightness and delay values persist across reboot. #:########################################################################################:# # 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. Three-range adaptive touch and clock brightness with periodic ambient rechecking. (Layout V1.1)" device_area: "Bedroom" # Project Naming project_name: "Guition.JC1060P470C_I_W_Y" project_version: "v1.34" # 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" # Ambient Light Sensor Settings # TEMT6000: VCC=3V3, GND=GND, SIG/OUT=GPIO20. ambient_light_pin: "GPIO20" ambient_light_update_interval: "1s" # Touchscreen Settings touch_sda_pin: "GPIO7" touch_scl_pin: "GPIO8" touch_interrupt_pin: "GPIO21" touch_reset_pin: "GPIO22" # Adaptive Display Defaults # # These initialise the persistent ESPHome number entities on first install or # after flash preferences are erased. Once adjusted through Home Assistant, # restored values take precedence after reboot. # # Observed TEMT6000 values before installation: # Very dark: approximately 6-12 lx # Bright light: approximately 642-643 lx ambient_lux_multiplier_default: "200" display_dark_lux_threshold_default: "20" display_bright_lux_threshold_default: "250" display_dark_touch_brightness_default: "25" display_normal_touch_brightness_default: "80" display_bright_touch_brightness_default: "95" display_dark_clock_brightness_default: "4" display_normal_clock_brightness_default: "15" display_bright_clock_brightness_default: "60" display_dim_delay_default: "30" display_clock_delay_default: "60" # The ADC samples every second and publishes a filtered reading about every # three seconds. This controls how often the current screen brightness is # re-evaluated against the latest lux value. adaptive_backlight_recheck_interval: "10s" # 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_front_door_camera_entity: "camera.camera2" security_driveway_camera_entity: "camera.camera5" security_garage_camera_entity: "camera.camera6" security_caravan_camera_entity: "camera.camera4" security_bike_shed_camera_entity: "camera.trailer" # Home Assistant Entities - Status / Environment local_tides_entity: "sensor.local_tides" rain_today_entity: "sensor.open_meteo_rain_chance_today" rain_tomorrow_entity: "sensor.open_meteo_rain_chance_tomorrow" 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" jobs_summary_entity: "sensor.jobs_tracker_summary" # 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 quiet_time_entity: "input_boolean.quiet_time" # Home Assistant Entities - Actions bedtime_button_entity: "input_button.bedtime_mode" # Home Assistant Entities - Climate lounge_fire_entity: "switch.rinnai_neo_gas_heater_heater_operation" lounge_fire_status_entity: "sensor.rinnai_neo_gas_heater_rinnai_neo_gas_heater_status_text" 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" # Home Assistant Entities - Security Controls downstairs_guests_occupied_entity: "input_boolean.downstairs_flat_occupied" garage_door_contact_entity: "binary_sensor.garage_roller_door_x18rs_contact" external_garage_light_entity: "switch.esp_garageextcornerlight_garage_external_light" # Home Assistant Entities - Jobs Tracker # Slots 1-5 are currently active. The page reserves 15 positions in total. job_1_state_entity: "sensor.jobs_tracker_dog_feeding_morning" job_1_id: "dog_feeding_morning" job_2_state_entity: "sensor.jobs_tracker_dog_feeding_evening" job_2_id: "dog_feeding_evening" job_3_state_entity: "sensor.jobs_tracker_dog_depooping" job_3_id: "dog_depooping" job_4_state_entity: "sensor.jobs_tracker_dishwasher_unpacked" job_4_id: "dishwasher_unpacked" job_5_state_entity: "sensor.jobs_tracker_rubbish_removed" job_5_id: "rubbish_removed" #:########################################################################################:# # 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 - script.execute: refresh_navigation_time - script.execute: refresh_jobs_page #:########################################################################################:# # 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 #:########################################################################################:# # 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 - script.execute: refresh_navigation_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 # Brighten immediately on the first touch event. on_touch: then: - script.execute: apply_active_backlight #:########################################################################################:# # 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, .:/-%°+()" # Compact headings and long summary values in the status strips. - file: "gfonts://Roboto" id: font_header size: 14 glyphs: - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789, .:/-%°+()" # Jobs-page job names. - file: type: gfonts family: Roboto weight: 500 id: font_job_name size: 22 bpp: 4 glyphs: - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789, .:/-%°+()" # Jobs-page status and relative-time line. - file: type: gfonts family: Roboto weight: 400 id: font_job_status size: 18 bpp: 4 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 - "\U000F014E" # mdi-clipboard-check # Large bottom-navigation icons. - file: "https://raw.githubusercontent.com/Templarian/MaterialDesign-Webfont/master/fonts/materialdesignicons-webfont.ttf" id: font_mdi_navigation size: 50 bpp: 4 glyphs: - "\U000F004D" # mdi-arrow-left - "\U000F07A8" # mdi-car-estate - "\U000F050F" # mdi-thermometer - "\U000F068A" # mdi-shield-home - "\U000F04B2" # mdi-sleep # Live 24-hour time shown in bottom-right clock buttons. - file: type: gfonts family: Roboto weight: 500 id: font_navigation_time size: 42 bpp: 4 glyphs: - "0123456789:" # 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 # Compact Climate-page icons. - file: "https://raw.githubusercontent.com/Templarian/MaterialDesign-Webfont/master/fonts/materialdesignicons-webfont.ttf" id: font_mdi_climate size: 30 bpp: 4 glyphs: - "\U000F0425" # mdi-power - "\U000F0238" # mdi-fire - "\U000F0717" # mdi-snowflake - "\U000F0E0A" # mdi-water-outline - "\U000F032A" # mdi-leaf - "\U000F04C5" # mdi-speedometer - "\U000F0768" # mdi-atom - "\U000F1797" # mdi-ceiling-fan - "\U000F0D43" # mdi-air-filter # 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 .:/-%°()" #:########################################################################################:# # NUMBERS: # Persistent ambient-light and display settings #:########################################################################################:# # # These appear in Home Assistant as configuration entities and are restored # from flash after reboot. Brightness values are percentages. # number: - platform: template id: ambient_lux_multiplier name: "${friendly_name} Ambient Lux Multiplier" icon: mdi:tune-vertical entity_category: config optimistic: true restore_value: true initial_value: "${ambient_lux_multiplier_default}" min_value: 25 max_value: 1000 step: 5 mode: box - platform: template id: display_dark_lux_threshold name: "${friendly_name} Dark Lux Threshold" icon: mdi:weather-night entity_category: config optimistic: true restore_value: true initial_value: "${display_dark_lux_threshold_default}" unit_of_measurement: "lx" min_value: 0 max_value: 200 step: 1 mode: box - platform: template id: display_bright_lux_threshold name: "${friendly_name} Bright Lux Threshold" icon: mdi:white-balance-sunny entity_category: config optimistic: true restore_value: true initial_value: "${display_bright_lux_threshold_default}" unit_of_measurement: "lx" min_value: 25 max_value: 1000 step: 5 mode: box - platform: template id: display_dark_touch_brightness name: "${friendly_name} Dark Touch Brightness" icon: mdi:brightness-3 entity_category: config optimistic: true restore_value: true initial_value: "${display_dark_touch_brightness_default}" unit_of_measurement: "%" min_value: 2 max_value: 60 step: 1 mode: slider - platform: template id: display_normal_touch_brightness name: "${friendly_name} Normal Touch Brightness" icon: mdi:brightness-6 entity_category: config optimistic: true restore_value: true initial_value: "${display_normal_touch_brightness_default}" unit_of_measurement: "%" min_value: 5 max_value: 100 step: 1 mode: slider - platform: template id: display_bright_touch_brightness name: "${friendly_name} Bright Touch Brightness" icon: mdi:brightness-7 entity_category: config optimistic: true restore_value: true initial_value: "${display_bright_touch_brightness_default}" unit_of_measurement: "%" min_value: 10 max_value: 100 step: 1 mode: slider - platform: template id: display_dark_clock_brightness name: "${friendly_name} Dark Clock Brightness" icon: mdi:clock-outline entity_category: config optimistic: true restore_value: true initial_value: "${display_dark_clock_brightness_default}" unit_of_measurement: "%" min_value: 1 max_value: 30 step: 1 mode: slider - platform: template id: display_normal_clock_brightness name: "${friendly_name} Normal Clock Brightness" icon: mdi:clock-outline entity_category: config optimistic: true restore_value: true initial_value: "${display_normal_clock_brightness_default}" unit_of_measurement: "%" min_value: 1 max_value: 60 step: 1 mode: slider - platform: template id: display_bright_clock_brightness name: "${friendly_name} Bright Clock Brightness" icon: mdi:clock-outline entity_category: config optimistic: true restore_value: true initial_value: "${display_bright_clock_brightness_default}" unit_of_measurement: "%" min_value: 5 max_value: 100 step: 1 mode: slider - platform: template id: display_dim_delay name: "${friendly_name} Dim Delay" icon: mdi:timer-sand entity_category: config optimistic: true restore_value: true initial_value: "${display_dim_delay_default}" unit_of_measurement: "s" min_value: 10 max_value: 600 step: 5 mode: box - platform: template id: display_clock_delay name: "${friendly_name} Clock Page Delay" icon: mdi:timer-outline entity_category: config optimistic: true restore_value: true initial_value: "${display_clock_delay_default}" unit_of_measurement: "s" min_value: 15 max_value: 900 step: 5 mode: box #:########################################################################################:# # 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}" #:######################################################################################:# # TEMT6000 AMBIENT LIGHT SENSOR #:######################################################################################:# # # The ADC sensor exposes the measured SIG/OUT voltage for calibration. # The illuminance sensor uses the standard TEMT6000 breakout approximation: # # lux = voltage x configurable multiplier # # Calibration, thresholds, brightness and delay values are persistent ESPHome # number entities and can be adjusted from Home Assistant. # # GPIO20 is sampled every second. The median filter publishes every third # sample, and the template lux value updates every three seconds. - platform: adc id: ambient_light_voltage name: "${friendly_name} Ambient Light Voltage" pin: "${ambient_light_pin}" attenuation: auto update_interval: "${ambient_light_update_interval}" accuracy_decimals: 3 icon: mdi:sine-wave filters: - median: window_size: 7 send_every: 3 send_first_at: 1 - platform: template id: ambient_light_illuminance name: "${friendly_name} Ambient Light" device_class: illuminance state_class: measurement unit_of_measurement: "lx" accuracy_decimals: 0 icon: mdi:brightness-6 update_interval: 3s lambda: |- const float voltage = id(ambient_light_voltage).state; float multiplier = id(ambient_lux_multiplier).state; if (isnan(voltage)) { return NAN; } if (isnan(multiplier) || multiplier <= 0.0f) { multiplier = 200.0f; } return voltage * multiplier; - 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] - platform: homeassistant id: rain_today entity_id: "${rain_today_entity}" internal: true on_value: then: - script.execute: refresh_header_status - platform: homeassistant id: rain_tomorrow entity_id: "${rain_tomorrow_entity}" internal: true on_value: then: - script.execute: refresh_header_status # 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 #:######################################################################################:# # JOBS TRACKER NUMERIC ATTRIBUTES #:######################################################################################:# - platform: homeassistant id: job_1_hours_since_completed entity_id: "${job_1_state_entity}" attribute: hours_since_completed internal: true on_value: then: - script.execute: refresh_jobs_page - platform: homeassistant id: job_1_hours_since_required entity_id: "${job_1_state_entity}" attribute: hours_since_required internal: true on_value: then: - script.execute: refresh_jobs_page - platform: homeassistant id: job_1_display_due_ts entity_id: "${job_1_state_entity}" attribute: display_due_ts internal: true on_value: then: - script.execute: refresh_jobs_page - platform: homeassistant id: job_2_hours_since_completed entity_id: "${job_2_state_entity}" attribute: hours_since_completed internal: true on_value: then: - script.execute: refresh_jobs_page - platform: homeassistant id: job_2_hours_since_required entity_id: "${job_2_state_entity}" attribute: hours_since_required internal: true on_value: then: - script.execute: refresh_jobs_page - platform: homeassistant id: job_2_display_due_ts entity_id: "${job_2_state_entity}" attribute: display_due_ts internal: true on_value: then: - script.execute: refresh_jobs_page - platform: homeassistant id: job_3_hours_since_completed entity_id: "${job_3_state_entity}" attribute: hours_since_completed internal: true on_value: then: - script.execute: refresh_jobs_page - platform: homeassistant id: job_3_hours_since_required entity_id: "${job_3_state_entity}" attribute: hours_since_required internal: true on_value: then: - script.execute: refresh_jobs_page - platform: homeassistant id: job_3_display_due_ts entity_id: "${job_3_state_entity}" attribute: display_due_ts internal: true on_value: then: - script.execute: refresh_jobs_page - platform: homeassistant id: job_4_hours_since_completed entity_id: "${job_4_state_entity}" attribute: hours_since_completed internal: true on_value: then: - script.execute: refresh_jobs_page - platform: homeassistant id: job_4_hours_since_required entity_id: "${job_4_state_entity}" attribute: hours_since_required internal: true on_value: then: - script.execute: refresh_jobs_page - platform: homeassistant id: job_4_display_due_ts entity_id: "${job_4_state_entity}" attribute: display_due_ts internal: true on_value: then: - script.execute: refresh_jobs_page - platform: homeassistant id: job_5_hours_since_completed entity_id: "${job_5_state_entity}" attribute: hours_since_completed internal: true on_value: then: - script.execute: refresh_jobs_page - platform: homeassistant id: job_5_hours_since_required entity_id: "${job_5_state_entity}" attribute: hours_since_required internal: true on_value: then: - script.execute: refresh_jobs_page - platform: homeassistant id: job_5_display_due_ts entity_id: "${job_5_state_entity}" attribute: display_due_ts internal: true on_value: then: - script.execute: refresh_jobs_page #:########################################################################################:# # 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_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_downstairs_guests_occupied entity_id: "${downstairs_guests_occupied_entity}" internal: true on_state: then: - script.execute: refresh_security_controls - platform: homeassistant id: ha_garage_door_open entity_id: "${garage_door_contact_entity}" internal: true on_state: then: - script.execute: refresh_security_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 while the screen is using idle/clock brightness. - id: display_is_dimmed type: bool restore_value: false initial_value: "false" # True only while the Security page is visible. - id: security_page_active type: bool restore_value: false initial_value: "false" # 1 = Doorbell, 2 = Front Door, 3 = Driveway, 4 = Garage, # 5 = Caravan, 6 = Bike Shed. - id: security_camera_selection type: int restore_value: false initial_value: "1" #:########################################################################################:# # SCRIPTS: # Refresh all state-dependent LVGL button colours #:########################################################################################:# script: #:######################################################################################:# # ADAPTIVE DISPLAY BACKLIGHT #:######################################################################################:# # # Three ambient ranges are used: # Dark: lux <= Dark Lux Threshold # Normal: between the two thresholds # Bright: lux >= Bright Lux Threshold # # The active and clock/idle brightness levels are independently adjustable # and restored after reboot. - id: apply_active_backlight mode: restart then: - lambda: |- id(display_is_dimmed) = false; - light.turn_on: id: display_backlight brightness: !lambda |- const float lux = id(ambient_light_illuminance).state; float dark_threshold = id(display_dark_lux_threshold).state; float bright_threshold = id(display_bright_lux_threshold).state; if (isnan(dark_threshold)) { dark_threshold = 20.0f; } if ( isnan(bright_threshold) || bright_threshold <= dark_threshold ) { bright_threshold = 250.0f; } float brightness_percent; if (isnan(lux) || lux <= dark_threshold) { brightness_percent = id(display_dark_touch_brightness).state; id(ambient_light_band_text).publish_state("Dark"); } else if (lux >= bright_threshold) { brightness_percent = id(display_bright_touch_brightness).state; id(ambient_light_band_text).publish_state("Bright"); } else { brightness_percent = id(display_normal_touch_brightness).state; id(ambient_light_band_text).publish_state("Normal"); } if (isnan(brightness_percent)) { brightness_percent = 25.0f; } brightness_percent = std::max(1.0f, std::min(100.0f, brightness_percent)); return brightness_percent / 100.0f; transition_length: 150ms - id: apply_dim_backlight mode: restart then: - lambda: |- id(display_is_dimmed) = true; - light.turn_on: id: display_backlight brightness: !lambda |- const float lux = id(ambient_light_illuminance).state; float dark_threshold = id(display_dark_lux_threshold).state; float bright_threshold = id(display_bright_lux_threshold).state; if (isnan(dark_threshold)) { dark_threshold = 20.0f; } if ( isnan(bright_threshold) || bright_threshold <= dark_threshold ) { bright_threshold = 250.0f; } float brightness_percent; if (isnan(lux) || lux <= dark_threshold) { brightness_percent = id(display_dark_clock_brightness).state; id(ambient_light_band_text).publish_state("Dark"); } else if (lux >= bright_threshold) { brightness_percent = id(display_bright_clock_brightness).state; id(ambient_light_band_text).publish_state("Bright"); } else { brightness_percent = id(display_normal_clock_brightness).state; id(ambient_light_band_text).publish_state("Normal"); } if (isnan(brightness_percent)) { brightness_percent = 4.0f; } brightness_percent = std::max(1.0f, std::min(100.0f, brightness_percent)); return brightness_percent / 100.0f; transition_length: 700ms #:######################################################################################:# # BOTTOM NAVIGATION LIVE TIME #:######################################################################################:# - id: refresh_navigation_time mode: restart then: - if: condition: lambda: |- return ( id(lvgl_ready) && id(ha_time).now().is_valid() ); then: - lvgl.label.update: id: clock_now_button_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: jobs_clock_button_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: vehicles_clock_button_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: climate_clock_button_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: security_clock_button_label text: !lambda |- auto now = id(ha_time).now(); char buffer[8]; now.strftime(buffer, sizeof(buffer), "%H:%M"); return std::string(buffer); #:######################################################################################:# # JOBS PAGE STATE REFRESH #:######################################################################################:# - id: refresh_jobs_page mode: restart then: - if: condition: lambda: |- return id(lvgl_ready); then: - lvgl.widget.update: id: job_1_button bg_color: !lambda |- const auto state = id(job_1_state).state; if (state == "Complete") { return lv_color_hex(0x2E7D32); } if (state == "Not Due") { return lv_color_hex(0x7E8995); } if (state == "Due") { return lv_color_hex(0xB08A18); } if (state == "Overdue") { return lv_color_hex(0xC66A1B); } if (state == "Missed") { return lv_color_hex(0xB53636); } return lv_color_hex(0x28313D); - lvgl.label.update: id: job_1_status_label text: !lambda |- const auto state = id(job_1_state).state; if (state == "Complete") { const float raw_hours = id(job_1_hours_since_completed).state; if (isnan(raw_hours) || raw_hours < 0.0f) { return std::string("Complete"); } const int hours = (int) lroundf(raw_hours); if (hours <= 0) { return std::string("Complete - Just now"); } char buffer[48]; if (hours < 24) { snprintf( buffer, sizeof(buffer), hours == 1 ? "Complete - 1 hr ago" : "Complete - %d hrs ago", hours ); } else { const int days = (hours + 12) / 24; snprintf( buffer, sizeof(buffer), days == 1 ? "Complete - 1 day ago" : "Complete - %d days ago", days ); } return std::string(buffer); } if (state == "Not Due") { const float due_ts = id(job_1_display_due_ts).state; const auto now = id(ha_time).now(); if ( isnan(due_ts) || due_ts <= 0.0f || !now.is_valid() ) { return std::string("Not Due"); } const int64_t seconds_remaining = (int64_t) due_ts - (int64_t) now.timestamp; if (seconds_remaining <= 0) { return std::string("Not Due - Soon"); } const int hours = (int) ((seconds_remaining + 3599) / 3600); char buffer[48]; if (hours < 24) { snprintf( buffer, sizeof(buffer), hours == 1 ? "Not Due - In 1 hr" : "Not Due - In %d hrs", hours ); } else { const int days = (hours + 23) / 24; snprintf( buffer, sizeof(buffer), days == 1 ? "Not Due - In 1 day" : "Not Due - In %d days", days ); } return std::string(buffer); } if (state == "Due") { const float raw_hours = id(job_1_hours_since_required).state; if (isnan(raw_hours) || raw_hours <= 0.0f) { return std::string("Due - Just now"); } const int hours = (int) lroundf(raw_hours); char buffer[48]; if (hours < 24) { snprintf( buffer, sizeof(buffer), hours == 1 ? "Due - 1 hr ago" : "Due - %d hrs ago", hours ); } else { const int days = (hours + 12) / 24; snprintf( buffer, sizeof(buffer), days == 1 ? "Due - 1 day ago" : "Due - %d days ago", days ); } return std::string(buffer); } if (state == "Overdue") { const float raw_hours = id(job_1_hours_since_required).state; if (isnan(raw_hours) || raw_hours <= 0.0f) { return std::string("Overdue"); } const int hours = (int) lroundf(raw_hours); char buffer[48]; if (hours < 24) { snprintf( buffer, sizeof(buffer), hours == 1 ? "Overdue - 1 hr" : "Overdue - %d hrs", hours ); } else { const int days = (hours + 12) / 24; snprintf( buffer, sizeof(buffer), days == 1 ? "Overdue - 1 day" : "Overdue - %d days", days ); } return std::string(buffer); } if (state == "Missed") { return std::string("Missed"); } if ( state.empty() || state == "unknown" || state == "unavailable" ) { return std::string("Unknown"); } return state; - lvgl.widget.update: id: job_2_button bg_color: !lambda |- const auto state = id(job_2_state).state; if (state == "Complete") { return lv_color_hex(0x2E7D32); } if (state == "Not Due") { return lv_color_hex(0x7E8995); } if (state == "Due") { return lv_color_hex(0xB08A18); } if (state == "Overdue") { return lv_color_hex(0xC66A1B); } if (state == "Missed") { return lv_color_hex(0xB53636); } return lv_color_hex(0x28313D); - lvgl.label.update: id: job_2_status_label text: !lambda |- const auto state = id(job_2_state).state; if (state == "Complete") { const float raw_hours = id(job_2_hours_since_completed).state; if (isnan(raw_hours) || raw_hours < 0.0f) { return std::string("Complete"); } const int hours = (int) lroundf(raw_hours); if (hours <= 0) { return std::string("Complete - Just now"); } char buffer[48]; if (hours < 24) { snprintf( buffer, sizeof(buffer), hours == 1 ? "Complete - 1 hr ago" : "Complete - %d hrs ago", hours ); } else { const int days = (hours + 12) / 24; snprintf( buffer, sizeof(buffer), days == 1 ? "Complete - 1 day ago" : "Complete - %d days ago", days ); } return std::string(buffer); } if (state == "Not Due") { const float due_ts = id(job_2_display_due_ts).state; const auto now = id(ha_time).now(); if ( isnan(due_ts) || due_ts <= 0.0f || !now.is_valid() ) { return std::string("Not Due"); } const int64_t seconds_remaining = (int64_t) due_ts - (int64_t) now.timestamp; if (seconds_remaining <= 0) { return std::string("Not Due - Soon"); } const int hours = (int) ((seconds_remaining + 3599) / 3600); char buffer[48]; if (hours < 24) { snprintf( buffer, sizeof(buffer), hours == 1 ? "Not Due - In 1 hr" : "Not Due - In %d hrs", hours ); } else { const int days = (hours + 23) / 24; snprintf( buffer, sizeof(buffer), days == 1 ? "Not Due - In 1 day" : "Not Due - In %d days", days ); } return std::string(buffer); } if (state == "Due") { const float raw_hours = id(job_2_hours_since_required).state; if (isnan(raw_hours) || raw_hours <= 0.0f) { return std::string("Due - Just now"); } const int hours = (int) lroundf(raw_hours); char buffer[48]; if (hours < 24) { snprintf( buffer, sizeof(buffer), hours == 1 ? "Due - 1 hr ago" : "Due - %d hrs ago", hours ); } else { const int days = (hours + 12) / 24; snprintf( buffer, sizeof(buffer), days == 1 ? "Due - 1 day ago" : "Due - %d days ago", days ); } return std::string(buffer); } if (state == "Overdue") { const float raw_hours = id(job_2_hours_since_required).state; if (isnan(raw_hours) || raw_hours <= 0.0f) { return std::string("Overdue"); } const int hours = (int) lroundf(raw_hours); char buffer[48]; if (hours < 24) { snprintf( buffer, sizeof(buffer), hours == 1 ? "Overdue - 1 hr" : "Overdue - %d hrs", hours ); } else { const int days = (hours + 12) / 24; snprintf( buffer, sizeof(buffer), days == 1 ? "Overdue - 1 day" : "Overdue - %d days", days ); } return std::string(buffer); } if (state == "Missed") { return std::string("Missed"); } if ( state.empty() || state == "unknown" || state == "unavailable" ) { return std::string("Unknown"); } return state; - lvgl.widget.update: id: job_3_button bg_color: !lambda |- const auto state = id(job_3_state).state; if (state == "Complete") { return lv_color_hex(0x2E7D32); } if (state == "Not Due") { return lv_color_hex(0x7E8995); } if (state == "Due") { return lv_color_hex(0xB08A18); } if (state == "Overdue") { return lv_color_hex(0xC66A1B); } if (state == "Missed") { return lv_color_hex(0xB53636); } return lv_color_hex(0x28313D); - lvgl.label.update: id: job_3_status_label text: !lambda |- const auto state = id(job_3_state).state; if (state == "Complete") { const float raw_hours = id(job_3_hours_since_completed).state; if (isnan(raw_hours) || raw_hours < 0.0f) { return std::string("Complete"); } const int hours = (int) lroundf(raw_hours); if (hours <= 0) { return std::string("Complete - Just now"); } char buffer[48]; if (hours < 24) { snprintf( buffer, sizeof(buffer), hours == 1 ? "Complete - 1 hr ago" : "Complete - %d hrs ago", hours ); } else { const int days = (hours + 12) / 24; snprintf( buffer, sizeof(buffer), days == 1 ? "Complete - 1 day ago" : "Complete - %d days ago", days ); } return std::string(buffer); } if (state == "Not Due") { const float due_ts = id(job_3_display_due_ts).state; const auto now = id(ha_time).now(); if ( isnan(due_ts) || due_ts <= 0.0f || !now.is_valid() ) { return std::string("Not Due"); } const int64_t seconds_remaining = (int64_t) due_ts - (int64_t) now.timestamp; if (seconds_remaining <= 0) { return std::string("Not Due - Soon"); } const int hours = (int) ((seconds_remaining + 3599) / 3600); char buffer[48]; if (hours < 24) { snprintf( buffer, sizeof(buffer), hours == 1 ? "Not Due - In 1 hr" : "Not Due - In %d hrs", hours ); } else { const int days = (hours + 23) / 24; snprintf( buffer, sizeof(buffer), days == 1 ? "Not Due - In 1 day" : "Not Due - In %d days", days ); } return std::string(buffer); } if (state == "Due") { const float raw_hours = id(job_3_hours_since_required).state; if (isnan(raw_hours) || raw_hours <= 0.0f) { return std::string("Due - Just now"); } const int hours = (int) lroundf(raw_hours); char buffer[48]; if (hours < 24) { snprintf( buffer, sizeof(buffer), hours == 1 ? "Due - 1 hr ago" : "Due - %d hrs ago", hours ); } else { const int days = (hours + 12) / 24; snprintf( buffer, sizeof(buffer), days == 1 ? "Due - 1 day ago" : "Due - %d days ago", days ); } return std::string(buffer); } if (state == "Overdue") { const float raw_hours = id(job_3_hours_since_required).state; if (isnan(raw_hours) || raw_hours <= 0.0f) { return std::string("Overdue"); } const int hours = (int) lroundf(raw_hours); char buffer[48]; if (hours < 24) { snprintf( buffer, sizeof(buffer), hours == 1 ? "Overdue - 1 hr" : "Overdue - %d hrs", hours ); } else { const int days = (hours + 12) / 24; snprintf( buffer, sizeof(buffer), days == 1 ? "Overdue - 1 day" : "Overdue - %d days", days ); } return std::string(buffer); } if (state == "Missed") { return std::string("Missed"); } if ( state.empty() || state == "unknown" || state == "unavailable" ) { return std::string("Unknown"); } return state; - lvgl.widget.update: id: job_4_button bg_color: !lambda |- const auto state = id(job_4_state).state; if (state == "Complete") { return lv_color_hex(0x2E7D32); } if (state == "Not Due") { return lv_color_hex(0x7E8995); } if (state == "Due") { return lv_color_hex(0xB08A18); } if (state == "Overdue") { return lv_color_hex(0xC66A1B); } if (state == "Missed") { return lv_color_hex(0xB53636); } return lv_color_hex(0x28313D); - lvgl.label.update: id: job_4_status_label text: !lambda |- const auto state = id(job_4_state).state; if (state == "Complete") { const float raw_hours = id(job_4_hours_since_completed).state; if (isnan(raw_hours) || raw_hours < 0.0f) { return std::string("Complete"); } const int hours = (int) lroundf(raw_hours); if (hours <= 0) { return std::string("Complete - Just now"); } char buffer[48]; if (hours < 24) { snprintf( buffer, sizeof(buffer), hours == 1 ? "Complete - 1 hr ago" : "Complete - %d hrs ago", hours ); } else { const int days = (hours + 12) / 24; snprintf( buffer, sizeof(buffer), days == 1 ? "Complete - 1 day ago" : "Complete - %d days ago", days ); } return std::string(buffer); } if (state == "Not Due") { const float due_ts = id(job_4_display_due_ts).state; const auto now = id(ha_time).now(); if ( isnan(due_ts) || due_ts <= 0.0f || !now.is_valid() ) { return std::string("Not Due"); } const int64_t seconds_remaining = (int64_t) due_ts - (int64_t) now.timestamp; if (seconds_remaining <= 0) { return std::string("Not Due - Soon"); } const int hours = (int) ((seconds_remaining + 3599) / 3600); char buffer[48]; if (hours < 24) { snprintf( buffer, sizeof(buffer), hours == 1 ? "Not Due - In 1 hr" : "Not Due - In %d hrs", hours ); } else { const int days = (hours + 23) / 24; snprintf( buffer, sizeof(buffer), days == 1 ? "Not Due - In 1 day" : "Not Due - In %d days", days ); } return std::string(buffer); } if (state == "Due") { const float raw_hours = id(job_4_hours_since_required).state; if (isnan(raw_hours) || raw_hours <= 0.0f) { return std::string("Due - Just now"); } const int hours = (int) lroundf(raw_hours); char buffer[48]; if (hours < 24) { snprintf( buffer, sizeof(buffer), hours == 1 ? "Due - 1 hr ago" : "Due - %d hrs ago", hours ); } else { const int days = (hours + 12) / 24; snprintf( buffer, sizeof(buffer), days == 1 ? "Due - 1 day ago" : "Due - %d days ago", days ); } return std::string(buffer); } if (state == "Overdue") { const float raw_hours = id(job_4_hours_since_required).state; if (isnan(raw_hours) || raw_hours <= 0.0f) { return std::string("Overdue"); } const int hours = (int) lroundf(raw_hours); char buffer[48]; if (hours < 24) { snprintf( buffer, sizeof(buffer), hours == 1 ? "Overdue - 1 hr" : "Overdue - %d hrs", hours ); } else { const int days = (hours + 12) / 24; snprintf( buffer, sizeof(buffer), days == 1 ? "Overdue - 1 day" : "Overdue - %d days", days ); } return std::string(buffer); } if (state == "Missed") { return std::string("Missed"); } if ( state.empty() || state == "unknown" || state == "unavailable" ) { return std::string("Unknown"); } return state; - lvgl.widget.update: id: job_5_button bg_color: !lambda |- const auto state = id(job_5_state).state; if (state == "Complete") { return lv_color_hex(0x2E7D32); } if (state == "Not Due") { return lv_color_hex(0x7E8995); } if (state == "Due") { return lv_color_hex(0xB08A18); } if (state == "Overdue") { return lv_color_hex(0xC66A1B); } if (state == "Missed") { return lv_color_hex(0xB53636); } return lv_color_hex(0x28313D); - lvgl.label.update: id: job_5_status_label text: !lambda |- const auto state = id(job_5_state).state; if (state == "Complete") { const float raw_hours = id(job_5_hours_since_completed).state; if (isnan(raw_hours) || raw_hours < 0.0f) { return std::string("Complete"); } const int hours = (int) lroundf(raw_hours); if (hours <= 0) { return std::string("Complete - Just now"); } char buffer[48]; if (hours < 24) { snprintf( buffer, sizeof(buffer), hours == 1 ? "Complete - 1 hr ago" : "Complete - %d hrs ago", hours ); } else { const int days = (hours + 12) / 24; snprintf( buffer, sizeof(buffer), days == 1 ? "Complete - 1 day ago" : "Complete - %d days ago", days ); } return std::string(buffer); } if (state == "Not Due") { const float due_ts = id(job_5_display_due_ts).state; const auto now = id(ha_time).now(); if ( isnan(due_ts) || due_ts <= 0.0f || !now.is_valid() ) { return std::string("Not Due"); } const int64_t seconds_remaining = (int64_t) due_ts - (int64_t) now.timestamp; if (seconds_remaining <= 0) { return std::string("Not Due - Soon"); } const int hours = (int) ((seconds_remaining + 3599) / 3600); char buffer[48]; if (hours < 24) { snprintf( buffer, sizeof(buffer), hours == 1 ? "Not Due - In 1 hr" : "Not Due - In %d hrs", hours ); } else { const int days = (hours + 23) / 24; snprintf( buffer, sizeof(buffer), days == 1 ? "Not Due - In 1 day" : "Not Due - In %d days", days ); } return std::string(buffer); } if (state == "Due") { const float raw_hours = id(job_5_hours_since_required).state; if (isnan(raw_hours) || raw_hours <= 0.0f) { return std::string("Due - Just now"); } const int hours = (int) lroundf(raw_hours); char buffer[48]; if (hours < 24) { snprintf( buffer, sizeof(buffer), hours == 1 ? "Due - 1 hr ago" : "Due - %d hrs ago", hours ); } else { const int days = (hours + 12) / 24; snprintf( buffer, sizeof(buffer), days == 1 ? "Due - 1 day ago" : "Due - %d days ago", days ); } return std::string(buffer); } if (state == "Overdue") { const float raw_hours = id(job_5_hours_since_required).state; if (isnan(raw_hours) || raw_hours <= 0.0f) { return std::string("Overdue"); } const int hours = (int) lroundf(raw_hours); char buffer[48]; if (hours < 24) { snprintf( buffer, sizeof(buffer), hours == 1 ? "Overdue - 1 hr" : "Overdue - %d hrs", hours ); } else { const int days = (hours + 12) / 24; snprintf( buffer, sizeof(buffer), days == 1 ? "Overdue - 1 day" : "Overdue - %d days", days ); } return std::string(buffer); } if (state == "Missed") { return std::string("Missed"); } if ( state.empty() || state == "unknown" || state == "unavailable" ) { return std::string("Unknown"); } return state; # Update Bedroom and Clock status values after LVGL has finished constructing. - id: refresh_header_status mode: restart then: - if: condition: lambda: |- return id(lvgl_ready); then: - lvgl.label.update: id: bedroom_high_tide_value_label text: !lambda |- const auto value = id(high_tide_time).state; if (value.size() >= 16) { return value.substr(11, 5); } return std::string("--:--"); - lvgl.label.update: id: clock_high_tide_value_label text: !lambda |- const auto value = id(high_tide_time).state; if (value.size() >= 16) { return value.substr(11, 5); } return std::string("--:--"); - lvgl.label.update: id: bedroom_rain_today_value_label text: !lambda |- const float value = id(rain_today).state; if (isnan(value)) { return std::string("--%"); } char buffer[12]; snprintf(buffer, sizeof(buffer), "%.0f%%", value); return std::string(buffer); - lvgl.label.update: id: clock_rain_today_value_label text: !lambda |- const float value = id(rain_today).state; if (isnan(value)) { return std::string("--%"); } char buffer[12]; snprintf(buffer, sizeof(buffer), "%.0f%%", value); return std::string(buffer); - lvgl.label.update: id: bedroom_rain_tomorrow_value_label text: !lambda |- const float value = id(rain_tomorrow).state; if (isnan(value)) { return std::string("--%"); } char buffer[12]; snprintf(buffer, sizeof(buffer), "%.0f%%", value); return std::string(buffer); - lvgl.label.update: id: clock_rain_tomorrow_value_label text: !lambda |- const float value = id(rain_tomorrow).state; if (isnan(value)) { return std::string("--%"); } char buffer[12]; snprintf(buffer, sizeof(buffer), "%.0f%%", value); return std::string(buffer); - lvgl.label.update: id: bedroom_jobs_summary_value_label text: !lambda |- const auto value = id(jobs_summary).state; if ( value.empty() || value == "unknown" || value == "unavailable" ) { return std::string("--"); } return value; # 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 |- 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_header_status - script.execute: refresh_climate_controls - script.execute: refresh_vehicle_controls - script.execute: refresh_security_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"); } if (id(atto_lock_optimistic) == 0) { return std::string("Unlocked"); } const auto state = id(ha_atto_lock_state).state; if (state == "locked") { return std::string("Locked"); } if (state == "locking") { return std::string("Locking..."); } if (state == "unlocking") { return std::string("Unlocking..."); } return std::string("Unlocked"); - 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 labels match the other top-row data. The alert # background is fully hidden while idle, so no edge can be drawn. - 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 hidden: !lambda |- return !id(ha_atto_charging).state; # 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("Disabled"); - 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: # Lounge gas-heater state and user-facing status text. - lvgl.label.update: id: lounge_fire_button_label text: !lambda |- const auto status = id(ha_lounge_fire_status).state; if ( status.empty() || status == "unknown" || status == "unavailable" ) { return std::string("Fire: --"); } return std::string("Fire: ") + status; - lvgl.widget.update: id: lounge_fire_button bg_color: !lambda |- return id(ha_lounge_fire).state ? lv_color_hex(0x8A3F22) : lv_color_hex(0x28313D); # 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("Fan:Off"); } const float percentage = id(ha_ceiling_fan_percentage).state; if (isnan(percentage) || percentage < 50.0f) { return std::string("Fan:Low"); } if (percentage < 85.0f) { return std::string("Fan:Medium"); } return std::string("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 text and Quiet highlighting. - lvgl.label.update: id: air_filter_quiet_label text: !lambda |- const float level = id(ha_air_filter_level).state; if (isnan(level)) { return std::string("Filter:--"); } if (level >= 0.0f && level <= 2.0f) { return std::string("Filter:Quiet"); } char buffer[24]; snprintf( buffer, sizeof(buffer), "Filter:%.0f", level ); return std::string(buffer); - 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); # Update Security-page non-camera control button colours. - id: refresh_security_controls mode: restart then: - if: condition: lambda: |- return id(lvgl_ready); then: - lvgl.label.update: id: security_downstairs_guests_label text: !lambda |- return id(ha_downstairs_guests_occupied).state ? std::string("Downstairs Guests\nOccupied (Hold)") : std::string("Downstairs Guests\nNot Occupied (Hold)"); - lvgl.widget.update: id: security_downstairs_guests_button bg_color: !lambda |- return id(ha_downstairs_guests_occupied).state ? lv_color_hex(0xB53636) : lv_color_hex(0x28313D); border_color: !lambda |- return id(ha_downstairs_guests_occupied).state ? lv_color_hex(0xE06C6C) : lv_color_hex(0x548C6C); - lvgl.label.update: id: security_garage_door_state_label text: !lambda |- return id(ha_garage_door_open).state ? std::string("Garage Door State\nOpen") : std::string("Garage Door State\nClosed"); - lvgl.widget.update: id: security_garage_door_state bg_color: !lambda |- return id(ha_garage_door_open).state ? lv_color_hex(0xB53636) : lv_color_hex(0x315843); border_color: !lambda |- return id(ha_garage_door_open).state ? lv_color_hex(0xE06C6C) : lv_color_hex(0x548C6C); - lvgl.widget.update: id: security_external_garage_light_button bg_color: !lambda |- return id(ha_ext_garage_light).state ? lv_color_hex(0xB88918) : 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_front_door_button bg_color: !lambda |- return id(security_camera_selection) == 2 ? lv_color_hex(0x493535) : lv_color_hex(0x28313D); - lvgl.widget.update: id: security_driveway_button bg_color: !lambda |- return id(security_camera_selection) == 3 ? lv_color_hex(0x493535) : lv_color_hex(0x28313D); - lvgl.widget.update: id: security_garage_camera_button bg_color: !lambda |- return id(security_camera_selection) == 4 ? lv_color_hex(0x493535) : lv_color_hex(0x28313D); - lvgl.widget.update: id: security_caravan_camera_button bg_color: !lambda |- return id(security_camera_selection) == 5 ? lv_color_hex(0x493535) : lv_color_hex(0x28313D); - lvgl.widget.update: id: security_bike_shed_camera_button bg_color: !lambda |- return id(security_camera_selection) == 6 ? 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: - if: condition: lambda: |- std::string path; switch (id(security_camera_selection)) { case 2: path = id(ha_front_door_entity_picture).state; break; case 3: path = id(ha_driveway_entity_picture).state; break; case 4: path = id(ha_garage_camera_entity_picture).state; break; case 5: path = id(ha_caravan_entity_picture).state; break; case 6: path = id(ha_bike_shed_entity_picture).state; break; default: path = id(ha_doorbell_entity_picture).state; break; } return ( !path.empty() && path != "unknown" && path != "unavailable" ); then: - lvgl.label.update: id: security_camera_status_label text: !lambda |- std::string camera_name; switch (id(security_camera_selection)) { case 2: camera_name = "Front Door"; break; case 3: camera_name = "Driveway"; break; case 4: camera_name = "Garage"; break; case 5: camera_name = "Caravan"; break; case 6: camera_name = "Bike Shed"; break; default: camera_name = "Doorbell"; break; } return std::string("Loading ") + camera_name + "..."; - online_image.set_url: id: security_camera_image url: !lambda |- std::string path; switch (id(security_camera_selection)) { case 2: path = id(ha_front_door_entity_picture).state; break; case 3: path = id(ha_driveway_entity_picture).state; break; case 4: path = id(ha_garage_camera_entity_picture).state; break; case 5: path = id(ha_caravan_entity_picture).state; break; case 6: path = id(ha_bike_shed_entity_picture).state; break; default: path = id(ha_doorbell_entity_picture).state; break; } 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 |- std::string camera_name; switch (id(security_camera_selection)) { case 2: camera_name = "Front Door"; break; case 3: camera_name = "Driveway"; break; case 4: camera_name = "Garage"; break; case 5: camera_name = "Caravan"; break; case 6: camera_name = "Bike Shed"; break; default: camera_name = "Doorbell"; break; } return std::string("Waiting for ") + camera_name + " 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: # Re-evaluate the screen against the latest filtered lux reading. This allows # the clock to brighten during the day and dim again as the room gets darker. - interval: "${adaptive_backlight_recheck_interval}" then: - if: condition: lambda: |- return id(lvgl_ready); then: - if: condition: lambda: |- return id(display_is_dimmed); then: - script.execute: apply_dim_backlight else: - script.execute: apply_active_backlight - interval: 10s then: - if: condition: lambda: |- return id(ha_time).now().is_valid(); then: - script.execute: refresh_navigation_time - script.execute: refresh_jobs_page - 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: - lvgl.widget.update: id: garage_lights_button bg_color: 0xB88918 on_turn_off: then: - lvgl.widget.update: id: garage_lights_button bg_color: 0x28313D - platform: homeassistant id: ha_lounge_fire entity_id: "${lounge_fire_entity}" internal: true on_turn_on: then: - script.execute: refresh_climate_controls on_turn_off: then: - script.execute: refresh_climate_controls - platform: homeassistant id: ha_ext_garage_light entity_id: "${external_garage_light_entity}" internal: true on_turn_on: then: - script.execute: refresh_security_controls on_turn_off: then: - script.execute: refresh_security_controls #:########################################################################################:# # TEXT SENSOR COMPONENT: # https://esphome.io/components/text_sensor/ #:########################################################################################:# text_sensor: #:######################################################################################:# # AMBIENT LIGHT DIAGNOSTICS #:######################################################################################:# - platform: template id: ambient_light_band_text name: "${friendly_name} Ambient Light Band" icon: mdi:theme-light-dark entity_category: diagnostic update_interval: never #:######################################################################################:# # BEDROOM / CLOCK STATUS TEXT #:######################################################################################:# - platform: homeassistant id: high_tide_time entity_id: "${local_tides_entity}" attribute: next_high_tide_time internal: true on_value: then: - script.execute: refresh_header_status - platform: homeassistant id: jobs_summary entity_id: "${jobs_summary_entity}" internal: true on_value: then: - script.execute: refresh_header_status - platform: homeassistant id: ha_lounge_fire_status entity_id: "${lounge_fire_status_entity}" internal: true on_value: then: - script.execute: refresh_climate_controls #:######################################################################################:# # JOBS TRACKER STATES #:######################################################################################:# - platform: homeassistant id: job_1_state entity_id: "${job_1_state_entity}" internal: true on_value: then: - script.execute: refresh_jobs_page - platform: homeassistant id: job_2_state entity_id: "${job_2_state_entity}" internal: true on_value: then: - script.execute: refresh_jobs_page - platform: homeassistant id: job_3_state entity_id: "${job_3_state_entity}" internal: true on_value: then: - script.execute: refresh_jobs_page - platform: homeassistant id: job_4_state entity_id: "${job_4_state_entity}" internal: true on_value: then: - script.execute: refresh_jobs_page - platform: homeassistant id: job_5_state entity_id: "${job_5_state_entity}" internal: true on_value: then: - script.execute: refresh_jobs_page #:######################################################################################:# # 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_front_door_entity_picture entity_id: "${security_front_door_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: 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) == 3 ); then: - script.execute: refresh_security_camera - platform: homeassistant id: ha_garage_camera_entity_picture entity_id: "${security_garage_camera_entity}" attribute: entity_picture internal: true on_value: then: - if: condition: lambda: |- return ( id(security_page_active) && id(security_camera_selection) == 4 ); then: - script.execute: refresh_security_camera - platform: homeassistant id: ha_caravan_entity_picture entity_id: "${security_caravan_camera_entity}" attribute: entity_picture internal: true on_value: then: - if: condition: lambda: |- return ( id(security_page_active) && id(security_camera_selection) == 5 ); then: - script.execute: refresh_security_camera - platform: homeassistant id: ha_bike_shed_entity_picture entity_id: "${security_bike_shed_camera_entity}" attribute: entity_picture internal: true on_value: then: - if: condition: lambda: |- return ( id(security_page_active) && id(security_camera_selection) == 6 ); 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 Security camera 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: # First stage: dim the current page but leave it visible. - timeout: !lambda |- const float seconds = id(display_dim_delay).state; return (uint32_t) ( (isnan(seconds) ? 30.0f : std::max(10.0f, seconds)) * 1000.0f ); then: - script.execute: apply_dim_backlight # Second stage: show the Clock page and release any camera image. - timeout: !lambda |- const float seconds = id(display_clock_delay).state; return (uint32_t) ( (isnan(seconds) ? 60.0f : std::max(15.0f, seconds)) * 1000.0f ); then: - lambda: |- id(security_page_active) = false; - lvgl.page.show: id: clock_page animation: FADE_IN time: 250ms - script.execute: apply_dim_backlight - delay: 50ms - online_image.release: security_camera_image 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: 22 y: 31 width: 16 height: 16 radius: 8 bg_color: 0xF06A6A bg_opa: COVER border_width: 0 shadow_width: 0 clickable: false # Compact Bedtime control beside the API connection indicator. - button: id: bedtime_button x: 50 y: 8 width: 210 height: 68 bg_color: 0x3B3048 bg_opa: COVER border_width: 2 border_color: 0x735E88 radius: 13 pressed: bg_color: 0x584668 widgets: - label: id: bedtime_button_label align: CENTER text: "\U000F04B2" text_font: font_mdi_navigation text_align: CENTER text_color: 0xFFFFFF on_long_press: then: - homeassistant.action: action: input_button.press data: entity_id: "${bedtime_button_entity}" # Divider between Bedtime and the compact status strip. - obj: x: 268 y: 11 width: 1 height: 54 bg_color: 0x354454 bg_opa: COVER border_width: 0 clickable: false # Seven compact status readings across the top-right. - label: x: 274 y: 9 width: 104 text: "High Tide" text_font: font_header text_align: CENTER text_color: 0x6EBCC9 - label: id: bedroom_high_tide_value_label x: 274 y: 36 width: 104 text: "--:--" text_font: font_small text_align: CENTER text_color: 0x8BD6E1 - obj: x: 377 y: 11 width: 1 height: 54 bg_color: 0x354454 bg_opa: COVER border_width: 0 clickable: false - label: x: 378 y: 9 width: 104 text: "Rain Today" text_font: font_header text_align: CENTER text_color: 0x6C9FD2 - label: id: bedroom_rain_today_value_label x: 378 y: 36 width: 104 text: "--%" text_font: font_small text_align: CENTER text_color: 0x85B8E8 - obj: x: 481 y: 11 width: 1 height: 54 bg_color: 0x354454 bg_opa: COVER border_width: 0 clickable: false - label: x: 482 y: 9 width: 104 text: "Rain Tomorrow" text_font: font_header text_align: CENTER text_color: 0x6C9FD2 - label: id: bedroom_rain_tomorrow_value_label x: 482 y: 36 width: 104 text: "--%" text_font: font_small text_align: CENTER text_color: 0x85B8E8 - obj: x: 585 y: 11 width: 1 height: 54 bg_color: 0x354454 bg_opa: COVER border_width: 0 clickable: false - label: x: 586 y: 9 width: 104 text: "Moon" text_font: font_header text_align: CENTER text_color: 0xA9A0D4 - label: id: bedroom_moon_value_label x: 586 y: 36 width: 104 text: "--.-%" text_font: font_small text_align: CENTER text_color: 0xC6BDEC - obj: x: 689 y: 11 width: 1 height: 54 bg_color: 0x354454 bg_opa: COVER border_width: 0 clickable: false - label: x: 690 y: 9 width: 104 text: "Outdoor" text_font: font_header text_align: CENTER text_color: 0x84B5D8 - label: id: bedroom_outdoor_value_label x: 690 y: 36 width: 104 text: "--.-°C" text_font: font_small text_align: CENTER text_color: 0x9CCBEA - obj: x: 793 y: 11 width: 1 height: 54 bg_color: 0x354454 bg_opa: COVER border_width: 0 clickable: false - label: x: 794 y: 9 width: 104 text: "Indoor" text_font: font_header text_align: CENTER text_color: 0xD6B78F - label: id: bedroom_temperature_label x: 794 y: 36 width: 104 text: "--.-°C" text_font: font_small text_align: CENTER text_color: 0xE8C79D - obj: x: 897 y: 11 width: 1 height: 54 bg_color: 0x354454 bg_opa: COVER border_width: 0 clickable: false - label: x: 898 y: 9 width: 104 text: "Humidity" text_font: font_header text_align: CENTER text_color: 0x80BDA4 - label: id: bedroom_humidity_label x: 898 y: 36 width: 104 text: "--%" text_font: font_small 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: - 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: jobs_summary_navigation_button x: 18 y: 475 width: 190 height: 92 bg_color: 0x263443 bg_opa: COVER border_width: 2 border_color: 0x52677B radius: 16 pressed: bg_color: 0x34495C widgets: - label: align: LEFT_MID x: 12 text: "\U000F014E" text_font: font_mdi_button text_color: 0xAFC7DC - label: x: 47 y: 7 width: 132 text: "Jobs Summary" text_font: font_header text_align: CENTER text_color: 0xAFC7DC - label: id: bedroom_jobs_summary_value_label x: 47 y: 29 width: 132 height: 54 text: "--" text_font: font_header text_align: CENTER text_color: 0xFFFFFF long_mode: WRAP on_click: - lvgl.page.show: id: jobs_page animation: MOVE_LEFT time: 220ms - script.execute: refresh_jobs_page - button: id: climate_navigation_button x: 414 y: 475 width: 190 height: 92 widgets: - label: id: climate_navigation_button_label align: CENTER text: "\U000F050F" text_font: font_mdi_navigation text_color: 0xFFFFFF bg_color: 0x315843 bg_opa: COVER border_width: 2 border_color: 0x548C6C radius: 16 pressed: bg_color: 0x7D3030 on_click: - lvgl.page.show: id: climate_page animation: MOVE_LEFT time: 220ms - button: id: atto_navigation_button x: 216 y: 475 width: 190 height: 92 widgets: - label: id: atto_navigation_button_label align: CENTER text: "\U000F07A8" text_font: font_mdi_navigation 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 - script.execute: apply_active_backlight - 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: "\U000F068A" text_font: font_mdi_navigation 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; id(security_camera_selection) = 1; - lvgl.label.update: id: security_camera_status_label text: "Loading Doorbell..." - lvgl.page.show: id: security_page animation: NONE time: 0ms - script.execute: apply_active_backlight - script.execute: refresh_security_controls - script.execute: refresh_security_camera_buttons # Allow the Security page to draw before starting HTTP image work. - delay: 75ms - 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: "--:--" text_font: font_navigation_time 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 - script.execute: apply_dim_backlight #:####################################################################################:# # JOBS PAGE #:####################################################################################:# - id: jobs_page skip: true bg_color: 0x111923 bg_opa: COVER widgets: #:################################################################################:# # ACTIVE JOBS #:################################################################################:# - button: id: job_1_button x: 18 y: 18 width: 320 height: 76 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x526276 radius: 14 pressed: bg_color: 0x42627A widgets: - label: align: TOP_MID y: 7 width: 300 text: "Dog Feeding Morning" text_font: font_job_name text_align: CENTER text_color: 0xFFFFFF - label: id: job_1_status_label align: TOP_MID y: 40 width: 300 text: "Unknown" text_font: font_job_status text_align: CENTER text_color: 0xFFFFFF on_long_press: then: - lvgl.label.update: id: job_1_status_label text: "Completing..." - homeassistant.action: action: script.jobs_tracker_route_completion data: manual_job_id: "${job_1_id}" - delay: 2s - script.execute: refresh_jobs_page - button: id: job_2_button x: 352 y: 18 width: 320 height: 76 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x526276 radius: 14 pressed: bg_color: 0x42627A widgets: - label: align: TOP_MID y: 7 width: 300 text: "Dog Feeding Evening" text_font: font_job_name text_align: CENTER text_color: 0xFFFFFF - label: id: job_2_status_label align: TOP_MID y: 40 width: 300 text: "Unknown" text_font: font_job_status text_align: CENTER text_color: 0xFFFFFF on_long_press: then: - lvgl.label.update: id: job_2_status_label text: "Completing..." - homeassistant.action: action: script.jobs_tracker_route_completion data: manual_job_id: "${job_2_id}" - delay: 2s - script.execute: refresh_jobs_page - button: id: job_3_button x: 686 y: 18 width: 320 height: 76 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x526276 radius: 14 pressed: bg_color: 0x42627A widgets: - label: align: TOP_MID y: 7 width: 300 text: "Dog De-pooping" text_font: font_job_name text_align: CENTER text_color: 0xFFFFFF - label: id: job_3_status_label align: TOP_MID y: 40 width: 300 text: "Unknown" text_font: font_job_status text_align: CENTER text_color: 0xFFFFFF on_long_press: then: - lvgl.label.update: id: job_3_status_label text: "Completing..." - homeassistant.action: action: script.jobs_tracker_route_completion data: manual_job_id: "${job_3_id}" - delay: 2s - script.execute: refresh_jobs_page - button: id: job_4_button x: 18 y: 103 width: 320 height: 76 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x526276 radius: 14 pressed: bg_color: 0x42627A widgets: - label: align: TOP_MID y: 7 width: 300 text: "Dishwasher Unpacked" text_font: font_job_name text_align: CENTER text_color: 0xFFFFFF - label: id: job_4_status_label align: TOP_MID y: 40 width: 300 text: "Unknown" text_font: font_job_status text_align: CENTER text_color: 0xFFFFFF on_long_press: then: - lvgl.label.update: id: job_4_status_label text: "Completing..." - homeassistant.action: action: script.jobs_tracker_route_completion data: manual_job_id: "${job_4_id}" - delay: 2s - script.execute: refresh_jobs_page - button: id: job_5_button x: 352 y: 103 width: 320 height: 76 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x526276 radius: 14 pressed: bg_color: 0x42627A widgets: - label: align: TOP_MID y: 7 width: 300 text: "Rubbish Removed" text_font: font_job_name text_align: CENTER text_color: 0xFFFFFF - label: id: job_5_status_label align: TOP_MID y: 40 width: 300 text: "Unknown" text_font: font_job_status text_align: CENTER text_color: 0xFFFFFF on_long_press: then: - lvgl.label.update: id: job_5_status_label text: "Completing..." - homeassistant.action: action: script.jobs_tracker_route_completion data: manual_job_id: "${job_5_id}" - delay: 2s - script.execute: refresh_jobs_page #:################################################################################:# # RESERVED JOB SLOTS 6-15 #:################################################################################:# - obj: id: job_slot_6 x: 686 y: 103 width: 320 height: 76 bg_color: 0x1B2530 bg_opa: COVER border_width: 1 border_color: 0x334150 radius: 14 clickable: false - obj: id: job_slot_7 x: 18 y: 188 width: 320 height: 76 bg_color: 0x1B2530 bg_opa: COVER border_width: 1 border_color: 0x334150 radius: 14 clickable: false - obj: id: job_slot_8 x: 352 y: 188 width: 320 height: 76 bg_color: 0x1B2530 bg_opa: COVER border_width: 1 border_color: 0x334150 radius: 14 clickable: false - obj: id: job_slot_9 x: 686 y: 188 width: 320 height: 76 bg_color: 0x1B2530 bg_opa: COVER border_width: 1 border_color: 0x334150 radius: 14 clickable: false - obj: id: job_slot_10 x: 18 y: 273 width: 320 height: 76 bg_color: 0x1B2530 bg_opa: COVER border_width: 1 border_color: 0x334150 radius: 14 clickable: false - obj: id: job_slot_11 x: 352 y: 273 width: 320 height: 76 bg_color: 0x1B2530 bg_opa: COVER border_width: 1 border_color: 0x334150 radius: 14 clickable: false - obj: id: job_slot_12 x: 686 y: 273 width: 320 height: 76 bg_color: 0x1B2530 bg_opa: COVER border_width: 1 border_color: 0x334150 radius: 14 clickable: false - obj: id: job_slot_13 x: 18 y: 358 width: 320 height: 76 bg_color: 0x1B2530 bg_opa: COVER border_width: 1 border_color: 0x334150 radius: 14 clickable: false - obj: id: job_slot_14 x: 352 y: 358 width: 320 height: 76 bg_color: 0x1B2530 bg_opa: COVER border_width: 1 border_color: 0x334150 radius: 14 clickable: false - obj: id: job_slot_15 x: 686 y: 358 width: 320 height: 76 bg_color: 0x1B2530 bg_opa: COVER border_width: 1 border_color: 0x334150 radius: 14 clickable: false #:################################################################################:# # BOTTOM NAVIGATION #:################################################################################:# - button: id: jobs_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: "\U000F004D" text_font: font_mdi_navigation text_color: 0xFFFFFF on_click: - lvgl.page.show: id: bedroom_page animation: MOVE_RIGHT time: 220ms - button: id: jobs_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: id: jobs_clock_button_label align: CENTER text: "--:--" text_font: font_navigation_time text_color: 0xFFFFFF on_click: - lvgl.page.show: id: clock_page animation: FADE_IN time: 250ms - script.execute: apply_dim_backlight #:####################################################################################:# # VEHICLES PAGE #:####################################################################################:# - id: vehicles_page skip: true bg_color: 0x111923 bg_opa: COVER widgets: #:################################################################################:# # VEHICLE STATUS / SECOND-ROW 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: 31 width: 166 text: "Locked" text_font: font_vehicle_bold 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 # Red alert background only; hidden completely while not charging. - obj: id: atto_charging_status x: 766 y: 22 width: 220 height: 92 bg_color: 0xD32F2F bg_opa: COVER border_width: 0 outline_width: 0 shadow_width: 0 radius: 17 clickable: false hidden: true # Sibling labels use the same absolute coordinates as the other data. - label: x: 783 y: 53 text: "\U000F0084" text_font: font_mdi_vehicle text_color: 0xFFFFFF - label: x: 823 y: 31 width: 145 text: "Atto Charging" text_font: font_small text_align: CENTER text_color: 0xAEB8C4 - label: id: atto_charging_state_label x: 823 y: 60 width: 145 text: "Off" text_font: font_vehicle_bold text_align: CENTER text_color: 0xFFFFFF - button: id: atto_schedule_button x: 272 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: "Disabled" 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: 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_driver_seat_button_label x: 44 y: 19 width: 166 text: "Driver\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 / TOP-ROW DATA #:################################################################################:# # Cabin, battery level, range and charging are display-only. # 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: "Battery" 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: 519 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\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: "\U000F004D" text_font: font_mdi_navigation 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: id: vehicles_clock_button_label align: CENTER text: "--:--" text_font: font_navigation_time text_color: 0xFFFFFF on_click: - lvgl.page.show: id: clock_page animation: FADE_IN time: 250ms - script.execute: apply_dim_backlight #:####################################################################################:# # CLIMATE PAGE #:####################################################################################:# - id: climate_page bg_color: 0x111923 bg_opa: COVER widgets: - label: x: 18 y: 41 width: 180 text: "Lounge Gas Heater" text_font: font_small text_align: CENTER text_color: 0x8FA0B3 - label: x: 220 y: 41 width: 782 text: "Master Bedroom Heat Pump" text_font: font_small text_align: CENTER text_color: 0x8FA0B3 #:################################################################################:# # LOUNGE GAS HEATER / HEAT-PUMP MODES #:################################################################################:# - button: id: lounge_fire_button x: 18 y: 72 width: 180 height: 96 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x9B5B42 radius: 17 pressed: bg_color: 0x8A3F22 widgets: - label: id: lounge_fire_button_label align: CENTER text: "Fire: --" text_font: font_small text_align: CENTER text_color: 0xFFFFFF on_click: - switch.toggle: ha_lounge_fire # A 22-pixel gap separates the Lounge Fire from the Heat Pump group. - button: id: climate_off_button x: 220 y: 72 width: 188 height: 96 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x5A7A68 radius: 17 pressed: bg_color: 0x548C6C widgets: - label: align: TOP_MID y: 9 text: "\U000F0425" text_font: font_mdi_climate text_color: 0xFFFFFF - label: align: BOTTOM_MID y: -10 text: "Off" text_font: font_small text_color: 0xFFFFFF on_click: - homeassistant.action: action: script.turn_on data: entity_id: "${climate_off_script}" - button: id: climate_heat_button x: 418 y: 72 width: 188 height: 96 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0xA6672B radius: 17 pressed: bg_color: 0xB76822 widgets: - label: align: TOP_MID y: 9 text: "\U000F0238" text_font: font_mdi_climate text_color: 0xFFFFFF - label: align: BOTTOM_MID y: -10 text: "Heat" text_font: font_small text_color: 0xFFFFFF on_click: - homeassistant.action: action: script.turn_on data: entity_id: "${climate_heat_script}" - button: id: climate_cool_button x: 616 y: 72 width: 188 height: 96 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x3E7FA7 radius: 17 pressed: bg_color: 0x2E6A96 widgets: - label: align: TOP_MID y: 9 text: "\U000F0717" text_font: font_mdi_climate text_color: 0xFFFFFF - label: align: BOTTOM_MID y: -10 text: "Cool" text_font: font_small text_color: 0xFFFFFF on_click: - homeassistant.action: action: script.turn_on data: entity_id: "${climate_cool_script}" - button: id: climate_dry_button x: 814 y: 72 width: 188 height: 96 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x427B96 radius: 17 pressed: bg_color: 0x32677F widgets: - label: align: TOP_MID y: 9 text: "\U000F0E0A" text_font: font_mdi_climate text_color: 0xFFFFFF - label: align: BOTTOM_MID y: -10 text: "Dehumid" text_font: font_small 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: TOP_MID y: 10 text: "\U000F032A" text_font: font_mdi_climate text_color: 0xFFFFFF - label: align: BOTTOM_MID y: -12 text: "Quiet" text_font: font_small 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: TOP_MID y: 10 text: "\U000F04C5" text_font: font_mdi_climate text_color: 0xFFFFFF - label: align: BOTTOM_MID y: -12 text: "Boost" text_font: font_small 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: TOP_MID y: 10 text: "\U000F0768" text_font: font_mdi_climate text_color: 0xFFFFFF - label: align: BOTTOM_MID y: -12 text: "Nanoe-X" text_font: font_small 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: align: LEFT_MID x: 18 text: "\U000F1797" text_font: font_mdi_climate text_color: 0xFFFFFF - label: id: ceiling_fan_cycle_label align: CENTER x: 18 text: "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: LEFT_MID x: 18 text: "\U000F0D43" text_font: font_mdi_climate text_color: 0xFFFFFF - label: id: air_filter_quiet_label align: CENTER x: 18 text: "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: "\U000F004D" text_font: font_mdi_navigation 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: id: climate_clock_button_label align: CENTER text: "--:--" text_font: font_navigation_time text_color: 0xFFFFFF on_click: - lvgl.page.show: id: clock_page animation: FADE_IN time: 250ms - script.execute: apply_dim_backlight #:####################################################################################:# # SECURITY PAGE #:####################################################################################:# - id: security_page skip: true bg_color: 0x0D1218 bg_opa: COVER widgets: #:################################################################################:# # SIX-CAMERA SELECTION ROW #:################################################################################:# - button: id: security_doorbell_button x: 18 y: 18 width: 156 height: 70 bg_color: 0x493535 bg_opa: COVER border_width: 2 border_color: 0x9A6666 radius: 14 pressed: bg_color: 0x654949 widgets: - label: align: CENTER text: "Doorbell" text_font: font_small text_color: 0xFFFFFF on_press: - lambda: |- id(security_camera_selection) = 1; - lvgl.label.update: id: security_camera_status_label text: "Loading Doorbell..." # Optimistic selection and download start on touch-down. - script.execute: refresh_security_camera_buttons - script.execute: refresh_security_camera - button: id: security_front_door_button x: 184 y: 18 width: 156 height: 70 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x9A6666 radius: 14 pressed: bg_color: 0x654949 widgets: - label: align: CENTER text: "Front Door" text_font: font_small text_color: 0xFFFFFF on_press: - lambda: |- id(security_camera_selection) = 2; - lvgl.label.update: id: security_camera_status_label text: "Loading Front Door..." # Optimistic selection and download start on touch-down. - script.execute: refresh_security_camera_buttons - script.execute: refresh_security_camera - button: id: security_driveway_button x: 350 y: 18 width: 156 height: 70 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x9A6666 radius: 14 pressed: bg_color: 0x654949 widgets: - label: align: CENTER text: "Driveway" text_font: font_small text_color: 0xFFFFFF on_press: - lambda: |- id(security_camera_selection) = 3; - lvgl.label.update: id: security_camera_status_label text: "Loading Driveway..." # Optimistic selection and download start on touch-down. - script.execute: refresh_security_camera_buttons - script.execute: refresh_security_camera - button: id: security_garage_camera_button x: 516 y: 18 width: 156 height: 70 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x9A6666 radius: 14 pressed: bg_color: 0x654949 widgets: - label: align: CENTER text: "Garage" text_font: font_small text_color: 0xFFFFFF on_press: - lambda: |- id(security_camera_selection) = 4; - lvgl.label.update: id: security_camera_status_label text: "Loading Garage..." # Optimistic selection and download start on touch-down. - script.execute: refresh_security_camera_buttons - script.execute: refresh_security_camera - button: id: security_caravan_camera_button x: 682 y: 18 width: 156 height: 70 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x9A6666 radius: 14 pressed: bg_color: 0x654949 widgets: - label: align: CENTER text: "Caravan" text_font: font_small text_color: 0xFFFFFF on_press: - lambda: |- id(security_camera_selection) = 5; - lvgl.label.update: id: security_camera_status_label text: "Loading Caravan..." # Optimistic selection and download start on touch-down. - script.execute: refresh_security_camera_buttons - script.execute: refresh_security_camera - button: id: security_bike_shed_camera_button x: 848 y: 18 width: 156 height: 70 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x9A6666 radius: 14 pressed: bg_color: 0x654949 widgets: - label: align: CENTER text: "Bike Shed" text_font: font_small text_color: 0xFFFFFF on_press: - lambda: |- id(security_camera_selection) = 6; - lvgl.label.update: id: security_camera_status_label text: "Loading Bike Shed..." # Optimistic selection and download start on touch-down. - script.execute: refresh_security_camera_buttons - script.execute: refresh_security_camera #:################################################################################:# # SECURITY CONTROLS #:################################################################################:# - button: id: security_downstairs_guests_button x: 25 y: 105 width: 290 height: 70 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x548C6C radius: 16 pressed: bg_color: 0x416F55 widgets: - label: id: security_downstairs_guests_label align: CENTER text: "Downstairs Guests\nNot Occupied (Hold)" text_font: font_small text_align: CENTER text_color: 0xFFFFFF on_long_press: then: - homeassistant.action: action: input_boolean.toggle data: entity_id: "${downstairs_guests_occupied_entity}" - obj: id: security_garage_door_state x: 25 y: 190 width: 290 height: 70 bg_color: 0x315843 bg_opa: COVER border_width: 2 border_color: 0x548C6C outline_width: 0 shadow_width: 0 radius: 16 clickable: false widgets: - label: id: security_garage_door_state_label align: CENTER text: "Garage Door State\nClosed" text_font: font_small text_align: CENTER text_color: 0xFFFFFF - 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_external_garage_light_button x: 25 y: 360 width: 290 height: 70 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x806D45 radius: 16 pressed: bg_color: 0xA87500 widgets: - label: align: CENTER text: "Ext Garage Light" text_font: font_button text_color: 0xFFFFFF on_click: - switch.toggle: ha_ext_garage_light #:################################################################################:# # 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: "\U000F004D" text_font: font_mdi_navigation text_color: 0xFFFFFF on_click: - lambda: |- id(security_page_active) = false; - lvgl.page.show: id: bedroom_page animation: NONE time: 0ms - delay: 50ms - online_image.release: security_camera_image - 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: id: security_clock_button_label align: CENTER text: "--:--" text_font: font_navigation_time text_color: 0xFFFFFF on_click: - lambda: |- id(security_page_active) = false; - lvgl.page.show: id: clock_page animation: NONE time: 0ms - script.execute: apply_dim_backlight - delay: 50ms - online_image.release: security_camera_image #:####################################################################################:# # 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 - script.execute: apply_active_backlight - 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 # Seven compact status readings. - label: x: 22 y: 454 width: 138 text: "High Tide" text_font: font_header text_align: CENTER text_color: 0x6EBCC9 clickable: false - label: id: clock_high_tide_value_label x: 22 y: 483 width: 138 text: "--:--" text_font: font_button text_align: CENTER text_color: 0x8BD6E1 clickable: false - obj: x: 160 y: 455 width: 1 height: 65 bg_color: 0x354454 bg_opa: COVER border_width: 0 clickable: false - label: x: 162 y: 454 width: 138 text: "Rain Today" text_font: font_header text_align: CENTER text_color: 0x6C9FD2 clickable: false - label: id: clock_rain_today_value_label x: 162 y: 483 width: 138 text: "--%" text_font: font_button text_align: CENTER text_color: 0x85B8E8 clickable: false - obj: x: 300 y: 455 width: 1 height: 65 bg_color: 0x354454 bg_opa: COVER border_width: 0 clickable: false - label: x: 302 y: 454 width: 138 text: "Rain Tomorrow" text_font: font_header text_align: CENTER text_color: 0x6C9FD2 clickable: false - label: id: clock_rain_tomorrow_value_label x: 302 y: 483 width: 138 text: "--%" text_font: font_button text_align: CENTER text_color: 0x85B8E8 clickable: false - obj: x: 440 y: 455 width: 1 height: 65 bg_color: 0x354454 bg_opa: COVER border_width: 0 clickable: false - label: x: 442 y: 454 width: 138 text: "Moon" text_font: font_header text_align: CENTER text_color: 0xA9A0D4 clickable: false - label: id: clock_moon_value_label x: 442 y: 483 width: 138 text: "--.-%" text_font: font_button text_align: CENTER text_color: 0xC6BDEC clickable: false - obj: x: 580 y: 455 width: 1 height: 65 bg_color: 0x354454 bg_opa: COVER border_width: 0 clickable: false - label: x: 582 y: 454 width: 138 text: "Outdoor" text_font: font_header text_align: CENTER text_color: 0x84B5D8 clickable: false - label: id: clock_outdoor_value_label x: 582 y: 483 width: 138 text: "--.-°C" text_font: font_button text_align: CENTER text_color: 0x9CCBEA clickable: false - obj: x: 720 y: 455 width: 1 height: 65 bg_color: 0x354454 bg_opa: COVER border_width: 0 clickable: false - label: x: 722 y: 454 width: 138 text: "Indoor" text_font: font_header text_align: CENTER text_color: 0xD6B78F clickable: false - label: id: clock_temperature_label x: 722 y: 483 width: 138 text: "--.-°C" text_font: font_button text_align: CENTER text_color: 0xE8C79D clickable: false - obj: x: 860 y: 455 width: 1 height: 65 bg_color: 0x354454 bg_opa: COVER border_width: 0 clickable: false - label: x: 862 y: 454 width: 140 text: "Humidity" text_font: font_header text_align: CENTER text_color: 0x80BDA4 clickable: false - label: id: clock_humidity_label x: 862 y: 483 width: 140 text: "--%" text_font: font_button 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