#:########################################################################################:# # 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.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 Nano-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, Nano-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 #:########################################################################################:# # OPERATION NOTES: # - This build returns to the proven V0.8a structure. # - The only functional clock changes are: # - 24-hour HH:MM display # - AM/PM label and update removed # - The 24-hour clock font is increased to 280px. # - The date uses a dedicated 64px font. # - Moon, Outdoor, Indoor and Humidity readings are positioned near the bottom. # - The display remains in its native 1024x600 landscape orientation. # - The default page contains nine Bedroom / nearby controls. # - The bottom navigation row contains Bedtime, Climate, Future1, Future2 and Clock. # - After the configured idle period: # - The large clock page is displayed. # - Backlight brightness is reduced. # - Touching the clock: # - Returns to the Bedroom controls. # - Restores normal brightness. # - The Climate page contains heat-pump, ceiling-fan, air-filter and Night Power controls. # - Sleep: 2 hrs starts a local 120-minute countdown and turns the heat pump off. # - Pressing the active Sleep button again cancels the countdown. #:########################################################################################:# # OFFLINE NOTES: # - LVGL, the clock, touch and backlight operate locally. # - Home Assistant control buttons require an API connection. # - The clock continues using the most recently synchronised time. #:########################################################################################:# #:########################################################################################:# # 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. Security page with Home Assistant Doorbell camera snapshots. (Layout V1.1)" device_area: "Bedroom" # Project Naming project_name: "Guition.JC1060P470C_I_W_Y" project_version: "v1.3" # Passwords & Secrets api_key: !secret esp-api_key ota_pass: !secret esp-ota_pass # Device Settings log_level: "INFO" update_interval: "60s" # Display Settings backlight_pin: "GPIO23" backlight_frequency: "1220Hz" # Touchscreen Settings touch_sda_pin: "GPIO7" touch_scl_pin: "GPIO8" touch_interrupt_pin: "GPIO21" touch_reset_pin: "GPIO22" # Clock / Screen Settings clock_idle_timeout: "60s" control_brightness: "80%" clock_brightness: "4%" # Security Camera Settings # Change this only if homeassistant.local is not reachable from the panel. home_assistant_internal_url: "http://homeassistant.local:8123" security_camera_refresh_interval: "5s" security_doorbell_camera_entity: "camera.view_road_front_door_fluent" # Home Assistant Entities - Status / Environment moon_phase_entity: "sensor.astroweather_moon_phase" outdoor_temperature_entity: "sensor.lounge_outside_temperature" bedroom_temperature_entity: "sensor.bedroom_1_environment_z19th_temperature" bedroom_humidity_entity: "sensor.bedroom_1_environment_z19th_humidity" # Home Assistant Entities - Bedroom / Nearby Controls north_lights_entity: "light.tasmo_ifan02_3793_bedrm1_1" south_lights_entity: "switch.master_bedroom_master_bedroom_main_lights_south_relay_2_master_bedroom_south_lights" bedside_lamp_entity: "switch.master_bedroom_bedside_lamp_arlec_bulb_master_bedroom_bedside_lamp_arlec_bulb_jump_fade_up_down" hall_floor_entity: "switch.main_hallway_nightlights_1_relay_1_main_hallway_nightlights" hall_lights_entity: "switch.main_hallway_lightswitch_relay_1_main_hallway_lights" bathroom_lights_entity: "switch.main_bathroom_lightswitch_3_2_all_main_bathroom_lights" patio_lights_entity: "switch.esp_garageentrylights_relay_2_patio_lights" garage_lights_entity: "switch.esp_garageentrylights_relay_3_garage_lights" bathroom_fan_entity: "switch.esp_mainbathfancombo_relay_1_extract_fan" # Home Assistant Entities - Status garage_door_entity: "binary_sensor.garage_roller_door_x18rs_contact" quiet_time_entity: "input_boolean.quiet_time" # Home Assistant Entities - Actions bedtime_button_entity: "input_button.bedtime_mode" # Home Assistant Entities - Climate climate_entity: "climate.master_bedroom" climate_requested_mode_entity: "input_select.master_bedroom_hvac_requested_mode" climate_profile_entity: "input_select.master_bedroom_heat_pump_profile" climate_off_script: "script.master_bedroom_hvac_off" climate_heat_script: "script.master_bedroom_hvac_heat" climate_cool_script: "script.master_bedroom_hvac_cool" climate_dry_script: "script.master_bedroom_hvac_dry" climate_setpoint_adjust_script: "script.master_bedroom_setpoint_adjust" # Home Assistant Entities - Ceiling Fan / Air Filter / Night Power ceiling_fan_entity: "fan.tasmo_ifan02_3793_bedrm1_1" air_filter_level_entity: "number.bedroom_1_air_filter_bedroom_1_air_filter_speed_level" climate_night_power_entity: "input_boolean.master_bedroom_night_power_mode" nanoe_entity: "switch.master_bedroom_nanoe" #:########################################################################################:# # 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: - delay: 2s - lambda: |- id(lvgl_ready) = true; - script.execute: refresh_api_indicator - script.execute: refresh_button_states - script.execute: refresh_sleep_timer #:########################################################################################:# # ESP PLATFORM AND FRAMEWORK: # https://esphome.io/components/esp32.html #:########################################################################################:# esp32: variant: ESP32P4 engineering_sample: true cpu_frequency: 360MHz flash_size: 16MB framework: type: esp-idf advanced: enable_idf_experimental_features: true disable_vfs_support_termios: false disable_vfs_support_select: false disable_vfs_support_dir: false sdkconfig_options: CONFIG_SPIRAM_XIP_FROM_PSRAM: "y" CONFIG_SDMMC_HOST_DEFAULT: "y" #:########################################################################################:# # PSRAM: # https://esphome.io/components/psram.html #:########################################################################################:# psram: mode: hex speed: 200MHz #:########################################################################################:# # ESP32 HOSTED WIFI COPROCESSOR: # https://esphome.io/components/esp32_hosted.html #:########################################################################################:# esp32_hosted: variant: ESP32C6 reset_pin: GPIO54 cmd_pin: GPIO19 clk_pin: GPIO18 d0_pin: GPIO14 d1_pin: GPIO15 d2_pin: GPIO16 d3_pin: GPIO17 active_high: true use_psram: true #:########################################################################################:# # ESP LDO: # Supplies the ESP32-P4 MIPI-DSI PHY #:########################################################################################:# esp_ldo: - channel: 3 id: dsi_phy_enable voltage: 2.5V adjustable: true #:########################################################################################:# # LOGGER: # https://esphome.io/components/logger.html #:########################################################################################:# logger: level: "${log_level}" baud_rate: 115200 hardware_uart: USB_SERIAL_JTAG #:########################################################################################:# # HOME ASSISTANT API: #:########################################################################################:# api: on_client_connected: then: - logger.log: level: INFO format: "API connected: ESP Bedside Panel is online" - script.execute: refresh_api_indicator # HA may deliver imported entity states before all LVGL widgets are ready. # Run a delayed two-pass refresh so startup colours always match HA. - script.execute: startup_button_state_sync on_client_disconnected: then: - logger.log: level: WARN format: "API disconnected: ESP Bedside Panel is offline from Home Assistant" # Allow another API client to remain connected before showing offline. - delay: 500ms - script.execute: refresh_api_indicator #:########################################################################################:# # OTA: # https://esphome.io/components/ota/esphome.html #:########################################################################################:# ota: - platform: esphome password: "${ota_pass}" #:########################################################################################:# # TIME: # https://esphome.io/components/time/homeassistant.html #:########################################################################################:# time: - platform: homeassistant id: ha_time #:########################################################################################:# # I2C: # https://esphome.io/components/i2c.html #:########################################################################################:# i2c: - id: touch_i2c sda: "${touch_sda_pin}" scl: "${touch_scl_pin}" frequency: 400kHz scan: true #:########################################################################################:# # OUTPUT: # https://esphome.io/components/output/ledc.html #:########################################################################################:# output: - platform: ledc pin: "${backlight_pin}" id: backlight_output frequency: "${backlight_frequency}" #:########################################################################################:# # LIGHT: # https://esphome.io/components/light/monochromatic.html #:########################################################################################:# light: - platform: monochromatic output: backlight_output name: "Backlight" id: display_backlight icon: "mdi:brightness-6" restore_mode: RESTORE_DEFAULT_ON default_transition_length: 500ms gamma_correct: 1.0 #:########################################################################################:# # DISPLAY: # https://esphome.io/components/display/mipi_dsi.html #:########################################################################################:# display: - platform: mipi_dsi model: JC1060P470 id: main_display color_order: RGB reset_pin: number: GPIO5 hsync_pulse_width: 20 update_interval: never auto_clear_enabled: false #:########################################################################################:# # TOUCHSCREEN: # https://esphome.io/components/touchscreen/gt911.html #:########################################################################################:# touchscreen: - platform: gt911 id: main_touchscreen display: main_display i2c_id: touch_i2c address: 0x5D interrupt_pin: number: "${touch_interrupt_pin}" mode: input: true reset_pin: number: "${touch_reset_pin}" mode: output: true #:########################################################################################:# # FONTS: # Reduced font set for lower startup memory use # https://esphome.io/components/font.html #:########################################################################################:# font: - file: "gfonts://Roboto" id: font_clock size: 280 glyphs: - "0123456789:" - file: "gfonts://Roboto" id: font_date size: 64 glyphs: - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789, " - file: "gfonts://Roboto" id: font_title size: 36 glyphs: - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789, .:/-%°+()" - file: "gfonts://Roboto" id: font_button size: 24 glyphs: - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789 .:/-%°+()" - file: "gfonts://Roboto" id: font_small size: 20 glyphs: - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789, .:/-%°+()" # Dedicated MDI font for reliable setpoint chevron icons. - file: "https://raw.githubusercontent.com/Templarian/MaterialDesign-Webfont/master/fonts/materialdesignicons-webfont.ttf" id: font_mdi_setpoint size: 48 bpp: 4 glyphs: - "\U000F0140" # mdi-chevron-down - "\U000F0143" # mdi-chevron-up #:########################################################################################:# # HOME ASSISTANT NUMERIC SENSORS: # https://esphome.io/components/sensor/homeassistant.html #:########################################################################################:# sensor: - platform: uptime name: "${friendly_name} Uptime" id: uptime_sensor update_interval: "${update_interval}" - platform: wifi_signal name: "${friendly_name} WiFi Signal" id: wifi_signal_sensor update_interval: "${update_interval}" - platform: homeassistant id: moon_phase entity_id: "${moon_phase_entity}" internal: true on_value: then: - lvgl.label.update: id: bedroom_moon_value_label text: format: "%.1f%%" args: [x] - lvgl.label.update: id: clock_moon_value_label text: format: "%.1f%%" args: [x] - platform: homeassistant id: outdoor_temperature entity_id: "${outdoor_temperature_entity}" internal: true on_value: then: - lvgl.label.update: id: bedroom_outdoor_value_label text: format: "%.1f°C" args: [x] - lvgl.label.update: id: clock_outdoor_value_label text: format: "%.1f°C" args: [x] - platform: homeassistant id: bedroom_temperature entity_id: "${bedroom_temperature_entity}" internal: true on_value: then: - lvgl.label.update: id: bedroom_temperature_label text: format: "%.1f°C" args: [x] - lvgl.label.update: id: clock_temperature_label text: format: "%.1f°C" args: [x] - platform: homeassistant id: bedroom_humidity entity_id: "${bedroom_humidity_entity}" internal: true on_value: then: - lvgl.label.update: id: bedroom_humidity_label text: format: "%.0f%%" args: [x] - lvgl.label.update: id: clock_humidity_label text: format: "%.0f%%" args: [x] # Current heat-pump target temperature. - platform: homeassistant id: ha_climate_setpoint entity_id: "${climate_entity}" attribute: temperature internal: true on_value: then: - script.execute: refresh_climate_controls # Current ceiling-fan percentage, normally 33 / 66 / 100. - platform: homeassistant id: ha_ceiling_fan_percentage entity_id: "${ceiling_fan_entity}" attribute: percentage internal: true on_value: then: - script.execute: refresh_climate_controls # Current bedroom air-filter speed level. - platform: homeassistant id: ha_air_filter_level entity_id: "${air_filter_level_entity}" internal: true on_value: then: - script.execute: refresh_climate_controls #:########################################################################################:# # HOME ASSISTANT BINARY SENSORS: # Imported states used to colour LVGL buttons #:########################################################################################:# binary_sensor: - platform: homeassistant id: ha_north_lights entity_id: "${north_lights_entity}" internal: true on_state: then: - lvgl.widget.update: id: north_lights_button bg_color: !lambda |- return x ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - platform: homeassistant id: ha_south_lights entity_id: "${south_lights_entity}" internal: true on_state: then: - lvgl.widget.update: id: south_lights_button bg_color: !lambda |- return x ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - platform: homeassistant id: ha_bedside_lamp entity_id: "${bedside_lamp_entity}" internal: true on_state: then: - lvgl.widget.update: id: bedside_lamp_button bg_color: !lambda |- return x ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - platform: homeassistant id: ha_hall_floor entity_id: "${hall_floor_entity}" internal: true on_state: then: - lvgl.widget.update: id: hall_floor_button bg_color: !lambda |- return x ? lv_color_hex(0x92702D) : lv_color_hex(0x28313D); - platform: homeassistant id: ha_hall_lights entity_id: "${hall_lights_entity}" internal: true on_state: then: - lvgl.widget.update: id: hall_lights_button bg_color: !lambda |- return x ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - platform: homeassistant id: ha_bathroom_lights entity_id: "${bathroom_lights_entity}" internal: true on_state: then: - lvgl.widget.update: id: bathroom_lights_button bg_color: !lambda |- return x ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - platform: homeassistant id: ha_patio_lights entity_id: "${patio_lights_entity}" internal: true on_state: then: - lvgl.widget.update: id: patio_lights_button bg_color: !lambda |- return x ? lv_color_hex(0xA87500) : lv_color_hex(0x28313D); - platform: homeassistant id: ha_garage_lights entity_id: "${garage_lights_entity}" internal: true on_state: then: - lvgl.widget.update: id: garage_lights_button bg_color: !lambda |- if (id(ha_garage_door).state) { return lv_color_hex(0xC25B13); } return x ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - platform: homeassistant id: ha_garage_door entity_id: "${garage_door_entity}" internal: true on_state: then: - lvgl.widget.update: id: garage_lights_button bg_color: !lambda |- if (x) { return lv_color_hex(0xC25B13); } return id(ha_garage_lights).state ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - platform: homeassistant id: ha_bathroom_fan entity_id: "${bathroom_fan_entity}" internal: true on_state: then: - lvgl.widget.update: id: bathroom_fan_button bg_color: !lambda |- return x ? lv_color_hex(0x1668A8) : lv_color_hex(0x28313D); - platform: homeassistant id: ha_ceiling_fan_on entity_id: "${ceiling_fan_entity}" internal: true on_state: then: - script.execute: refresh_climate_controls - platform: homeassistant id: ha_climate_night_power entity_id: "${climate_night_power_entity}" internal: true on_state: then: - script.execute: refresh_climate_controls - platform: homeassistant id: ha_nanoe entity_id: "${nanoe_entity}" internal: true on_state: then: - lambda: |- id(nanoe_optimistic) = -1; - script.execute: refresh_climate_controls - platform: homeassistant id: ha_quiet_time entity_id: "${quiet_time_entity}" internal: true on_state: then: - lvgl.widget.update: id: bedtime_button bg_color: !lambda |- return x ? lv_color_hex(0x705295) : lv_color_hex(0x3B3048); - lvgl.label.update: id: clock_status_label text: !lambda |- return x ? std::string("Quiet Time active") : std::string(""); #:########################################################################################:# # GLOBALS: # Local optimistic Heat Pump profile state #:########################################################################################:# globals: # Prevent state callbacks from touching widgets while LVGL creates pages. - id: lvgl_ready type: bool restore_value: false initial_value: "false" # -1 = follow HA, 0 = Normal, 1 = Quiet, 2 = Boost. - id: climate_profile_optimistic type: int restore_value: false initial_value: "-1" # -1 = follow HA, 0 = off, 1 = on. - id: nanoe_optimistic type: int restore_value: false initial_value: "-1" # Unix timestamp at which the 2-hour Sleep timer expires. # A value of 0 means that the timer is inactive. - id: sleep_timer_end_epoch type: uint32_t restore_value: true initial_value: "0" # True only while the Security page is visible. - id: security_page_active type: bool restore_value: false initial_value: "false" #:########################################################################################:# # SCRIPTS: # Refresh all state-dependent LVGL button colours #:########################################################################################:# script: # Update the API status indicator only after LVGL has finished constructing. - id: refresh_api_indicator mode: restart then: - if: condition: lambda: |- return id(lvgl_ready); then: - if: condition: api.connected: then: - lvgl.widget.update: id: bedroom_connection_indicator bg_color: 0x65D483 else: - lvgl.widget.update: id: bedroom_connection_indicator bg_color: 0xF06A6A # Immediate refresh using the latest imported Home Assistant states. - id: refresh_button_states mode: restart then: - if: condition: api.connected: then: - lvgl.widget.update: id: bedroom_connection_indicator bg_color: 0x65D483 else: - lvgl.widget.update: id: bedroom_connection_indicator bg_color: 0xF06A6A - lvgl.widget.update: id: north_lights_button bg_color: !lambda |- return id(ha_north_lights).state ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - lvgl.widget.update: id: south_lights_button bg_color: !lambda |- return id(ha_south_lights).state ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - lvgl.widget.update: id: bedside_lamp_button bg_color: !lambda |- return id(ha_bedside_lamp).state ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - lvgl.widget.update: id: hall_floor_button bg_color: !lambda |- return id(ha_hall_floor).state ? lv_color_hex(0x92702D) : lv_color_hex(0x28313D); - lvgl.widget.update: id: hall_lights_button bg_color: !lambda |- return id(ha_hall_lights).state ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - lvgl.widget.update: id: bathroom_lights_button bg_color: !lambda |- return id(ha_bathroom_lights).state ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - lvgl.widget.update: id: patio_lights_button bg_color: !lambda |- return id(ha_patio_lights).state ? lv_color_hex(0xA87500) : lv_color_hex(0x28313D); - lvgl.widget.update: id: garage_lights_button bg_color: !lambda |- if (id(ha_garage_door).state) { return lv_color_hex(0xC25B13); } return id(ha_garage_lights).state ? lv_color_hex(0xB88918) : lv_color_hex(0x28313D); - lvgl.widget.update: id: bathroom_fan_button bg_color: !lambda |- return id(ha_bathroom_fan).state ? lv_color_hex(0x1668A8) : lv_color_hex(0x28313D); - lvgl.widget.update: id: bedtime_button bg_color: !lambda |- return id(ha_quiet_time).state ? lv_color_hex(0x705295) : lv_color_hex(0x3B3048); - lvgl.label.update: id: clock_status_label text: !lambda |- return id(ha_quiet_time).state ? std::string("Quiet Time active") : std::string(""); - script.execute: refresh_climate_controls # Update Climate-page labels and highlight the currently selected settings. - id: refresh_climate_controls mode: restart then: - if: condition: lambda: |- return id(lvgl_ready); then: # Heat-pump mode buttons. The requested mode wins while a change is # pending; when it is "none", the actual climate state is used. - lvgl.widget.update: id: climate_off_button bg_color: !lambda |- const auto requested = id(ha_climate_requested_mode).state; const auto current = id(ha_climate_current_mode).state; const bool requested_none = ( requested.empty() || requested == "none" || requested == "None" || requested == "unknown" || requested == "unavailable" ); const bool active = ( requested == "off" || requested == "Off" || (requested_none && current == "off") ); return active ? lv_color_hex(0x548C6C) : lv_color_hex(0x28313D); - lvgl.widget.update: id: climate_heat_button bg_color: !lambda |- const auto requested = id(ha_climate_requested_mode).state; const auto current = id(ha_climate_current_mode).state; const bool requested_none = ( requested.empty() || requested == "none" || requested == "None" || requested == "unknown" || requested == "unavailable" ); const bool active = ( requested == "heat" || requested == "Heat" || (requested_none && current == "heat") ); return active ? lv_color_hex(0xB76822) : lv_color_hex(0x28313D); - lvgl.widget.update: id: climate_cool_button bg_color: !lambda |- const auto requested = id(ha_climate_requested_mode).state; const auto current = id(ha_climate_current_mode).state; const bool requested_none = ( requested.empty() || requested == "none" || requested == "None" || requested == "unknown" || requested == "unavailable" ); const bool active = ( requested == "cool" || requested == "Cool" || (requested_none && current == "cool") ); return active ? lv_color_hex(0x2E6A96) : lv_color_hex(0x28313D); - lvgl.widget.update: id: climate_dry_button bg_color: !lambda |- const auto requested = id(ha_climate_requested_mode).state; const auto current = id(ha_climate_current_mode).state; const bool requested_none = ( requested.empty() || requested == "none" || requested == "None" || requested == "unknown" || requested == "unavailable" ); const bool active = ( requested == "dry" || requested == "Dry" || requested == "dehumidify" || requested == "Dehumidify" || ( requested_none && ( current == "dry" || current == "dehumidify" ) ) ); return active ? lv_color_hex(0x32677F) : lv_color_hex(0x28313D); # Quiet / Boost are mutually exclusive. Optimistic state wins # immediately; actual Panasonic preset is authoritative afterwards. - lvgl.widget.update: id: climate_quiet_button bg_color: !lambda |- int selected = id(climate_profile_optimistic); if (selected < 0) { const auto preset = id(ha_climate_preset_mode).state; const auto helper = id(ha_climate_profile).state; if ( preset == "quiet" || preset == "Quiet" || helper == "Quiet" || helper == "quiet" ) { selected = 1; } else if ( preset == "powerful" || preset == "Powerful" || helper == "Boost" || helper == "boost" ) { selected = 2; } else { selected = 0; } } return selected == 1 ? lv_color_hex(0x315843) : lv_color_hex(0x28313D); - lvgl.widget.update: id: climate_boost_button bg_color: !lambda |- int selected = id(climate_profile_optimistic); if (selected < 0) { const auto preset = id(ha_climate_preset_mode).state; const auto helper = id(ha_climate_profile).state; if ( preset == "quiet" || preset == "Quiet" || helper == "Quiet" || helper == "quiet" ) { selected = 1; } else if ( preset == "powerful" || preset == "Powerful" || helper == "Boost" || helper == "boost" ) { selected = 2; } else { selected = 0; } } return selected == 2 ? lv_color_hex(0x6B4A86) : lv_color_hex(0x28313D); - lvgl.widget.update: id: climate_nanoe_button bg_color: !lambda |- bool active = id(ha_nanoe).state; if (id(nanoe_optimistic) >= 0) { active = id(nanoe_optimistic) == 1; } return active ? lv_color_hex(0x315E82) : lv_color_hex(0x28313D); # Heat-pump target setpoint. - lvgl.label.update: id: climate_setpoint_value_label text: !lambda |- const float value = id(ha_climate_setpoint).state; if (isnan(value)) { return std::string("--.-°C"); } char buffer[16]; snprintf(buffer, sizeof(buffer), "%.1f°C", value); return std::string(buffer); # One-button ceiling-fan cycle: Off -> Low -> Medium -> High -> Off. - lvgl.label.update: id: ceiling_fan_cycle_label text: !lambda |- if (!id(ha_ceiling_fan_on).state) { return std::string("Ceiling Fan: Off"); } const float percentage = id(ha_ceiling_fan_percentage).state; if (isnan(percentage) || percentage < 50.0f) { return std::string("Ceiling Fan: Low"); } if (percentage < 85.0f) { return std::string("Ceiling Fan: Medium"); } return std::string("Ceiling Fan: High"); - lvgl.widget.update: id: ceiling_fan_cycle_button bg_color: !lambda |- return id(ha_ceiling_fan_on).state ? lv_color_hex(0x315E82) : lv_color_hex(0x28313D); # Air Filter Quiet is highlighted for filter levels 0, 1 or 2. - lvgl.widget.update: id: air_filter_quiet_button bg_color: !lambda |- const float level = id(ha_air_filter_level).state; return ( !isnan(level) && level >= 0.0f && level <= 2.0f ) ? lv_color_hex(0x315843) : lv_color_hex(0x28313D); # Climate 9pm-12 follows the Night Power helper state. - lvgl.widget.update: id: climate_night_power_button bg_color: !lambda |- return id(ha_climate_night_power).state ? lv_color_hex(0x5A4778) : lv_color_hex(0x28313D); # Update the Sleep timer label and active button colour. - id: refresh_sleep_timer mode: restart then: - if: condition: lambda: |- return id(lvgl_ready); then: - lvgl.label.update: id: sleep_timer_button_label text: !lambda |- const auto now = id(ha_time).now(); if ( id(sleep_timer_end_epoch) == 0 || !now.is_valid() || now.timestamp >= id(sleep_timer_end_epoch) ) { return std::string("Sleep: 2 hrs (0)"); } const uint32_t seconds_remaining = id(sleep_timer_end_epoch) - now.timestamp; const uint32_t minutes_remaining = (seconds_remaining + 59) / 60; char buffer[32]; snprintf( buffer, sizeof(buffer), "Sleep: 2 hrs (%u)", static_cast(minutes_remaining) ); return std::string(buffer); - lvgl.widget.update: id: sleep_timer_button bg_color: !lambda |- const auto now = id(ha_time).now(); const bool active = ( id(sleep_timer_end_epoch) > 0 && now.is_valid() && now.timestamp < id(sleep_timer_end_epoch) ); return active ? lv_color_hex(0x6B4A86) : lv_color_hex(0x28313D); # Download a current Doorbell image through the authenticated proxy path # supplied by the Home Assistant camera entity. - id: refresh_security_camera mode: restart then: - if: condition: lambda: |- return ( id(lvgl_ready) && id(security_page_active) ); then: - if: condition: lambda: |- const auto path = id(ha_doorbell_entity_picture).state; return ( !path.empty() && path != "unknown" && path != "unavailable" ); then: - lvgl.label.update: id: security_camera_status_label text: "Loading Doorbell..." - online_image.set_url: id: security_camera_image url: !lambda |- const std::string path = id(ha_doorbell_entity_picture).state; std::string url; if ( path.rfind("http://", 0) == 0 || path.rfind("https://", 0) == 0 ) { url = path; } else { url = std::string( "${home_assistant_internal_url}" ) + path; } char suffix[32]; snprintf( suffix, sizeof(suffix), "%ctime=%lu", url.find('?') == std::string::npos ? '?' : '&', static_cast(millis()) ); return url + suffix; else: - lvgl.label.update: id: security_camera_status_label text: "Waiting for Doorbell camera" - id: release_climate_profile_optimistic mode: restart then: - delay: 5s - lambda: |- id(climate_profile_optimistic) = -1; - script.execute: refresh_climate_controls - id: release_nanoe_optimistic mode: restart then: - delay: 5s - lambda: |- id(nanoe_optimistic) = -1; - script.execute: refresh_climate_controls # Two passes allow time for LVGL creation and Home Assistant state delivery. - id: startup_button_state_sync mode: restart then: - delay: 1500ms - script.execute: refresh_button_states - delay: 3500ms - script.execute: refresh_button_states - script.execute: refresh_sleep_timer #:########################################################################################:# # INTERVAL: # Update minute-resolution clock values locally #:########################################################################################:# interval: - interval: 10s then: - if: condition: lambda: |- return id(ha_time).now().is_valid(); then: - lvgl.label.update: id: clock_time_label text: !lambda |- auto now = id(ha_time).now(); char buffer[8]; now.strftime(buffer, sizeof(buffer), "%H:%M"); return std::string(buffer); - lvgl.label.update: id: clock_date_label text: !lambda |- auto now = id(ha_time).now(); char weekday[16]; char month[16]; now.strftime(weekday, sizeof(weekday), "%A"); now.strftime(month, sizeof(month), "%B"); char buffer[48]; snprintf( buffer, sizeof(buffer), "%s, %d %s", weekday, now.day_of_month, month ); return std::string(buffer); # Maintain the local two-hour Sleep countdown. - if: condition: lambda: |- const auto now = id(ha_time).now(); return ( id(sleep_timer_end_epoch) > 0 && now.is_valid() && now.timestamp >= id(sleep_timer_end_epoch) ); then: - lambda: |- id(sleep_timer_end_epoch) = 0; - homeassistant.action: action: script.turn_on data: entity_id: "${climate_off_script}" - script.execute: refresh_sleep_timer else: - script.execute: refresh_sleep_timer - interval: "${security_camera_refresh_interval}" then: - if: condition: lambda: |- return id(security_page_active); then: - script.execute: refresh_security_camera #:########################################################################################:# # TEXT SENSOR COMPONENT: # https://esphome.io/components/text_sensor/ #:########################################################################################:# text_sensor: # 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 a short-lived authenticated camera proxy URL in # the 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); then: - script.execute: refresh_security_camera - platform: version name: "${friendly_name} ESPHome Version" hide_timestamp: true - platform: wifi_info ip_address: name: "${friendly_name} IP Address" ssid: name: "${friendly_name} Connected SSID" bssid: name: "${friendly_name} Connected BSSID" mac_address: name: "${friendly_name} MAC Address" #:########################################################################################:# # HTTP REQUEST: # Used by the online Doorbell image # https://esphome.io/components/http_request/ #:########################################################################################:# http_request: timeout: 10s follow_redirects: true #:########################################################################################:# # ONLINE SECURITY CAMERA IMAGE: # Home Assistant camera proxy image, resized for the Security page # https://esphome.io/components/image/online_image/ #:########################################################################################:# image: - platform: online_image id: security_camera_image url: "${home_assistant_internal_url}/" format: JPEG type: RGB565 resize: 640x360 update_interval: never buffer_size: 32768 on_download_finished: then: - if: condition: lambda: |- return ( id(lvgl_ready) && id(security_page_active) ); then: - lvgl.image.update: id: security_camera_widget src: security_camera_image - lvgl.label.update: id: security_camera_status_label text: "" on_error: then: - if: condition: lambda: |- return ( id(lvgl_ready) && id(security_page_active) ); then: - lvgl.label.update: id: security_camera_status_label text: "Camera image unavailable" #:########################################################################################:# # LVGL: # https://esphome.io/components/lvgl/ #:########################################################################################:# lvgl: id: panel_lvgl displays: - main_display touchscreens: - touchscreen_id: main_touchscreen long_press_time: 1200ms long_press_repeat_time: 500ms rotation: 0 buffer_size: 6% log_level: WARN default_font: font_button page_wrap: false theme: dark_mode: true on_idle: - timeout: "${clock_idle_timeout}" then: - lambda: |- id(security_page_active) = false; - online_image.release: security_camera_image - lvgl.page.show: id: clock_page animation: FADE_IN time: 250ms - light.turn_on: id: display_backlight brightness: "${clock_brightness}" transition_length: 1500ms pages: #:####################################################################################:# # BEDROOM PAGE #:####################################################################################:# - id: bedroom_page bg_color: 0x10151D bg_opa: COVER widgets: # Home Assistant API status: # Green = connected, red = disconnected. - obj: id: bedroom_connection_indicator x: 30 y: 25 width: 18 height: 18 radius: 9 bg_color: 0xF06A6A bg_opa: COVER border_width: 0 shadow_width: 0 clickable: false # Four status readings across the top - label: x: 355 y: 12 width: 150 text: "Moon" text_font: font_small text_align: CENTER text_color: 0xA9A0D4 - label: id: bedroom_moon_value_label x: 355 y: 39 width: 150 text: "--.-%" text_font: font_button text_align: CENTER text_color: 0xC6BDEC - label: x: 510 y: 12 width: 150 text: "Outdoor" text_font: font_small text_align: CENTER text_color: 0x84B5D8 - label: id: bedroom_outdoor_value_label x: 510 y: 39 width: 150 text: "--.-°C" text_font: font_button text_align: CENTER text_color: 0x9CCBEA - label: x: 665 y: 12 width: 150 text: "Indoor" text_font: font_small text_align: CENTER text_color: 0xD6B78F - label: id: bedroom_temperature_label x: 665 y: 39 width: 150 text: "--.-°C" text_font: font_button text_align: CENTER text_color: 0xE8C79D - label: x: 820 y: 12 width: 165 text: "Humidity" text_font: font_small text_align: CENTER text_color: 0x80BDA4 - label: id: bedroom_humidity_label x: 820 y: 39 width: 165 text: "--%" text_font: font_button text_align: CENTER text_color: 0x97D2B7 - button: id: north_lights_button x: 35 y: 95 width: 300 height: 105 widgets: - label: id: north_lights_button_label align: CENTER 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: 362 y: 95 width: 300 height: 105 widgets: - label: id: south_lights_button_label align: CENTER 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: id: bedside_lamp_button_label align: CENTER 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: 35 y: 218 width: 300 height: 105 widgets: - label: id: hall_floor_button_label align: CENTER 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: id: hall_lights_button_label align: CENTER 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: 689 y: 218 width: 300 height: 105 widgets: - label: id: bathroom_lights_button_label align: CENTER 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: 35 y: 341 width: 300 height: 105 widgets: - label: id: patio_lights_button_label align: CENTER 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: 362 y: 341 width: 300 height: 105 widgets: - label: id: garage_lights_button_label align: CENTER 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: - homeassistant.action: action: switch.toggle data: entity_id: "${garage_lights_entity}" - button: id: bathroom_fan_button x: 689 y: 341 width: 300 height: 105 widgets: - label: id: bathroom_fan_button_label align: CENTER text: "Bathroom Fan" text_font: font_button text_color: 0xFFFFFF bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x445365 radius: 18 pressed: bg_color: 0x425166 on_click: - homeassistant.action: action: switch.toggle data: entity_id: "${bathroom_fan_entity}" #:################################################################################:# # BEDROOM PAGE BOTTOM NAVIGATION #:################################################################################:# - button: id: bedtime_button x: 18 y: 475 width: 190 height: 92 widgets: - label: id: bedtime_button_label align: CENTER text: "Bedtime (Hold)" text_font: font_button text_color: 0xFFFFFF bg_color: 0x3B3048 bg_opa: COVER border_width: 2 border_color: 0x735E88 radius: 16 pressed: bg_color: 0x584668 on_long_press: then: - homeassistant.action: action: input_button.press data: entity_id: "${bedtime_button_entity}" - lvgl.label.update: id: bedtime_button_label text: "Activated" - delay: 2s - lvgl.label.update: id: bedtime_button_label text: "Bedtime (Hold)" - button: id: climate_navigation_button x: 216 y: 475 width: 190 height: 92 widgets: - label: id: climate_navigation_button_label align: CENTER text: "Climate" text_font: font_button text_color: 0xFFFFFF bg_color: 0x315843 bg_opa: COVER border_width: 2 border_color: 0x548C6C radius: 16 pressed: bg_color: 0x416F55 on_click: - lvgl.page.show: id: climate_page animation: MOVE_LEFT time: 220ms - button: id: future_1_navigation_button x: 414 y: 475 width: 190 height: 92 widgets: - label: id: future_1_navigation_button_label align: CENTER text: "Future1" text_font: font_button text_color: 0xAEB8C4 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x445365 radius: 16 pressed: bg_color: 0x34404F - button: id: security_navigation_button x: 612 y: 475 width: 190 height: 92 widgets: - label: id: security_navigation_button_label align: CENTER text: "Security" text_font: font_button text_color: 0xFFFFFF bg_color: 0x493535 bg_opa: COVER border_width: 2 border_color: 0x805858 radius: 16 pressed: bg_color: 0x654949 on_click: - lambda: |- id(security_page_active) = true; - lvgl.page.show: id: security_page animation: MOVE_LEFT time: 220ms - light.turn_on: id: display_backlight brightness: "${control_brightness}" transition_length: 250ms - script.execute: refresh_security_camera - button: id: clock_now_button x: 810 y: 475 width: 190 height: 92 widgets: - label: id: clock_now_button_label align: CENTER text: "Clock" text_font: font_button text_color: 0xFFFFFF bg_color: 0x214F72 bg_opa: COVER border_width: 2 border_color: 0x3E7FA7 radius: 16 pressed: bg_color: 0x2E6A96 on_click: - lvgl.page.show: id: clock_page animation: FADE_IN time: 250ms - light.turn_on: id: display_backlight brightness: "${clock_brightness}" transition_length: 1500ms #:####################################################################################:# # CLIMATE PAGE #:####################################################################################:# - id: climate_page bg_color: 0x111923 bg_opa: COVER widgets: - label: x: 25 y: 41 width: 220 text: "Heat Pump" text_font: font_small text_align: CENTER text_color: 0x8FA0B3 #:################################################################################:# # HEAT-PUMP MODES #:################################################################################:# - button: id: climate_off_button x: 25 y: 72 width: 220 height: 96 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x5A7A68 radius: 17 pressed: bg_color: 0x548C6C widgets: - label: align: CENTER text: "Off" text_font: font_button text_color: 0xFFFFFF on_click: - homeassistant.action: action: script.turn_on data: entity_id: "${climate_off_script}" - button: id: climate_heat_button x: 272 y: 72 width: 220 height: 96 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0xA6672B radius: 17 pressed: bg_color: 0xB76822 widgets: - label: align: CENTER text: "Heat" text_font: font_button text_color: 0xFFFFFF on_click: - homeassistant.action: action: script.turn_on data: entity_id: "${climate_heat_script}" - button: id: climate_cool_button x: 519 y: 72 width: 220 height: 96 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x3E7FA7 radius: 17 pressed: bg_color: 0x2E6A96 widgets: - label: align: CENTER text: "Cool" text_font: font_button text_color: 0xFFFFFF on_click: - homeassistant.action: action: script.turn_on data: entity_id: "${climate_cool_script}" - button: id: climate_dry_button x: 766 y: 72 width: 220 height: 96 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x427B96 radius: 17 pressed: bg_color: 0x32677F widgets: - label: align: CENTER text: "Dehumid" text_font: font_button text_color: 0xFFFFFF on_click: - homeassistant.action: action: script.turn_on data: entity_id: "${climate_dry_script}" #:################################################################################:# # HEAT-PUMP PROFILE / NANO-X / SETPOINT #:################################################################################:# - label: x: 625 y: 187 width: 230 text: "Setpoint" text_font: font_small text_align: CENTER text_color: 0x8FA0B3 - button: id: climate_quiet_button x: 25 y: 218 width: 145 height: 105 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x548C6C radius: 18 pressed: bg_color: 0x315843 widgets: - label: align: CENTER text: "Quiet" text_font: font_button text_color: 0xFFFFFF on_click: # Quiet toggles between Quiet and Normal. - lambda: |- int selected = id(climate_profile_optimistic); if (selected < 0) { const auto preset = id(ha_climate_preset_mode).state; const auto helper = id(ha_climate_profile).state; if ( preset == "quiet" || preset == "Quiet" || helper == "Quiet" || helper == "quiet" ) { selected = 1; } else if ( preset == "powerful" || preset == "Powerful" || helper == "Boost" || helper == "boost" ) { selected = 2; } else { selected = 0; } } id(climate_profile_optimistic) = selected == 1 ? 0 : 1; - script.execute: refresh_climate_controls - if: condition: lambda: |- return id(climate_profile_optimistic) == 1; then: - homeassistant.action: action: input_select.select_option data: entity_id: "${climate_profile_entity}" option: "Quiet" - homeassistant.action: action: climate.set_preset_mode data: entity_id: "${climate_entity}" preset_mode: "quiet" else: - homeassistant.action: action: input_select.select_option data: entity_id: "${climate_profile_entity}" option: "Normal" - homeassistant.action: action: climate.set_preset_mode data: entity_id: "${climate_entity}" preset_mode: "none" - script.execute: release_climate_profile_optimistic - button: id: climate_boost_button x: 180 y: 218 width: 145 height: 105 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x80619B radius: 18 pressed: bg_color: 0x6B4A86 widgets: - label: align: CENTER text: "Boost" text_font: font_button text_color: 0xFFFFFF on_click: # Boost toggles between Boost and Normal. - lambda: |- int selected = id(climate_profile_optimistic); if (selected < 0) { const auto preset = id(ha_climate_preset_mode).state; const auto helper = id(ha_climate_profile).state; if ( preset == "quiet" || preset == "Quiet" || helper == "Quiet" || helper == "quiet" ) { selected = 1; } else if ( preset == "powerful" || preset == "Powerful" || helper == "Boost" || helper == "boost" ) { selected = 2; } else { selected = 0; } } id(climate_profile_optimistic) = selected == 2 ? 0 : 2; - script.execute: refresh_climate_controls - if: condition: lambda: |- return id(climate_profile_optimistic) == 2; then: - homeassistant.action: action: input_select.select_option data: entity_id: "${climate_profile_entity}" option: "Boost" - homeassistant.action: action: climate.set_preset_mode data: entity_id: "${climate_entity}" preset_mode: "powerful" else: - homeassistant.action: action: input_select.select_option data: entity_id: "${climate_profile_entity}" option: "Normal" - homeassistant.action: action: climate.set_preset_mode data: entity_id: "${climate_entity}" preset_mode: "none" - script.execute: release_climate_profile_optimistic - button: id: climate_nanoe_button x: 335 y: 218 width: 145 height: 105 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x47789B radius: 18 pressed: bg_color: 0x315E82 widgets: - label: align: CENTER text: "Nano-X" text_font: font_button text_color: 0xFFFFFF on_click: - lambda: |- bool active = id(ha_nanoe).state; if (id(nanoe_optimistic) >= 0) { active = id(nanoe_optimistic) == 1; } id(nanoe_optimistic) = active ? 0 : 1; - script.execute: refresh_climate_controls - if: condition: lambda: |- return id(nanoe_optimistic) == 1; then: - homeassistant.action: action: switch.turn_on data: entity_id: "${nanoe_entity}" else: - homeassistant.action: action: switch.turn_off data: entity_id: "${nanoe_entity}" - script.execute: release_nanoe_optimistic - button: id: climate_setpoint_down_button x: 505 y: 218 width: 110 height: 105 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x526276 radius: 18 pressed: bg_color: 0x38485A widgets: - label: align: CENTER text: "\U000F0140" text_font: font_mdi_setpoint text_color: 0xFFFFFF on_click: - homeassistant.action: action: "${climate_setpoint_adjust_script}" data: step: "-0.5" - button: id: climate_setpoint_display x: 625 y: 218 width: 230 height: 105 bg_color: 0x1C2632 bg_opa: COVER border_width: 2 border_color: 0x5A6C82 radius: 18 widgets: - label: id: climate_setpoint_value_label align: CENTER text: "--.-°C" text_font: font_title text_color: 0xFFFFFF - button: id: climate_setpoint_up_button x: 865 y: 218 width: 120 height: 105 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x526276 radius: 18 pressed: bg_color: 0x38485A widgets: - label: align: CENTER text: "\U000F0143" text_font: font_mdi_setpoint text_color: 0xFFFFFF on_click: - homeassistant.action: action: "${climate_setpoint_adjust_script}" data: step: "0.5" #:################################################################################:# # CEILING FAN / AIR FILTER / NIGHT POWER / SLEEP TIMER #:################################################################################:# - button: id: ceiling_fan_cycle_button x: 25 y: 356 width: 220 height: 100 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x47789B radius: 18 pressed: bg_color: 0x315E82 widgets: - label: id: ceiling_fan_cycle_label align: CENTER text: "Ceiling Fan: Off" text_font: font_small text_color: 0xFFFFFF on_click: # Off -> Low - if: condition: lambda: |- return !id(ha_ceiling_fan_on).state; then: - homeassistant.action: action: fan.set_percentage data: entity_id: "${ceiling_fan_entity}" percentage: "33" else: # Low -> Medium - if: condition: lambda: |- const float percentage = id(ha_ceiling_fan_percentage).state; return ( isnan(percentage) || percentage < 50.0f ); then: - homeassistant.action: action: fan.set_percentage data: entity_id: "${ceiling_fan_entity}" percentage: "66" else: # Medium -> High - if: condition: lambda: |- return ( id(ha_ceiling_fan_percentage).state < 85.0f ); then: - homeassistant.action: action: fan.set_percentage data: entity_id: "${ceiling_fan_entity}" percentage: "100" # High -> Off else: - homeassistant.action: action: fan.turn_off data: entity_id: "${ceiling_fan_entity}" - button: id: air_filter_quiet_button x: 272 y: 356 width: 220 height: 100 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x548C6C radius: 18 pressed: bg_color: 0x315843 widgets: - label: align: CENTER text: "Air Filter: Quiet" text_font: font_small text_color: 0xFFFFFF on_click: - homeassistant.action: action: number.set_value data: entity_id: "${air_filter_level_entity}" value: "2" - button: id: climate_night_power_button x: 519 y: 356 width: 220 height: 100 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x80619B radius: 18 pressed: bg_color: 0x5A4778 widgets: - label: align: CENTER text: "Climate: 9pm-12am" text_font: font_small text_color: 0xFFFFFF on_click: - homeassistant.action: action: input_boolean.toggle data: entity_id: "${climate_night_power_entity}" - button: id: sleep_timer_button x: 766 y: 356 width: 220 height: 100 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x80619B radius: 18 pressed: bg_color: 0x6B4A86 widgets: - label: id: sleep_timer_button_label align: CENTER text: "Sleep: 2 hrs (0)" text_font: font_small text_color: 0xFFFFFF on_click: # Pressing an active Sleep timer cancels it. Otherwise, start # a new 120-minute countdown. - if: condition: lambda: |- const auto now = id(ha_time).now(); return ( id(sleep_timer_end_epoch) > 0 && now.is_valid() && now.timestamp < id(sleep_timer_end_epoch) ); then: - lambda: |- id(sleep_timer_end_epoch) = 0; else: - if: condition: lambda: |- return id(ha_time).now().is_valid(); then: - lambda: |- const auto now = id(ha_time).now(); id(sleep_timer_end_epoch) = now.timestamp + (120 * 60); - script.execute: refresh_sleep_timer #:################################################################################:# # BOTTOM NAVIGATION #:################################################################################:# - button: id: climate_back_button x: 612 y: 475 width: 190 height: 92 bg_color: 0x315843 bg_opa: COVER border_width: 2 border_color: 0x548C6C radius: 16 pressed: bg_color: 0x416F55 widgets: - label: align: CENTER text: "Back" text_font: font_button text_color: 0xFFFFFF on_click: - lvgl.page.show: id: bedroom_page animation: MOVE_RIGHT time: 220ms - button: id: climate_clock_button x: 810 y: 475 width: 190 height: 92 bg_color: 0x214F72 bg_opa: COVER border_width: 2 border_color: 0x3E7FA7 radius: 16 pressed: bg_color: 0x2E6A96 widgets: - label: align: CENTER text: "Clock" text_font: font_button text_color: 0xFFFFFF on_click: - lvgl.page.show: id: clock_page animation: FADE_IN time: 250ms - light.turn_on: id: display_backlight brightness: "${clock_brightness}" transition_length: 1500ms #:####################################################################################:# # SECURITY PAGE #:####################################################################################:# - id: security_page skip: true bg_color: 0x0D1218 bg_opa: COVER widgets: #:################################################################################:# # CAMERA SELECTION #:################################################################################:# - button: id: security_doorbell_button x: 25 y: 18 width: 220 height: 70 bg_color: 0x493535 bg_opa: COVER border_width: 2 border_color: 0x9A6666 radius: 16 pressed: bg_color: 0x654949 widgets: - label: align: CENTER text: "Doorbell" text_font: font_button text_color: 0xFFFFFF on_click: - script.execute: refresh_security_camera - button: id: security_camera_2_button x: 272 y: 18 width: 220 height: 70 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x445365 radius: 16 widgets: - label: align: CENTER text: "Camera2" text_font: font_button text_color: 0x8D98A6 - button: id: security_camera_3_button x: 519 y: 18 width: 220 height: 70 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x445365 radius: 16 widgets: - label: align: CENTER text: "Camera3" text_font: font_button text_color: 0x8D98A6 - button: id: security_camera_4_button x: 766 y: 18 width: 220 height: 70 bg_color: 0x28313D bg_opa: COVER border_width: 2 border_color: 0x445365 radius: 16 widgets: - label: align: CENTER text: "Camera4" text_font: font_button text_color: 0x8D98A6 #:################################################################################:# # 640x360 CAMERA IMAGE #:################################################################################:# - obj: x: 190 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: 192 y: 100 width: 640 height: 360 src: security_camera_image radius: 11 clip_corner: true clickable: false - label: id: security_camera_status_label x: 192 y: 260 width: 640 text: "Waiting for Doorbell camera" text_font: font_button text_align: CENTER text_color: 0xD4D8DE clickable: false #:################################################################################:# # BOTTOM NAVIGATION #:################################################################################:# - button: id: security_back_button x: 612 y: 475 width: 190 height: 92 bg_color: 0x315843 bg_opa: COVER border_width: 2 border_color: 0x548C6C radius: 16 pressed: bg_color: 0x416F55 widgets: - label: align: CENTER text: "Back" text_font: font_button text_color: 0xFFFFFF on_click: - lambda: |- id(security_page_active) = false; - online_image.release: security_camera_image - lvgl.page.show: id: bedroom_page animation: MOVE_RIGHT time: 220ms - button: id: security_clock_button x: 810 y: 475 width: 190 height: 92 bg_color: 0x214F72 bg_opa: COVER border_width: 2 border_color: 0x3E7FA7 radius: 16 pressed: bg_color: 0x2E6A96 widgets: - label: align: CENTER text: "Clock" text_font: font_button text_color: 0xFFFFFF on_click: - lambda: |- id(security_page_active) = false; - online_image.release: security_camera_image - lvgl.page.show: id: clock_page animation: FADE_IN time: 250ms - light.turn_on: id: display_backlight brightness: "${clock_brightness}" transition_length: 1500ms #:####################################################################################:# # CLOCK PAGE #:####################################################################################:# - id: clock_page skip: true bg_color: 0x020304 bg_opa: COVER widgets: - button: id: clock_wake_button x: 0 y: 0 width: 1024 height: 600 bg_opa: TRANSP border_width: 0 shadow_width: 0 radius: 0 text: "" on_click: then: - lvgl.page.show: id: bedroom_page animation: FADE_OUT time: 250ms - light.turn_on: id: display_backlight brightness: "${control_brightness}" transition_length: 500ms - label: id: clock_time_label align: TOP_MID y: -5 text: "--:--" text_font: font_clock text_color: 0xE4DDD2 clickable: false - label: id: clock_date_label align: TOP_MID y: 315 text: "Waiting for time" text_font: font_date text_color: 0xA8A198 clickable: false # Four status readings - label: x: 80 y: 455 width: 205 text: "Moon" text_font: font_small text_align: CENTER text_color: 0xA9A0D4 clickable: false - label: id: clock_moon_value_label x: 80 y: 490 width: 205 text: "--.-%" text_font: font_title text_align: CENTER text_color: 0xC6BDEC clickable: false - label: x: 285 y: 455 width: 205 text: "Outdoor" text_font: font_small text_align: CENTER text_color: 0x84B5D8 clickable: false - label: id: clock_outdoor_value_label x: 285 y: 490 width: 205 text: "--.-°C" text_font: font_title text_align: CENTER text_color: 0x9CCBEA clickable: false - label: x: 490 y: 455 width: 205 text: "Indoor" text_font: font_small text_align: CENTER text_color: 0xD6B78F clickable: false - label: id: clock_temperature_label x: 490 y: 490 width: 205 text: "--.-°C" text_font: font_title text_align: CENTER text_color: 0xE8C79D clickable: false - label: x: 695 y: 455 width: 245 text: "Humidity" text_font: font_small text_align: CENTER text_color: 0x80BDA4 clickable: false - label: id: clock_humidity_label x: 695 y: 490 width: 245 text: "--%" text_font: font_title text_align: CENTER text_color: 0x97D2B7 clickable: false - label: id: clock_status_label align: BOTTOM_MID y: -8 text: "" text_font: font_small text_color: 0x887799 clickable: false