diff --git a/components/nspanel_easy/page_screensaver.cpp b/components/nspanel_easy/page_screensaver.cpp new file mode 100644 index 00000000..cda3e085 --- /dev/null +++ b/components/nspanel_easy/page_screensaver.cpp @@ -0,0 +1,19 @@ +// page_screensaver.cpp + +#ifdef NSPANEL_EASY_PAGE_SCREENSAVER + +#include "page_screensaver.h" + +/** + * @file page_screensaver.cpp + * @brief Definitions for the Screensaver page. + * + * Provides the out-of-line definitions for variables declared in + * page_screensaver.h. + */ + +namespace esphome { +namespace nspanel_easy {} // namespace nspanel_easy +} // namespace esphome + +#endif // NSPANEL_EASY_PAGE_SCREENSAVER diff --git a/components/nspanel_easy/page_screensaver.h b/components/nspanel_easy/page_screensaver.h new file mode 100644 index 00000000..71aa99b2 --- /dev/null +++ b/components/nspanel_easy/page_screensaver.h @@ -0,0 +1,57 @@ +// page_screensaver.h + +#pragma once + +#ifdef NSPANEL_EASY_PAGE_SCREENSAVER + +#include "nextion_components.h" // For HMIComponent +#include "pages.h" // For page_names and get_page_id + +/** + * @file page_screensaver.h + * @brief Nextion component definitions for the Screensaver page. + * + * This file contains all component constants specific to the Screensaver page + * of the NSPanel interface, along with the persistent visibility flag used to + * avoid redundant display updates. + */ + +namespace esphome { +namespace nspanel_easy { + +namespace hmi { +namespace screensaver { + +/** + * @namespace screensaver + * @brief Components for the Screensaver page. + * + * Component ID mapping for the Screensaver page (index 9 in page_names array). + * Based on the Nextion HMI design file. + * Note: All components are local scope, so names do not include a page prefix. + */ + +// Page definition +constexpr HMIComponent PAGE = {"screensaver", get_page_id("screensaver")}; + +// Display components +constexpr HMIComponent TEXT = {"text", 4}; ///< Time/clock label (word-wrap enabled, 1000 chars max) + +// Touch capture components +constexpr HMIComponent WAKEUP = {"wakeup", 1}; ///< Full-screen wake-up touch area +constexpr HMIComponent SWIPE = {"swipe", 2}; ///< Swipe gesture capture area + +// Timers (for reference — not visual, excluded from ALL[]) +constexpr HMIComponent TIMER_SWIPESTORE = {"swipestore", 3}; ///< Swipe coordinate sampling timer (50 ms) + +// All visual components for iteration (timers and touch caps excluded) +constexpr HMIComponent ALL[] = {PAGE, TEXT}; + +constexpr size_t COMPONENT_COUNT = sizeof(ALL) / sizeof(ALL[0]); + +} // namespace screensaver +} // namespace hmi +} // namespace nspanel_easy +} // namespace esphome + +#endif // NSPANEL_EASY_PAGE_SCREENSAVER diff --git a/esphome/nspanel_esphome_base.yaml b/esphome/nspanel_esphome_base.yaml index 2b3eb85f..1f77679c 100644 --- a/esphome/nspanel_esphome_base.yaml +++ b/esphome/nspanel_esphome_base.yaml @@ -124,7 +124,7 @@ script: watchdog_round->stop(); return; } - ESP_LOGD("${TAG_BASE}", "The watchdog is starting a round"); + ESP_LOGV("${TAG_BASE}", "The watchdog is starting a round"); watchdog_round_wait_to_end->execute(); - id: watchdog_round_wait_to_end @@ -132,5 +132,5 @@ script: then: - script.wait: watchdog_round - lambda: |- - ESP_LOGD("${TAG_BASE}", "The watchdog completed the round"); + ESP_LOGV("${TAG_BASE}", "The watchdog completed the round"); ... diff --git a/esphome/nspanel_esphome_datetime.yaml b/esphome/nspanel_esphome_datetime.yaml index f2527672..7f5c7bf3 100644 --- a/esphome/nspanel_esphome_datetime.yaml +++ b/esphome/nspanel_esphome_datetime.yaml @@ -64,11 +64,15 @@ script: mode: restart then: - lambda: |- - if (system_flags.tft_upload_active) // Do not refresh if uploading TFT - return; + if (system_flags.tft_upload_active) return; // Do not refresh if uploading TFT // Time ESPTime now = id(time_provider).now(); + if (!now.is_valid()) { + ESP_LOGW("${TAG_DATETIME}", "refresh_datetime: time provider not yet synchronized"); + return; + } + std::string time_format_str = id(mui_time_format); // Resolve %-H (no-padding 24h hour) @@ -101,7 +105,7 @@ script: disp1->set_component_text("home.time", now.strftime(time_format_str).c_str()); // Date - render_date->execute("home.date", 0); + render_date->execute("home.date", static_cast(now.timestamp)); - id: render_date mode: restart @@ -151,6 +155,17 @@ script: if (timestamp > 0) dt = ESPTime::from_epoch_local(timestamp); + if (!dt.is_valid()) { + if (timestamp > 0) { + ESP_LOGW("${TAG_DATETIME}", "render_date(%s): invalid time from epoch %" PRIu32, + component.c_str(), timestamp); + } else { + ESP_LOGW("${TAG_DATETIME}", "render_date(%s): time provider not yet synchronized", + component.c_str()); + } + return; + } + const int dow = (dt.day_of_week + 5) % 7; const int mon = dt.month - 1; @@ -159,8 +174,16 @@ script: nspanel_easy::replace_all(date_str, "%a", weekdays_short[dow]); nspanel_easy::replace_all(date_str, "%B", months[mon]); nspanel_easy::replace_all(date_str, "%b", months_short[mon]); + nspanel_easy::replace_all(date_str, "%-d", to_string(dt.day_of_month).c_str()); + nspanel_easy::replace_all(date_str, "%-m", to_string(dt.month).c_str()); - disp1->set_component_text(component.c_str(), dt.strftime(date_str).c_str()); + const std::string date_string = dt.strftime(date_str); + if (date_string == "ERROR") { + ESP_LOGW("${TAG_DATETIME}", "render_date(%s): strftime failed - format '%s' (raw: '%s')", + component.c_str(), date_str.c_str(), id(mui_date_format).c_str()); + return; + } + disp1->set_component_text(component.c_str(), date_string.c_str()); - id: !extend stop_all then: diff --git a/esphome/nspanel_esphome_hw_display.yaml b/esphome/nspanel_esphome_hw_display.yaml index da1553b3..fd7a6091 100644 --- a/esphome/nspanel_esphome_hw_display.yaml +++ b/esphome/nspanel_esphome_hw_display.yaml @@ -71,7 +71,7 @@ display: uart_id: tf_uart command_spacing: 10ms # Increased from 5ms to prevent "Nextion Buffer Overflow" # dump_device_info: true # To-do: Enable when pr#13566 is released - max_commands_per_loop: 15 + # max_commands_per_loop: 15 # max_queue_size: 128 # Enable with v2026.6.0 update_interval: never on_setup: @@ -314,7 +314,7 @@ script: new_page_id != get_page_id("confirm") && new_page_id != get_page_id("keyb_num")) { detailed_entity->publish_state(""); - disp1->send_command("back_page_id=1"); + disp1->send_command_printf("back_page_id=%" PRIu8, get_page_id("home")); } else { // Report detailed entity ESP_LOGD("${TAG_HW_DISPLAY}", "Entity shown: %s", detailed_entity->state.c_str()); } @@ -325,9 +325,9 @@ script: } // Reset timers - if (current_page_id != get_page_id("screen_saver")) { - ESP_LOGV("${TAG_HW_DISPLAY}", "Reset timers"); - timer_reset_all->execute(); + if (new_page_id != get_page_id("screensaver")) { + ESP_LOGV("${TAG_HW_DISPLAY}", "Reset timers"); + timer_reset_all->execute(); } # Wait for other constructors to stop before telling the blueprint about a new page diff --git a/esphome/nspanel_esphome_localization.yaml b/esphome/nspanel_esphome_localization.yaml index 26b51f3e..21dae2ee 100644 --- a/esphome/nspanel_esphome_localization.yaml +++ b/esphome/nspanel_esphome_localization.yaml @@ -4134,8 +4134,8 @@ substitutions: LANG_ALARM_BYPASS: "${LOCALIZATION[LANG].alarm.bypass}" LANG_ALARM_DISARM: "${LOCALIZATION[LANG].alarm.disarm}" SENTINEL_NO_NAME: "__no_name__" - SENTINEL_UNAVAILABLE: "__unavailable__" - SENTINEL_UNKNOWN: "__unknown__" + SENTINEL_UNAVAILABLE: "_{_unavailable_}_" + SENTINEL_UNKNOWN: "_{_unknown_}_" esphome: platformio_options: diff --git a/esphome/nspanel_esphome_page_screensaver.yaml b/esphome/nspanel_esphome_page_screensaver.yaml index c4709225..8e2dec57 100644 --- a/esphome/nspanel_esphome_page_screensaver.yaml +++ b/esphome/nspanel_esphome_page_screensaver.yaml @@ -18,6 +18,10 @@ esphome: - -D NSPANEL_EASY_PAGE_SCREENSAVER globals: + - id: screensaver_background_color + type: uint16_t + restore_value: true + initial_value: 'Colors::RGB565_BLACK' - id: screensaver_display_time type: bool restore_value: true @@ -75,13 +79,17 @@ script: - id: !extend action_component_color then: - lambda: |- - if (page != "screensaver") return; - if (component == "text") { + if (page != "mem") return; + if (component == hmi::screensaver::TEXT.name) { id(screensaver_display_time_color) = color; + if (id(screensaver_display_time) and current_page_id == get_page_id("screensaver")) { + disp1->set_component_foreground_color(hmi::screensaver::TEXT.name, id(screensaver_display_time_color)); + } return; } - if (component == "screensaver_bco") { - disp1->set_component_value("screensaver_bco", color); + if (component == "screensaver_bco" and id(screensaver_background_color) != color) { + id(screensaver_background_color) = color; + disp1->send_command_printf("screensaver_bco=%" PRIu16, id(screensaver_background_color)); return; } @@ -90,11 +98,14 @@ script: - lambda: |- if (page != "mem") return; if (component == "screensaver_display_time") { - id(screensaver_display_time) = (val > 0); - page_screensaver->execute(); + const bool temp_display_time = (val > 0); + if (id(screensaver_display_time) != temp_display_time) { + id(screensaver_display_time) = temp_display_time; + page_screensaver->execute(); + } return; } - if (component == "screensaver_time_font") { + if (component == "screensaver_time_font" and id(screensaver_display_time_font) != val) { id(screensaver_display_time_font) = val; page_screensaver->execute(); return; @@ -107,49 +118,59 @@ script: timer_reset_all->execute(); return; } - static const bool wakeup_with_button_press = ${'true' if wakeup_with_button_press else 'false'}; - ESP_LOGV("${TAG_PAGE_SCREENSAVER}", "wakeup_with_button_press: %s", YESNO(wakeup_with_button_press)); - if (not wakeup_with_button_press) return; - wakeup->execute(true); + ESP_LOGV("${TAG_PAGE_SCREENSAVER}", "wakeup_with_button_press: ${wakeup_with_button_press}"); + if (${wakeup_with_button_press | lower}) { + wakeup->execute(true); + } + + - id: !extend boot_nextion + then: + - lambda: |- + disp1->send_command_printf("screensaver_bco=%" PRIu16, id(screensaver_background_color)); - id: !extend page_change then: - lambda: |- - if (new_page_id == get_page_id("screensaver")) - page_screensaver->execute(); + if (new_page_id != get_page_id("screensaver")) return; + page_screensaver->execute(); - id: page_screensaver - mode: single + mode: restart then: - - if: - condition: - - lambda: |- - return (current_page_id == get_page_id("screensaver") and not system_flags.tft_upload_active); - then: - - lambda: |- - page_screensaver_set_brightness->execute(); - disp1->send_command_printf("wakeup_page_id=%" PRIu8, wakeup_page_id); - if (id(screensaver_display_time)) { - disp1->set_component_font("text", id(screensaver_display_time_font)); - disp1->set_component_font_color("text", id(screensaver_display_time_color)); - disp1->show_component("text"); - refresh_datetime->execute(); - } - - delay: 5s - - script.execute: page_screensaver_set_brightness + - lambda: |- + if (current_page_id != get_page_id("screensaver")) return; + page_screensaver_set_brightness->execute(); + disp1->send_command_printf("wakeup_page_id=%" PRIu8, wakeup_page_id); + disp1->set_component_background_color("screensaver", id(screensaver_background_color)); + if (id(screensaver_display_time)) { + disp1->set_component_background_color(hmi::screensaver::TEXT.name, id(screensaver_background_color)); + disp1->set_component_foreground_color(hmi::screensaver::TEXT.name, id(screensaver_display_time_color)); + disp1->set_component_font(hmi::screensaver::TEXT.name, id(screensaver_display_time_font)); + disp1->show_component(hmi::screensaver::TEXT.name); + } + refresh_hardware_buttons_bars->execute(3); + if (id(screensaver_display_time)) refresh_datetime->execute(); + page_screensaver_set_brightness_with_wait->execute(); - id: page_screensaver_set_brightness mode: single then: - lambda: |- - if (current_page_id == get_page_id("screensaver")) - set_brightness->execute(int(display_sleep_brightness->state)); + if (current_page_id != get_page_id("screensaver")) return; + set_brightness->execute(int(display_sleep_brightness->state)); + + - id: page_screensaver_set_brightness_with_wait + mode: restart + then: + - delay: 5s + - script.execute: page_screensaver_set_brightness - id: !extend stop_all then: - lambda: |- page_screensaver->stop(); page_screensaver_set_brightness->stop(); + page_screensaver_set_brightness_with_wait->stop(); timer_sleep->stop(); - id: timer_sleep # Handles the sleep (go to screensaver page) after a timeout @@ -161,15 +182,12 @@ script: then: - delay: !lambda return (int(timeout_sleep->state) *1000); - lambda: |- - if ( - timeout_sleep->state > 0 and - current_page_id != get_page_id("screensaver") and - current_page_id != ${PAGE_BOOT_ID} - ) { - ESP_LOGD("${TAG_PAGE_SCREENSAVER}", "Sleep from '%s'", page_names[current_page_id]); - goto_page->execute(get_page_id("screensaver")); - set_brightness->execute(display_sleep_brightness->state); - } + if (timeout_sleep->state <= 0) return; // Sleep is disabled + if (current_page_id == get_page_id("screensaver")) return; // Already sleeping + if (current_page_id == get_page_id("boot")) return; // Don't sleep during boot + ESP_LOGD("${TAG_PAGE_SCREENSAVER}", "Sleep from '%s'", page_names[current_page_id]); + goto_page->execute(get_page_id("screensaver")); + set_brightness->execute(display_sleep_brightness->state); - id: !extend timer_reset_all then: diff --git a/esphome/nspanel_esphome_page_weather.yaml b/esphome/nspanel_esphome_page_weather.yaml index 99be2f8e..0baebe60 100644 --- a/esphome/nspanel_esphome_page_weather.yaml +++ b/esphome/nspanel_esphome_page_weather.yaml @@ -60,12 +60,6 @@ script: ESP_LOGW("${TAG_PAGE_WEATHER}", "Unexpected page_index: %" PRIu8, page_index); return; } - const ESPTime now = id(time_provider).now(); - // Anchor to local noon to avoid DST boundary edge cases where - // adding 86400s could resolve to the wrong calendar date - const int32_t seconds_to_noon = (12 - static_cast(now.hour)) * 3600; - const uint32_t timestamp = static_cast( - static_cast(now.timestamp) + seconds_to_noon + (page_index * ${SECONDS_PER_DAY})); static const char* const relative_days[5] = { "${LOCALIZATION[LANG].relative_day.today}", @@ -74,8 +68,16 @@ script: "${LOCALIZATION[LANG].relative_day.in_3_days}", "${LOCALIZATION[LANG].relative_day.in_4_days}" }; - disp1->set_component_text("day", relative_days[page_index]); + + const ESPTime now = id(time_provider).now(); + if (!now.is_valid()) return; + + // Anchor to local noon to avoid DST boundary edge cases where + // adding 86400s could resolve to the wrong calendar date + const int32_t seconds_to_noon = (12 - static_cast(now.hour)) * 3600; + const uint32_t timestamp = static_cast( + static_cast(now.timestamp) + seconds_to_noon + (page_index * ${SECONDS_PER_DAY})); render_date->execute("date", timestamp); - id: !extend stop_page_constructors diff --git a/esphome/nspanel_esphome_version.yaml b/esphome/nspanel_esphome_version.yaml index 071ed67b..345483e8 100644 --- a/esphome/nspanel_esphome_version.yaml +++ b/esphome/nspanel_esphome_version.yaml @@ -11,7 +11,7 @@ substitutions: # Value is imported from versioning/version.yaml <<: !include ../versioning/version.yaml # Minimum required versions for compatibility - min_blueprint_version: 14 + min_blueprint_version: 15 min_tft_version: 14 min_esphome_compiler_version: 2026.1.0 TAG_VERSIONING: nspanel.versioning diff --git a/nspanel_easy_blueprint.yaml b/nspanel_easy_blueprint.yaml index 2b82586e..093bfaf5 100644 --- a/nspanel_easy_blueprint.yaml +++ b/nspanel_easy_blueprint.yaml @@ -11,8 +11,6 @@ blueprint: description: > # NSPanel Easy Configuration via Blueprint - **Blueprint release**: 14 - This project enables comprehensive configuration of your NSPanel through a Blueprint featuring a user interface. @@ -4203,7 +4201,7 @@ trigger_variables: }} variables: - blueprint_version: 14 + blueprint_version: 15 pages: current: '{{ states(currentpage) }}' alarm: "alarm" @@ -7726,7 +7724,7 @@ actions: {{ nspanel_event.entity if nspanel_event is defined and nspanel_event.entity is defined and nspanel_event.entity is string and nspanel_event.entity | length > 1 - else (trigger.entity_id if trigger.entity_id is defined else trigger.event.data.entity_id) + else (trigger.entity_id if trigger.entity_id is defined else ((trigger.event | default({})).get('data') or {}).get('entity_id')) }} - *variable_entity - &entity_details_title_with_icon @@ -7907,7 +7905,7 @@ actions: {{ nspanel_event.entity if nspanel_event is defined and nspanel_event.entity is defined and nspanel_event.entity is string and nspanel_event.entity | length > 1 - else (trigger.entity_id if trigger.entity_id is defined else trigger.event.data.entity_id) + else (trigger.entity_id if trigger.entity_id is defined else ((trigger.event | default({})).get('data') or {}).get('entity_id')) }} entity_device_class: '{{ state_attr(entity_id, "device_class") | default("") }}' cover_icons: @@ -8044,7 +8042,7 @@ actions: {{ nspanel_event.entity if nspanel_event is defined and nspanel_event.entity is defined and nspanel_event.entity is string and nspanel_event.entity | length > 1 - else (trigger.entity_id if trigger.entity_id is defined else trigger.event.data.entity_id) + else (trigger.entity_id if trigger.entity_id is defined else ((trigger.event | default({})).get('data') or {}).get('entity_id')) }} fan: supported_features: '{{ state_attr(entity_id, "supported_features") | int(0) }}' @@ -8137,7 +8135,7 @@ actions: {{ nspanel_event.entity if nspanel_event is defined and nspanel_event.entity is defined and nspanel_event.entity is string and nspanel_event.entity | length > 1 - else (trigger.entity_id if trigger.entity_id is defined else trigger.event.data.entity_id) + else (trigger.entity_id if trigger.entity_id is defined else ((trigger.event | default({})).get('data') or {}).get('entity_id')) }} wait_completed: false - *variable_entity @@ -8236,7 +8234,7 @@ actions: {{ nspanel_event.entity if nspanel_event is defined and nspanel_event.entity is defined and nspanel_event.entity is string and nspanel_event.entity | length > 1 - else (trigger.entity_id if trigger.entity_id is defined else trigger.event.data.entity_id) + else (trigger.entity_id if trigger.entity_id is defined else ((trigger.event | default({})).get('data') or {}).get('entity_id')) }} alarm_entity: > {{ @@ -8280,7 +8278,7 @@ actions: {{ nspanel_event.entity if nspanel_event is defined and nspanel_event.entity is defined and nspanel_event.entity is string and nspanel_event.entity | length > 1 - else (trigger.entity_id if trigger.entity_id is defined else trigger.event.data.entity_id) + else (trigger.entity_id if trigger.entity_id is defined else ((trigger.event | default({})).get('data') or {}).get('entity_id')) }} climate_entity: > {{ @@ -8356,6 +8354,7 @@ actions: {{ climate_page_entities | selectattr("entity", "defined") + | rejectattr("entity", "eq", "") | rejectattr("entity", "eq", []) | list }} @@ -8553,7 +8552,7 @@ actions: {{ nspanel_event.entity if nspanel_event is defined and nspanel_event.entity is defined and nspanel_event.entity is string and nspanel_event.entity | length > 1 - else (trigger.entity_id if trigger.entity_id is defined else trigger.event.data.entity_id) + else (trigger.entity_id if trigger.entity_id is defined else ((trigger.event | default({})).get('data') or {}).get('entity_id')) }} - condition: - '{{ entity_id is defined }}' @@ -8598,7 +8597,7 @@ actions: {{ nspanel_event.entity if nspanel_event is defined and nspanel_event.entity is defined and nspanel_event.entity is string and nspanel_event.entity | length > 1 - else (trigger.entity_id if trigger.entity_id is defined else trigger.event.data.entity_id) + else (trigger.entity_id if trigger.entity_id is defined else ((trigger.event | default({})).get('data') or {}).get('entity_id')) }} - condition: - '{{ entity_id is defined }}' @@ -8647,7 +8646,7 @@ actions: nspanel_event.entity is defined and nspanel_event.entity is string and nspanel_event.entity | length > 1 - else (trigger.entity_id if trigger.entity_id is defined else trigger.event.data.entity_id) + else (trigger.entity_id if trigger.entity_id is defined else ((trigger.event | default({})).get('data') or {}).get('entity_id')) }} water_heater_entity: > {{