diff --git a/esphome/esp-bedside-panel.yaml b/esphome/esp-bedside-panel.yaml index c89ce9d..5a30957 100644 --- a/esphome/esp-bedside-panel.yaml +++ b/esphome/esp-bedside-panel.yaml @@ -6,6 +6,7 @@ # https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-bedside-panel.yaml #:########################################################################################:# # VERSIONS: +# V1.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 @@ -96,12 +97,12 @@ substitutions: # Device Naming device_name: "esp-bedside-panel" friendly_name: "ESP Bedside Panel" - description_comment: "Guition JC1060P470C_I_W_Y ESP32-P4 bedside control panel. Simplified Climate heading layout with aligned Heat Pump label. (Layout V1.1)" + 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.2" + project_version: "v1.3" # Passwords & Secrets api_key: !secret esp-api_key @@ -126,6 +127,12 @@ substitutions: 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" @@ -796,6 +803,12 @@ globals: 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 @@ -1237,6 +1250,75 @@ script: ? 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: @@ -1340,6 +1422,16 @@ interval: 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/ @@ -1383,6 +1475,23 @@ text_sensor: 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 @@ -1400,6 +1509,64 @@ text_sensor: 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/ @@ -1427,6 +1594,11 @@ lvgl: 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 @@ -1863,25 +2035,41 @@ lvgl: bg_color: 0x34404F - button: - id: future_2_navigation_button + id: security_navigation_button x: 612 y: 475 width: 190 height: 92 widgets: - label: - id: future_2_navigation_button_label + id: security_navigation_button_label align: CENTER - text: "Future2" + text: "Security" text_font: font_button - text_color: 0xAEB8C4 - bg_color: 0x28313D + text_color: 0xFFFFFF + bg_color: 0x493535 bg_opa: COVER border_width: 2 - border_color: 0x445365 + border_color: 0x805858 radius: 16 pressed: - bg_color: 0x34404F + 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 @@ -2579,6 +2767,200 @@ lvgl: 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 #:####################################################################################:#