diff --git a/esphome/esp-bedside-panel.yaml b/esphome/esp-bedside-panel.yaml index 03267e4..fb400e3 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.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 @@ -74,7 +75,9 @@ # - Touching the clock: # - Returns to the Bedroom controls. # - Restores normal brightness. -# - The Climate page contains working Off, Heat, Cool and Dehumid controls. +# - 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. @@ -91,12 +94,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. Larger bedside clock and date with lower environment status row. (Layout V1.1)" + description_comment: "Guition JC1060P470C_I_W_Y ESP32-P4 bedside control panel. V1.0 Bedroom, Climate and Clock release with local 2-hour Sleep timer. (Layout V1.1)" device_area: "Bedroom" # Project Naming project_name: "Guition.JC1060P470C_I_W_Y" - project_version: "v0.8u" + project_version: "v1.0" # Passwords & Secrets api_key: !secret esp-api_key @@ -223,6 +226,7 @@ esphome: - script.execute: refresh_api_indicator - script.execute: refresh_button_states + - script.execute: refresh_sleep_timer #:########################################################################################:# # ESP PLATFORM AND FRAMEWORK: @@ -783,6 +787,13 @@ globals: 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" + #:########################################################################################:# # SCRIPTS: # Refresh all state-dependent LVGL button colours @@ -1121,7 +1132,7 @@ script: snprintf(buffer, sizeof(buffer), "%.1f°C", value); return std::string(buffer); - # One-button ceiling-fan cycle: Off -> 1 -> 2 -> 3 -> Off. + # One-button ceiling-fan cycle: Off -> Low -> Medium -> High -> Off. - lvgl.label.update: id: ceiling_fan_cycle_label text: !lambda |- @@ -1132,14 +1143,14 @@ script: const float percentage = id(ha_ceiling_fan_percentage).state; if (isnan(percentage) || percentage < 50.0f) { - return std::string("Ceiling Fan: 1"); + return std::string("Ceiling Fan: Low"); } if (percentage < 85.0f) { - return std::string("Ceiling Fan: 2"); + return std::string("Ceiling Fan: Medium"); } - return std::string("Ceiling Fan: 3"); + return std::string("Ceiling Fan: High"); - lvgl.widget.update: id: ceiling_fan_cycle_button @@ -1170,6 +1181,60 @@ script: ? 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); + - id: release_climate_profile_optimistic mode: restart then: @@ -1199,6 +1264,7 @@ script: - delay: 3500ms - script.execute: refresh_button_states + - script.execute: refresh_sleep_timer #:########################################################################################:# # INTERVAL: @@ -1246,6 +1312,32 @@ interval: 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 + #:########################################################################################:# # TEXT SENSOR COMPONENT: # https://esphome.io/components/text_sensor/ @@ -2249,13 +2341,13 @@ lvgl: step: "0.5" #:################################################################################:# - # CEILING FAN / AIR FILTER / NIGHT POWER + # CEILING FAN / AIR FILTER / NIGHT POWER / SLEEP TIMER #:################################################################################:# - button: id: ceiling_fan_cycle_button x: 25 y: 356 - width: 306 + width: 220 height: 100 bg_color: 0x28313D bg_opa: COVER @@ -2269,10 +2361,10 @@ lvgl: id: ceiling_fan_cycle_label align: CENTER text: "Ceiling Fan: Off" - text_font: font_button + text_font: font_small text_color: 0xFFFFFF on_click: - # Off -> speed 1 + # Off -> Low - if: condition: lambda: |- @@ -2286,7 +2378,7 @@ lvgl: percentage: "33" else: - # Speed 1 -> speed 2 + # Low -> Medium - if: condition: lambda: |- @@ -2306,7 +2398,7 @@ lvgl: percentage: "66" else: - # Speed 2 -> speed 3 + # Medium -> High - if: condition: lambda: |- @@ -2322,7 +2414,7 @@ lvgl: entity_id: "${ceiling_fan_entity}" percentage: "100" - # Speed 3 -> Off + # High -> Off else: - homeassistant.action: action: fan.turn_off @@ -2331,9 +2423,9 @@ lvgl: - button: id: air_filter_quiet_button - x: 352 + x: 272 y: 356 - width: 306 + width: 220 height: 100 bg_color: 0x28313D bg_opa: COVER @@ -2345,8 +2437,8 @@ lvgl: widgets: - label: align: CENTER - text: "Air Filter Quiet" - text_font: font_button + text: "Air Filter: Quiet" + text_font: font_small text_color: 0xFFFFFF on_click: - homeassistant.action: @@ -2357,9 +2449,9 @@ lvgl: - button: id: climate_night_power_button - x: 679 + x: 519 y: 356 - width: 307 + width: 220 height: 100 bg_color: 0x28313D bg_opa: COVER @@ -2371,8 +2463,8 @@ lvgl: widgets: - label: align: CENTER - text: "Climate 9pm-12" - text_font: font_button + text: "Climate: 9pm-12am" + text_font: font_small text_color: 0xFFFFFF on_click: - homeassistant.action: @@ -2380,6 +2472,59 @@ lvgl: 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 #:################################################################################:#