#:########################################################################################:# # TITLE: ESP reTERMINAL E1001 DASHBOARD # # zorruno.com layout v1.1 2026 # #:########################################################################################:# # REPO: # https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-reterminal-e1001.yaml #:########################################################################################:# # VERSIONS: # V0.6.1 2026-07-20 Allowed GPIO4/GPIO5 to be shared by the EXT1 deep-sleep # wake source and the normal white-button binary sensors. # V0.6 2026-07-20 Added open/closed-eye panel-state indicator, retained MQTT awake/sleep # status publishing, and inverse black tiles for Due, Overdue and Missed/Skipped jobs. # V0.5 2026-07-20 Added five-minute deep sleep, timer and dual white-button wake, # retained MQTT sleep control, wake-source page selection, persistent # per-page display hashes and short post-button interaction timeout. # V0.4 2026-07-20 Improved Jobs page with large Material Design job icons, # state icons, two-line centred job names and larger status text. # V0.3 2026-07-20 Added two-page Jobs and Appliances dashboard, white-button page # selection, rounded tiles, full date header, Home Assistant job and # appliance data, inverse alert tiles, and change-only display refreshes. # V0.2 2026-07-20 Enabled ESP-IDF experimental features required by ESPHome # 2026.7.0 for OTA support with 32MB flash. # V0.1 2026-07-20 Initial ESPHome bring-up, Home Assistant API connection, # E1001 e-paper display test matrix and physical button setup. #:########################################################################################:# # HARDWARE: # - Seeed Studio reTerminal E1001 # - ESP32-S3 # - 32MB flash # - 8MB octal PSRAM # - 7.5 inch monochrome e-paper display # - 800x480 resolution # - Green refresh button: GPIO3 # - Right white button: GPIO4 # - Left white button: GPIO5 # - Onboard green LED: GPIO6, inverted # - Display SPI clock: GPIO7 # - Display SPI MOSI: GPIO9 # - Display CS: GPIO10 # - Display DC: GPIO11 # - Display reset: GPIO12 # - Display busy: GPIO13, inverted #:########################################################################################:# # OPERATION: # - The device sleeps for five minutes, then wakes and checks the currently displayed page. # - Either white button can wake the ESP32-S3 through EXT1 ANY_LOW. # - A left-button wake selects View Road Jobs; a right-button wake selects Appliances. # - The wake button is identified from the retained ESP32 EXT1 wake-status bitmask, so one # press both wakes the device and selects the intended page. # - Timer wakeups retain the page that was displayed before sleep. # - Per-page display hashes survive deep sleep. The e-paper panel refreshes only when the # selected page content, selected page, or visible awake/sleep indicator changes. # - Two open eyes indicate an interactive/kept-awake panel. Two closed eyes are written to # the e-paper panel before entering deep sleep. # - After a button interaction, the device remains awake briefly so another page can be # selected without waiting through another complete wake-and-connect cycle. # - Home Assistant entity data is imported through the encrypted ESPHome native API. # - MQTT is used only for retained deep-sleep control and retained awake/sleep status. # # MQTT COMMANDS: # - ${mqtt_command_topic}/deepsleep = ON # - Allow the device to return to deep sleep. # - ${mqtt_command_topic}/deepsleep = OFF # - Prevent deep sleep for OTA updates and development. # - Publish these commands as retained messages. # MQTT STATUS: # - ${mqtt_status_topic}/state = awake/sleeping (retained) # - ${mqtt_status_topic}/deepsleep = ON/OFF (retained) # # - Deep sleep is prevented by default until the retained MQTT command is received. # - If MQTT cannot be reached after the boot timeout, the device uses a battery-protecting # fallback and permits the normal five-minute sleep cycle for that wake. #:########################################################################################:# #:########################################################################################:# # SUBSTITUTIONS: # Specific device variable substitutions # If NOT using a secrets file, replace the passwords etc with quoted values. #:########################################################################################:# substitutions: # Device Naming device_name: "esp-reterminal-e1001" friendly_name: "reTerminal E1001 Dashboard" description_comment: "Seeed Studio reTerminal E1001 two-page Home Assistant jobs and appliance status dashboard with five-minute deep sleep, button wake and panel-state eyes. (Layout V1.1)" device_area: "Lounge" # Project Naming project_name: "SeeedStudio.reTerminal-E1001" project_version: "v0.6.1" # Passwords and Secrets api_key: !secret esp-api_key ota_pass: !secret esp-ota_pass mqtt_command_main_topic: !secret mqtt_command_main_topic mqtt_status_main_topic: !secret mqtt_status_main_topic # MQTT Local Controls mqtt_device_name: "reterminal-e1001" mqtt_command_topic: "${mqtt_command_main_topic}/${mqtt_device_name}" mqtt_status_topic: "${mqtt_status_main_topic}/${mqtt_device_name}" # Device Settings log_level: "INFO" display_check_interval: "30s" finished_highlight_seconds: "3600" job_hours_display_limit: "48" # Deep Sleep Settings sleep_interval: "5min" mqtt_connect_timeout: "30s" api_connect_timeout: "30s" ha_data_timeout: "25s" timer_wake_awake_seconds: "8" button_wake_awake_seconds: "30" mqtt_enable_awake_seconds: "15" #:########################################################################################:# # 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 #### # MQTT is used only for retained deep-sleep control and status. # Home Assistant entity states still arrive through the native API. 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 #### # Home Assistant provides local time through the native API below. #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}" on_boot: #:####################################################################################:# # EARLY WAKE-SOURCE DETECTION # #:####################################################################################:# - priority: 800 then: - lambda: |- id(wakeup_cause) = static_cast(esp_sleep_get_wakeup_cause()); id(wakeup_pin_mask) = esp_sleep_get_ext1_wakeup_status(); id(woke_by_button) = id(wakeup_cause) == ESP_SLEEP_WAKEUP_EXT1; // A physical-button wake is interactive, so show open eyes. A short // timer maintenance wake keeps the closed-eye icon unless the device // is explicitly kept awake through MQTT. id(panel_awake_indicator) = id(woke_by_button); const int previous_page = id(current_page); if (id(woke_by_button)) { // GPIO5 is the left white button: Jobs. if ((id(wakeup_pin_mask) & (1ULL << 5)) != 0) { id(current_page) = 0; } // GPIO4 is the right white button: Appliances. else if ((id(wakeup_pin_mask) & (1ULL << 4)) != 0) { id(current_page) = 1; } } id(page_changed_on_wake) = id(current_page) != previous_page; ESP_LOGI( "wakeup", "Wake cause=%d EXT1 mask=0x%llX page=%d changed=%s", id(wakeup_cause), static_cast(id(wakeup_pin_mask)), id(current_page) + 1, id(page_changed_on_wake) ? "yes" : "no" ); #:####################################################################################:# # DEFAULT TO STAYING AWAKE UNTIL MQTT CONTROL IS RECEIVED # #:####################################################################################:# - priority: 600 then: - deep_sleep.prevent: deep_sleep_1 - lambda: |- id(sleep_allowed) = false; id(boot_sequence_complete) = false; id(last_activity_ms) = millis(); id(awake_timeout_ms) = ${button_wake_awake_seconds} * 1000UL; - logger.log: "Deep sleep prevented by default while waiting for retained MQTT control." #:####################################################################################:# # NETWORK, HOME ASSISTANT DATA AND DISPLAY CHECK # #:####################################################################################:# - priority: -100 then: - wait_until: condition: mqtt.connected: timeout: "${mqtt_connect_timeout}" - if: condition: not: mqtt.connected: then: - logger.log: "MQTT unavailable after ${mqtt_connect_timeout}; allowing fallback sleep for battery protection." - lambda: |- id(sleep_allowed) = true; - deep_sleep.allow: deep_sleep_1 else: # Allow time for the retained deepsleep command to be delivered. - delay: 2s - wait_until: condition: api.connected: timeout: "${api_connect_timeout}" - wait_until: condition: lambda: |- if (!id(homeassistant_time).now().is_valid()) return false; if (id(current_page) == 0) { return id(job_dog_feeding_morning_state).has_state() && id(job_dog_feeding_evening_state).has_state() && id(job_dog_depooping_state).has_state() && id(job_dishwasher_unpacked_state).has_state() && id(job_rubbish_removed_state).has_state() && id(job_dog_feeding_morning_due_ts).has_state() && id(job_dog_feeding_evening_due_ts).has_state() && id(job_dog_depooping_due_ts).has_state() && id(job_dishwasher_unpacked_due_ts).has_state() && id(job_rubbish_removed_due_ts).has_state(); } return id(appliance_washing_state).has_state() && id(appliance_dryer_state).has_state() && id(appliance_main_dishwasher_state).has_state() && id(appliance_downstairs_dishwasher_state).has_state() && id(appliance_evcharger_32a_state).has_state() && id(appliance_evcharger_16a_state).has_state() && id(appliance_ryobi_left_state).has_state() && id(appliance_ryobi_right_state).has_state() && id(appliance_pool_pump_state).has_state() && id(appliance_3d_printer_state).has_state(); timeout: "${ha_data_timeout}" - if: condition: lambda: |- return id(current_page) == 0; then: - display.page.show: page_jobs else: - display.page.show: page_appliances - if: condition: lambda: |- return id(homeassistant_time).now().is_valid(); then: - if: condition: lambda: |- return id(page_changed_on_wake); then: - script.execute: id: refresh_display_if_changed force: true else: - script.execute: id: refresh_display_if_changed force: false - script.wait: refresh_display_if_changed else: - logger.log: "Home Assistant time/data were unavailable; preserving the existing e-paper image." - if: condition: mqtt.connected: then: - mqtt.publish: topic: "${mqtt_status_topic}/state" payload: "awake" qos: 1 retain: true - lambda: |- id(boot_sequence_complete) = true; id(last_activity_ms) = millis(); id(awake_timeout_ms) = (id(woke_by_button) ? ${button_wake_awake_seconds} : ${timer_wake_awake_seconds}) * 1000UL; ESP_LOGI( "deep_sleep", "Boot display check complete; inactivity window is %lu seconds", static_cast(id(awake_timeout_ms) / 1000UL) ); #:########################################################################################:# # ESP PLATFORM AND FRAMEWORK: # https://esphome.io/components/esp32.html #:########################################################################################:# esp32: board: esp32-s3-devkitc-1 flash_size: 32MB framework: type: arduino advanced: enable_idf_experimental_features: true #:########################################################################################:# # PSRAM: # https://esphome.io/components/psram.html #:########################################################################################:# psram: mode: octal speed: 80MHz #:########################################################################################:# # LOGGER: # The E1001 uses its CH340K USB-to-UART bridge on UART0. # https://esphome.io/components/logger.html #:########################################################################################:# logger: level: "${log_level}" baud_rate: 115200 hardware_uart: UART0 #:########################################################################################:# # SPI: # E-paper display bus # https://esphome.io/components/spi.html #:########################################################################################:# spi: id: epaper_spi clk_pin: GPIO7 mosi_pin: GPIO9 #:########################################################################################:# # HOME ASSISTANT TIME: # Used for the date header, job countdowns and appliance completion age. # https://esphome.io/components/time/homeassistant.html #:########################################################################################:# time: - platform: homeassistant id: homeassistant_time on_time_sync: then: - script.execute: id: refresh_display_if_changed force: false #:########################################################################################:# # GLOBALS: # Page, persistent display hashes, wake information and sleep-control state # https://esphome.io/components/globals.html #:########################################################################################:# globals: # The currently visible page survives deep sleep so timer wakes retain it. - id: current_page type: int restore_value: yes initial_value: "0" # A separate persistent hash is kept for each page. These are updated only when the # visible content really changes, avoiding a flash write on every five-minute wake. - id: last_jobs_signature_hash type: uint32_t restore_value: yes initial_value: "0" - id: last_appliances_signature_hash type: uint32_t restore_value: yes initial_value: "0" - id: display_update_required type: bool restore_value: no initial_value: "false" - id: wakeup_cause type: int restore_value: no initial_value: "0" - id: wakeup_pin_mask type: uint64_t restore_value: no initial_value: "0" - id: woke_by_button type: bool restore_value: no initial_value: "false" - id: page_changed_on_wake type: bool restore_value: no initial_value: "false" # Desired icon currently rendered in the display header. This is included in the # per-page signature, so changing eye state refreshes only when required. - id: panel_awake_indicator type: bool restore_value: no initial_value: "false" - id: sleep_allowed type: bool restore_value: no initial_value: "false" - id: boot_sequence_complete type: bool restore_value: no initial_value: "false" - id: last_activity_ms type: uint32_t restore_value: no initial_value: "0" - id: awake_timeout_ms type: uint32_t restore_value: no initial_value: "30000" #:########################################################################################:# # FONTS: # Bold, compact fonts for clear monochrome e-paper rendering. # https://esphome.io/components/font.html #:########################################################################################:# font: - file: type: gfonts family: Inter weight: 700 id: font_header size: 31 - file: type: gfonts family: Inter weight: 700 id: font_date size: 18 - file: type: gfonts family: Inter weight: 700 id: font_job_title size: 25 - file: type: gfonts family: Inter weight: 700 id: font_job_status size: 23 - file: type: gfonts family: Inter weight: 700 id: font_appliance_title size: 16 - file: type: gfonts family: Inter weight: 700 id: font_appliance_status size: 17 #:########################################################################################:# # JOB AND STATUS ICONS: # Material Design Icons are downloaded and embedded at compile time. # https://esphome.io/components/image/file.html #:########################################################################################:# image: #:######################################################################################:# # Job icons # #:######################################################################################:# - platform: file file: mdi:dog-side id: icon_job_dog type: BINARY transparency: chroma_key resize: 50x50 - platform: file file: mdi:emoticon-poop id: icon_job_poop type: BINARY transparency: chroma_key resize: 50x50 - platform: file file: mdi:dishwasher id: icon_job_dishwasher type: BINARY transparency: chroma_key resize: 50x50 - platform: file file: mdi:trash-can id: icon_job_rubbish type: BINARY transparency: chroma_key resize: 50x50 #:######################################################################################:# # State icons # #:######################################################################################:# - platform: file file: mdi:check-circle id: icon_status_complete type: BINARY transparency: chroma_key resize: 30x30 - platform: file file: mdi:clock-outline id: icon_status_due type: BINARY transparency: chroma_key resize: 30x30 - platform: file file: mdi:alert-octagon id: icon_status_overdue type: BINARY transparency: chroma_key resize: 30x30 - platform: file file: mdi:close-circle id: icon_status_missed type: BINARY transparency: chroma_key resize: 30x30 - platform: file file: mdi:help-circle-outline id: icon_status_unknown type: BINARY transparency: chroma_key resize: 30x30 #:########################################################################################:# # DISPLAY: # Two pages with rounded status tiles. # The display itself never runs on a blind polling interval. All refreshes pass through # refresh_display_if_changed before the physical e-paper update occurs. # https://esphome.io/components/display/waveshare_epaper.html #:########################################################################################:# display: - platform: waveshare_epaper id: epaper_display spi_id: epaper_spi model: 7.50inv2 cs_pin: GPIO10 dc_pin: GPIO11 reset_pin: number: GPIO12 inverted: false busy_pin: number: GPIO13 inverted: true update_interval: never pages: #:####################################################################################:# # PAGE 1 - VIEW ROAD JOBS # #:####################################################################################:# - id: page_jobs lambda: |- const int screen_width = 800; const int header_y = 58; const int radius = 9; const int normal_border = 2; const int emphasis_border = 4; const int hour_limit = ${job_hours_display_limit}; const auto now = id(homeassistant_time).now(); const int64_t now_ts = now.is_valid() ? now.timestamp : 0; auto fill_rounded = [&](int x, int y, int w, int h, int r, Color colour) { it.filled_rectangle(x + r, y, w - (2 * r), h, colour); it.filled_rectangle(x, y + r, w, h - (2 * r), colour); it.filled_circle(x + r, y + r, r, colour); it.filled_circle(x + w - r - 1, y + r, r, colour); it.filled_circle(x + r, y + h - r - 1, r, colour); it.filled_circle(x + w - r - 1, y + h - r - 1, r, colour); }; auto rounded_tile = [&](int x, int y, int w, int h, bool inverse, int border) { fill_rounded(x, y, w, h, radius, COLOR_ON); if (!inverse) { int inner_radius = radius - border; if (inner_radius < 2) inner_radius = 2; fill_rounded( x + border, y + border, w - (2 * border), h - (2 * border), inner_radius, COLOR_OFF ); } }; auto draw_panel_state_icon = [&](int centre_x, int centre_y) { if (id(panel_awake_indicator)) { // Two open eyes with pupils. it.circle(centre_x - 9, centre_y, 6, COLOR_ON); it.circle(centre_x + 9, centre_y, 6, COLOR_ON); it.filled_circle(centre_x - 9, centre_y, 2, COLOR_ON); it.filled_circle(centre_x + 9, centre_y, 2, COLOR_ON); } else { // Two closed eyelids, each drawn as a shallow downward curve. it.line(centre_x - 16, centre_y - 2, centre_x - 10, centre_y + 2, COLOR_ON); it.line(centre_x - 10, centre_y + 2, centre_x - 4, centre_y - 2, COLOR_ON); it.line(centre_x + 2, centre_y - 2, centre_x + 8, centre_y + 2, COLOR_ON); it.line(centre_x + 8, centre_y + 2, centre_x + 14, centre_y - 2, COLOR_ON); } }; auto make_date = [&]() -> std::string { if (!now.is_valid()) return "Waiting for Home Assistant time"; const char *weekdays[] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" }; const char *months[] = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; const int day = now.day_of_month; const int last_two = day % 100; const char *suffix = "th"; if (last_two < 11 || last_two > 13) { if ((day % 10) == 1) suffix = "st"; else if ((day % 10) == 2) suffix = "nd"; else if ((day % 10) == 3) suffix = "rd"; } char buffer[72]; snprintf( buffer, sizeof(buffer), "%s, %d%s %s %d", weekdays[now.day_of_week - 1], day, suffix, months[now.month - 1], now.year ); return std::string(buffer); }; auto draw_job_icon = [&](int icon_type, int x, int y, Color foreground, Color background) { if (icon_type == 0) { it.image(x, y, id(icon_job_dog), foreground, background); } else if (icon_type == 1) { it.image(x, y, id(icon_job_poop), foreground, background); } else if (icon_type == 2) { it.image(x, y, id(icon_job_dishwasher), foreground, background); } else { it.image(x, y, id(icon_job_rubbish), foreground, background); } }; auto draw_state_icon = [&](const std::string &state, int x, int y, Color foreground, Color background) { if (state == "Complete") { it.image(x, y, id(icon_status_complete), foreground, background); } else if (state == "Overdue") { it.image(x, y, id(icon_status_overdue), foreground, background); } else if (state == "Missed" || state == "Skipped") { it.image(x, y, id(icon_status_missed), foreground, background); } else if (state == "Due" || state == "Not Due") { it.image(x, y, id(icon_status_due), foreground, background); } else { it.image(x, y, id(icon_status_unknown), foreground, background); } }; auto draw_job = [&](int x, int y, int icon_type, const char *title_line_1, const char *title_line_2, const std::string &state, float due_ts, const std::string &completed_time) { const int w = 256; const int h = 197; std::string status = "Unavailable"; bool inverse = false; int border = normal_border; if (state == "Complete") { if (!completed_time.empty() && completed_time != "Never" && completed_time != "unknown") { status = "Completed " + completed_time; } else { status = "Complete"; } } else if (state == "Not Due") { if (now_ts > 0 && !isnan(due_ts) && due_ts > now_ts) { const int64_t remaining = static_cast(due_ts) - now_ts; const int hours = static_cast((remaining + 3599) / 3600); if (hours <= hour_limit) { status = "Due: " + std::to_string(hours) + (hours == 1 ? " Hour" : " Hours"); } else { const int days = static_cast((remaining + 86399) / 86400); status = "Due: " + std::to_string(days) + (days == 1 ? " Day" : " Days"); } } else { status = "Not Due"; } } else if (state == "Due") { status = "DUE NOW"; inverse = true; } else if (state == "Overdue") { status = "OVERDUE"; inverse = true; } else if (state == "Missed") { status = "MISSED"; inverse = true; } else if (state == "Skipped") { status = "SKIPPED"; inverse = true; } else if (!state.empty()) { status = state; } rounded_tile(x, y, w, h, inverse, border); const Color foreground = inverse ? COLOR_OFF : COLOR_ON; const Color background = inverse ? COLOR_ON : COLOR_OFF; // Main job icon, centred near the top of the tile. draw_job_icon(icon_type, x + ((w - 50) / 2), y + 8, foreground, background); // Small state icon in the upper-right corner. draw_state_icon(state, x + w - 42, y + 12, foreground, background); // Two-line job title and one large status line. it.printf( x + (w / 2), y + 83, id(font_job_title), foreground, TextAlign::CENTER, "%s", title_line_1 ); it.printf( x + (w / 2), y + 114, id(font_job_title), foreground, TextAlign::CENTER, "%s", title_line_2 ); it.printf( x + (w / 2), y + 160, id(font_job_status), foreground, TextAlign::CENTER, "%s", status.c_str() ); }; // Header. it.print(14, 28, id(font_header), TextAlign::CENTER_LEFT, "View Road Jobs"); draw_panel_state_icon(448, 28); const std::string date_text = make_date(); it.printf( screen_width - 14, 29, id(font_date), TextAlign::CENTER_RIGHT, "%s", date_text.c_str() ); it.line(8, header_y, screen_width - 9, header_y); // Three tiles on the top row and two centred on the lower row. draw_job( 8, 70, 0, "Dog Feeding", "Morning", id(job_dog_feeding_morning_state).state, id(job_dog_feeding_morning_due_ts).state, id(job_dog_feeding_morning_completed_time).state ); draw_job( 272, 70, 0, "Dog Feeding", "Evening", id(job_dog_feeding_evening_state).state, id(job_dog_feeding_evening_due_ts).state, id(job_dog_feeding_evening_completed_time).state ); draw_job( 536, 70, 1, "Dog", "De-pooping", id(job_dog_depooping_state).state, id(job_dog_depooping_due_ts).state, id(job_dog_depooping_completed_time).state ); draw_job( 140, 275, 2, "Dishwasher", "Unpacked", id(job_dishwasher_unpacked_state).state, id(job_dishwasher_unpacked_due_ts).state, id(job_dishwasher_unpacked_completed_time).state ); draw_job( 404, 275, 3, "Rubbish", "Removed", id(job_rubbish_removed_state).state, id(job_rubbish_removed_due_ts).state, id(job_rubbish_removed_completed_time).state ); #:####################################################################################:# # PAGE 2 - VIEW ROAD APPLIANCES # #:####################################################################################:# - id: page_appliances lambda: |- const int screen_width = 800; const int header_y = 58; const int radius = 8; const int normal_border = 2; const int emphasis_border = 4; const int finished_seconds = ${finished_highlight_seconds}; const auto now = id(homeassistant_time).now(); const int64_t now_ts = now.is_valid() ? now.timestamp : 0; auto fill_rounded = [&](int x, int y, int w, int h, int r, Color colour) { it.filled_rectangle(x + r, y, w - (2 * r), h, colour); it.filled_rectangle(x, y + r, w, h - (2 * r), colour); it.filled_circle(x + r, y + r, r, colour); it.filled_circle(x + w - r - 1, y + r, r, colour); it.filled_circle(x + r, y + h - r - 1, r, colour); it.filled_circle(x + w - r - 1, y + h - r - 1, r, colour); }; auto rounded_tile = [&](int x, int y, int w, int h, bool inverse, int border) { fill_rounded(x, y, w, h, radius, COLOR_ON); if (!inverse) { int inner_radius = radius - border; if (inner_radius < 2) inner_radius = 2; fill_rounded( x + border, y + border, w - (2 * border), h - (2 * border), inner_radius, COLOR_OFF ); } }; auto draw_panel_state_icon = [&](int centre_x, int centre_y) { if (id(panel_awake_indicator)) { // Two open eyes with pupils. it.circle(centre_x - 9, centre_y, 6, COLOR_ON); it.circle(centre_x + 9, centre_y, 6, COLOR_ON); it.filled_circle(centre_x - 9, centre_y, 2, COLOR_ON); it.filled_circle(centre_x + 9, centre_y, 2, COLOR_ON); } else { // Two closed eyelids, each drawn as a shallow downward curve. it.line(centre_x - 16, centre_y - 2, centre_x - 10, centre_y + 2, COLOR_ON); it.line(centre_x - 10, centre_y + 2, centre_x - 4, centre_y - 2, COLOR_ON); it.line(centre_x + 2, centre_y - 2, centre_x + 8, centre_y + 2, COLOR_ON); it.line(centre_x + 8, centre_y + 2, centre_x + 14, centre_y - 2, COLOR_ON); } }; auto make_date = [&]() -> std::string { if (!now.is_valid()) return "Waiting for Home Assistant time"; const char *weekdays[] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" }; const char *months[] = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; const int day = now.day_of_month; const int last_two = day % 100; const char *suffix = "th"; if (last_two < 11 || last_two > 13) { if ((day % 10) == 1) suffix = "st"; else if ((day % 10) == 2) suffix = "nd"; else if ((day % 10) == 3) suffix = "rd"; } char buffer[72]; snprintf( buffer, sizeof(buffer), "%s, %d%s %s %d", weekdays[now.day_of_week - 1], day, suffix, months[now.month - 1], now.year ); return std::string(buffer); }; auto completed_clock = [&](const std::string &stop_local) -> std::string { if (stop_local.length() >= 16) return stop_local.substr(11, 5); return "unknown"; }; auto draw_appliance = [&](int x, int y, const char *title_line_1, const char *title_line_2, const std::string &state, float stop_ts, float duration_seconds, const std::string &stop_local) { const int w = 190; const int h = 128; const bool valid_cycle = !isnan(stop_ts) && stop_ts > 0 && !isnan(duration_seconds) && duration_seconds > 0; bool recent_finish = false; if (valid_cycle && now_ts > 0) { const int64_t age = now_ts - static_cast(stop_ts); recent_finish = age >= 0 && age < finished_seconds; } std::string status = "Unavailable"; bool inverse = false; int border = normal_border; if (state == "Operating") { status = "OPERATING"; border = emphasis_border; } else if (valid_cycle && recent_finish) { status = "FINISHED"; inverse = true; } else if (valid_cycle) { status = "Completed at " + completed_clock(stop_local); } else if (state == "Standby") { status = "STANDBY"; } else if (state == "Off") { status = "OFF"; } else if (state == "Finished") { status = "FINISHED"; inverse = true; } else if (!state.empty()) { status = state; } rounded_tile(x, y, w, h, inverse, border); const Color text_colour = inverse ? COLOR_OFF : COLOR_ON; if (title_line_2[0] == '\0') { it.printf( x + (w / 2), y + 39, id(font_appliance_title), text_colour, TextAlign::CENTER, "%s", title_line_1 ); } else { it.printf( x + (w / 2), y + 25, id(font_appliance_title), text_colour, TextAlign::CENTER, "%s", title_line_1 ); it.printf( x + (w / 2), y + 49, id(font_appliance_title), text_colour, TextAlign::CENTER, "%s", title_line_2 ); } it.printf( x + (w / 2), y + 91, id(font_appliance_status), text_colour, TextAlign::CENTER, "%s", status.c_str() ); }; // Header. it.print(14, 28, id(font_header), TextAlign::CENTER_LEFT, "View Road Appliances"); draw_panel_state_icon(448, 28); const std::string date_text = make_date(); it.printf( screen_width - 14, 29, id(font_date), TextAlign::CENTER_RIGHT, "%s", date_text.c_str() ); it.line(8, header_y, screen_width - 9, header_y); // Four tiles on rows one and two; two centred tiles on row three. draw_appliance( 8, 70, "Washing", "Machine", id(appliance_washing_state).state, id(appliance_washing_stop_ts).state, id(appliance_washing_duration).state, id(appliance_washing_stop_local).state ); draw_appliance( 206, 70, "Dryer", "", id(appliance_dryer_state).state, id(appliance_dryer_stop_ts).state, id(appliance_dryer_duration).state, id(appliance_dryer_stop_local).state ); draw_appliance( 404, 70, "Main", "Dishwasher", id(appliance_main_dishwasher_state).state, id(appliance_main_dishwasher_stop_ts).state, id(appliance_main_dishwasher_duration).state, id(appliance_main_dishwasher_stop_local).state ); draw_appliance( 602, 70, "Downstairs", "Dishwasher", id(appliance_downstairs_dishwasher_state).state, id(appliance_downstairs_dishwasher_stop_ts).state, id(appliance_downstairs_dishwasher_duration).state, id(appliance_downstairs_dishwasher_stop_local).state ); draw_appliance( 8, 206, "EV Charger", "32A", id(appliance_evcharger_32a_state).state, id(appliance_evcharger_32a_stop_ts).state, id(appliance_evcharger_32a_duration).state, id(appliance_evcharger_32a_stop_local).state ); draw_appliance( 206, 206, "EV Charger", "16A", id(appliance_evcharger_16a_state).state, id(appliance_evcharger_16a_stop_ts).state, id(appliance_evcharger_16a_duration).state, id(appliance_evcharger_16a_stop_local).state ); draw_appliance( 404, 206, "Ryobi Charger", "Left", id(appliance_ryobi_left_state).state, id(appliance_ryobi_left_stop_ts).state, id(appliance_ryobi_left_duration).state, id(appliance_ryobi_left_stop_local).state ); draw_appliance( 602, 206, "Ryobi Charger", "Right", id(appliance_ryobi_right_state).state, id(appliance_ryobi_right_stop_ts).state, id(appliance_ryobi_right_duration).state, id(appliance_ryobi_right_stop_local).state ); draw_appliance( 206, 342, "Pool Pump", "", id(appliance_pool_pump_state).state, id(appliance_pool_pump_stop_ts).state, id(appliance_pool_pump_duration).state, id(appliance_pool_pump_stop_local).state ); draw_appliance( 404, 342, "3D Printer", "", id(appliance_3d_printer_state).state, id(appliance_3d_printer_stop_ts).state, id(appliance_3d_printer_duration).state, id(appliance_3d_printer_stop_local).state ); #:########################################################################################:# # DISPLAY CHANGE DETECTION: # Builds a signature from only the content that is currently visible. The display is not # physically refreshed when the visible wording, style and eye indicator would remain unchanged. # https://esphome.io/components/script.html #:########################################################################################:# script: - id: refresh_display_if_changed mode: restart parameters: force: bool then: # Debounce bursts of Home Assistant state restoration and page changes. - delay: 750ms - lambda: |- const auto now = id(homeassistant_time).now(); const int64_t now_ts = now.is_valid() ? now.timestamp : 0; const int hour_limit = ${job_hours_display_limit}; const int finished_seconds = ${finished_highlight_seconds}; std::string signature = id(current_page) == 0 ? "jobs" : "appliances"; auto append_text = [&](const std::string &value) { signature += "|"; signature += value; }; auto append_number = [&](int64_t value) { char buffer[32]; snprintf(buffer, sizeof(buffer), "|%lld", static_cast(value)); signature += buffer; }; // The visible header changes once per day, not every minute. append_text(id(panel_awake_indicator) ? "eyes_open" : "eyes_closed"); if (now.is_valid()) { append_number(now.year); append_number(now.day_of_year); } else { append_text("no_time"); } if (id(current_page) == 0) { auto append_job = [&](const std::string &state, float due_ts, const std::string &completed_time) { append_text(state); if (state == "Complete") { append_text(completed_time); } else if (state == "Not Due" && now_ts > 0 && !isnan(due_ts) && due_ts > now_ts) { const int64_t remaining = static_cast(due_ts) - now_ts; const int hours = static_cast((remaining + 3599) / 3600); if (hours <= hour_limit) { append_text("hours"); append_number(hours); } else { const int days = static_cast((remaining + 86399) / 86400); append_text("days"); append_number(days); } } }; append_job( id(job_dog_feeding_morning_state).state, id(job_dog_feeding_morning_due_ts).state, id(job_dog_feeding_morning_completed_time).state ); append_job( id(job_dog_feeding_evening_state).state, id(job_dog_feeding_evening_due_ts).state, id(job_dog_feeding_evening_completed_time).state ); append_job( id(job_dog_depooping_state).state, id(job_dog_depooping_due_ts).state, id(job_dog_depooping_completed_time).state ); append_job( id(job_dishwasher_unpacked_state).state, id(job_dishwasher_unpacked_due_ts).state, id(job_dishwasher_unpacked_completed_time).state ); append_job( id(job_rubbish_removed_state).state, id(job_rubbish_removed_due_ts).state, id(job_rubbish_removed_completed_time).state ); } else { auto append_appliance = [&](const std::string &state, float stop_ts, float duration_seconds, const std::string &stop_local) { const bool valid_cycle = !isnan(stop_ts) && stop_ts > 0 && !isnan(duration_seconds) && duration_seconds > 0; if (state == "Operating") { append_text("OPERATING"); return; } if (valid_cycle) { bool recent_finish = false; if (now_ts > 0) { const int64_t age = now_ts - static_cast(stop_ts); recent_finish = age >= 0 && age < finished_seconds; } append_text(recent_finish ? "FINISHED" : "COMPLETED"); append_text(stop_local); return; } append_text(state); }; append_appliance( id(appliance_washing_state).state, id(appliance_washing_stop_ts).state, id(appliance_washing_duration).state, id(appliance_washing_stop_local).state ); append_appliance( id(appliance_dryer_state).state, id(appliance_dryer_stop_ts).state, id(appliance_dryer_duration).state, id(appliance_dryer_stop_local).state ); append_appliance( id(appliance_main_dishwasher_state).state, id(appliance_main_dishwasher_stop_ts).state, id(appliance_main_dishwasher_duration).state, id(appliance_main_dishwasher_stop_local).state ); append_appliance( id(appliance_downstairs_dishwasher_state).state, id(appliance_downstairs_dishwasher_stop_ts).state, id(appliance_downstairs_dishwasher_duration).state, id(appliance_downstairs_dishwasher_stop_local).state ); append_appliance( id(appliance_evcharger_32a_state).state, id(appliance_evcharger_32a_stop_ts).state, id(appliance_evcharger_32a_duration).state, id(appliance_evcharger_32a_stop_local).state ); append_appliance( id(appliance_evcharger_16a_state).state, id(appliance_evcharger_16a_stop_ts).state, id(appliance_evcharger_16a_duration).state, id(appliance_evcharger_16a_stop_local).state ); append_appliance( id(appliance_ryobi_left_state).state, id(appliance_ryobi_left_stop_ts).state, id(appliance_ryobi_left_duration).state, id(appliance_ryobi_left_stop_local).state ); append_appliance( id(appliance_ryobi_right_state).state, id(appliance_ryobi_right_stop_ts).state, id(appliance_ryobi_right_duration).state, id(appliance_ryobi_right_stop_local).state ); append_appliance( id(appliance_pool_pump_state).state, id(appliance_pool_pump_stop_ts).state, id(appliance_pool_pump_duration).state, id(appliance_pool_pump_stop_local).state ); append_appliance( id(appliance_3d_printer_state).state, id(appliance_3d_printer_stop_ts).state, id(appliance_3d_printer_duration).state, id(appliance_3d_printer_stop_local).state ); } // Persistent 32-bit FNV-1a hash. A separate value is retained for each page. uint32_t signature_hash = 2166136261UL; for (const unsigned char character : signature) { signature_hash ^= static_cast(character); signature_hash *= 16777619UL; } const uint32_t previous_hash = id(current_page) == 0 ? id(last_jobs_signature_hash) : id(last_appliances_signature_hash); id(display_update_required) = force || previous_hash == 0 || signature_hash != previous_hash; if (id(display_update_required)) { if (id(current_page) == 0) { id(last_jobs_signature_hash) = signature_hash; } else { id(last_appliances_signature_hash) = signature_hash; } ESP_LOGI( "display", "Visible content changed or page selected; refreshing page %d (hash 0x%08lX)", id(current_page) + 1, static_cast(signature_hash) ); } else { ESP_LOGI( "display", "Page %d content unchanged (hash 0x%08lX); e-paper refresh skipped", id(current_page) + 1, static_cast(signature_hash) ); } - if: condition: lambda: |- return id(display_update_required); then: - light.turn_on: onboard_led - component.update: epaper_display - delay: 300ms - light.turn_off: onboard_led - globals.set: id: display_update_required value: "false" #:########################################################################################:# # PERIODIC DISPLAY AND SLEEP CHECKS: # The display check matters while MQTT OFF keeps the device awake. The sleep controller # uses inactivity rather than a fixed run_duration, so every button press resets the timer. # https://esphome.io/components/interval.html #:########################################################################################:# interval: - interval: "${display_check_interval}" then: - if: condition: lambda: |- return id(boot_sequence_complete); then: - script.execute: id: refresh_display_if_changed force: false - interval: 2s then: - if: condition: lambda: |- if (!id(boot_sequence_complete)) return false; if (!id(sleep_allowed)) return false; if (id(button_left).state || id(button_right).state) return false; const uint32_t elapsed = millis() - id(last_activity_ms); return elapsed >= id(awake_timeout_ms); then: - logger.log: "Inactivity period complete; preparing to enter five-minute deep sleep." # Avoid immediately waking again if the user is only just releasing a button. - delay: 350ms - if: condition: lambda: |- return id(sleep_allowed) && !id(button_left).state && !id(button_right).state; then: # Write the closed-eye icon before power-down. If the display already # shows closed eyes and nothing else changed, the hash check skips the # physical e-paper refresh. - lambda: |- id(panel_awake_indicator) = false; - script.execute: id: refresh_display_if_changed force: false - script.wait: refresh_display_if_changed - mqtt.publish: topic: "${mqtt_status_topic}/state" payload: "sleeping" qos: 1 retain: true - delay: 350ms - deep_sleep.enter: deep_sleep_1 #:########################################################################################:# # MQTT COMMANDS: # Retained ON allows sleep; retained OFF prevents sleep for OTA/development. # This extends common/mqtt_common.yaml with device-specific subscriptions. #:########################################################################################:# mqtt: discovery: false on_message: - topic: "${mqtt_command_topic}/deepsleep" payload: "ON" then: - lambda: |- id(sleep_allowed) = true; id(last_activity_ms) = millis(); id(awake_timeout_ms) = ${mqtt_enable_awake_seconds} * 1000UL; - deep_sleep.allow: deep_sleep_1 - logger.log: "Deep sleep ALLOWED by retained MQTT command." - mqtt.publish: topic: "${mqtt_status_topic}/deepsleep" payload: "ON" qos: 1 retain: true - topic: "${mqtt_command_topic}/deepsleep" payload: "OFF" then: - lambda: |- id(sleep_allowed) = false; id(panel_awake_indicator) = true; - deep_sleep.prevent: deep_sleep_1 - logger.log: "Deep sleep PREVENTED by retained MQTT command." - mqtt.publish: topic: "${mqtt_status_topic}/deepsleep" payload: "OFF" qos: 1 retain: true - mqtt.publish: topic: "${mqtt_status_topic}/state" payload: "awake" qos: 1 retain: true - if: condition: lambda: |- return id(boot_sequence_complete); then: - script.execute: id: refresh_display_if_changed force: false #:########################################################################################:# # DEEP SLEEP: # Five-minute timer wake plus either active-low white button through ESP32-S3 EXT1. # GPIO4 = right/appliances; GPIO5 = left/jobs. # https://esphome.io/components/deep_sleep.html #:########################################################################################:# deep_sleep: id: deep_sleep_1 sleep_duration: "${sleep_interval}" esp32_ext1_wakeup: pins: - number: GPIO4 allow_other_uses: true - number: GPIO5 allow_other_uses: true mode: ANY_LOW #:########################################################################################:# # HOME ASSISTANT JOB STATE IMPORTS: # Jobs Tracker states and display attributes # https://esphome.io/components/text_sensor/homeassistant.html # https://esphome.io/components/sensor/homeassistant.html #:########################################################################################:# text_sensor: # Device diagnostics retained from the initial build. - platform: wifi_info ip_address: id: wifi_ip_address name: "${friendly_name} IP Address" entity_category: diagnostic #:######################################################################################:# # Job states and completion times # #:######################################################################################:# - platform: homeassistant id: job_dog_feeding_morning_state entity_id: sensor.jobs_tracker_dog_feeding_morning internal: true - platform: homeassistant id: job_dog_feeding_morning_completed_time entity_id: sensor.jobs_tracker_dog_feeding_morning attribute: last_completed_time internal: true - platform: homeassistant id: job_dog_feeding_evening_state entity_id: sensor.jobs_tracker_dog_feeding_evening internal: true - platform: homeassistant id: job_dog_feeding_evening_completed_time entity_id: sensor.jobs_tracker_dog_feeding_evening attribute: last_completed_time internal: true - platform: homeassistant id: job_dog_depooping_state entity_id: sensor.jobs_tracker_dog_depooping internal: true - platform: homeassistant id: job_dog_depooping_completed_time entity_id: sensor.jobs_tracker_dog_depooping attribute: last_completed_time internal: true - platform: homeassistant id: job_dishwasher_unpacked_state entity_id: sensor.jobs_tracker_dishwasher_unpacked internal: true - platform: homeassistant id: job_dishwasher_unpacked_completed_time entity_id: sensor.jobs_tracker_dishwasher_unpacked attribute: last_completed_time internal: true - platform: homeassistant id: job_rubbish_removed_state entity_id: sensor.jobs_tracker_rubbish_removed internal: true - platform: homeassistant id: job_rubbish_removed_completed_time entity_id: sensor.jobs_tracker_rubbish_removed attribute: last_completed_time internal: true #:######################################################################################:# # Appliance states and completion-local timestamps # #:######################################################################################:# - platform: homeassistant id: appliance_washing_state entity_id: sensor.appliance_monitor_washing_machine_monitor internal: true - platform: homeassistant id: appliance_washing_stop_local entity_id: sensor.appliance_monitor_washing_machine_monitor attribute: cycle_stop_local internal: true - platform: homeassistant id: appliance_dryer_state entity_id: sensor.appliance_monitor_dryer_monitor internal: true - platform: homeassistant id: appliance_dryer_stop_local entity_id: sensor.appliance_monitor_dryer_monitor attribute: cycle_stop_local internal: true - platform: homeassistant id: appliance_main_dishwasher_state entity_id: sensor.appliance_monitor_main_dishwasher_monitor internal: true - platform: homeassistant id: appliance_main_dishwasher_stop_local entity_id: sensor.appliance_monitor_main_dishwasher_monitor attribute: cycle_stop_local internal: true - platform: homeassistant id: appliance_downstairs_dishwasher_state entity_id: sensor.appliance_monitor_downstairs_dishwasher_monitor internal: true - platform: homeassistant id: appliance_downstairs_dishwasher_stop_local entity_id: sensor.appliance_monitor_downstairs_dishwasher_monitor attribute: cycle_stop_local internal: true - platform: homeassistant id: appliance_evcharger_32a_state entity_id: sensor.appliance_monitor_ev_charger_32a_monitor internal: true - platform: homeassistant id: appliance_evcharger_32a_stop_local entity_id: sensor.appliance_monitor_ev_charger_32a_monitor attribute: cycle_stop_local internal: true - platform: homeassistant id: appliance_evcharger_16a_state entity_id: sensor.appliance_monitor_ev_charger_16a_monitor internal: true - platform: homeassistant id: appliance_evcharger_16a_stop_local entity_id: sensor.appliance_monitor_ev_charger_16a_monitor attribute: cycle_stop_local internal: true - platform: homeassistant id: appliance_ryobi_left_state entity_id: sensor.appliance_monitor_ryobicharger_left_monitor internal: true - platform: homeassistant id: appliance_ryobi_left_stop_local entity_id: sensor.appliance_monitor_ryobicharger_left_monitor attribute: cycle_stop_local internal: true - platform: homeassistant id: appliance_ryobi_right_state entity_id: sensor.appliance_monitor_ryobicharger_right_monitor internal: true - platform: homeassistant id: appliance_ryobi_right_stop_local entity_id: sensor.appliance_monitor_ryobicharger_right_monitor attribute: cycle_stop_local internal: true - platform: homeassistant id: appliance_pool_pump_state entity_id: sensor.appliance_monitor_pool_pump_monitor internal: true - platform: homeassistant id: appliance_pool_pump_stop_local entity_id: sensor.appliance_monitor_pool_pump_monitor attribute: cycle_stop_local internal: true - platform: homeassistant id: appliance_3d_printer_state entity_id: sensor.appliance_monitor_3d_printer_monitor internal: true - platform: homeassistant id: appliance_3d_printer_stop_local entity_id: sensor.appliance_monitor_3d_printer_monitor attribute: cycle_stop_local internal: true #:########################################################################################:# # HOME ASSISTANT NUMERIC ATTRIBUTE IMPORTS: # Job due timestamps plus appliance cycle stop times and durations #:########################################################################################:# sensor: #:######################################################################################:# # Job current due timestamps # #:######################################################################################:# - platform: homeassistant id: job_dog_feeding_morning_due_ts entity_id: sensor.jobs_tracker_dog_feeding_morning attribute: current_due_ts internal: true - platform: homeassistant id: job_dog_feeding_evening_due_ts entity_id: sensor.jobs_tracker_dog_feeding_evening attribute: current_due_ts internal: true - platform: homeassistant id: job_dog_depooping_due_ts entity_id: sensor.jobs_tracker_dog_depooping attribute: current_due_ts internal: true - platform: homeassistant id: job_dishwasher_unpacked_due_ts entity_id: sensor.jobs_tracker_dishwasher_unpacked attribute: current_due_ts internal: true - platform: homeassistant id: job_rubbish_removed_due_ts entity_id: sensor.jobs_tracker_rubbish_removed attribute: current_due_ts internal: true #:######################################################################################:# # Appliance cycle stop timestamps and completed cycle durations # #:######################################################################################:# - platform: homeassistant id: appliance_washing_stop_ts entity_id: sensor.appliance_monitor_washing_machine_monitor attribute: cycle_stop_ts internal: true - platform: homeassistant id: appliance_washing_duration entity_id: sensor.appliance_monitor_washing_machine_monitor attribute: cycle_duration_seconds internal: true - platform: homeassistant id: appliance_dryer_stop_ts entity_id: sensor.appliance_monitor_dryer_monitor attribute: cycle_stop_ts internal: true - platform: homeassistant id: appliance_dryer_duration entity_id: sensor.appliance_monitor_dryer_monitor attribute: cycle_duration_seconds internal: true - platform: homeassistant id: appliance_main_dishwasher_stop_ts entity_id: sensor.appliance_monitor_main_dishwasher_monitor attribute: cycle_stop_ts internal: true - platform: homeassistant id: appliance_main_dishwasher_duration entity_id: sensor.appliance_monitor_main_dishwasher_monitor attribute: cycle_duration_seconds internal: true - platform: homeassistant id: appliance_downstairs_dishwasher_stop_ts entity_id: sensor.appliance_monitor_downstairs_dishwasher_monitor attribute: cycle_stop_ts internal: true - platform: homeassistant id: appliance_downstairs_dishwasher_duration entity_id: sensor.appliance_monitor_downstairs_dishwasher_monitor attribute: cycle_duration_seconds internal: true - platform: homeassistant id: appliance_evcharger_32a_stop_ts entity_id: sensor.appliance_monitor_ev_charger_32a_monitor attribute: cycle_stop_ts internal: true - platform: homeassistant id: appliance_evcharger_32a_duration entity_id: sensor.appliance_monitor_ev_charger_32a_monitor attribute: cycle_duration_seconds internal: true - platform: homeassistant id: appliance_evcharger_16a_stop_ts entity_id: sensor.appliance_monitor_ev_charger_16a_monitor attribute: cycle_stop_ts internal: true - platform: homeassistant id: appliance_evcharger_16a_duration entity_id: sensor.appliance_monitor_ev_charger_16a_monitor attribute: cycle_duration_seconds internal: true - platform: homeassistant id: appliance_ryobi_left_stop_ts entity_id: sensor.appliance_monitor_ryobicharger_left_monitor attribute: cycle_stop_ts internal: true - platform: homeassistant id: appliance_ryobi_left_duration entity_id: sensor.appliance_monitor_ryobicharger_left_monitor attribute: cycle_duration_seconds internal: true - platform: homeassistant id: appliance_ryobi_right_stop_ts entity_id: sensor.appliance_monitor_ryobicharger_right_monitor attribute: cycle_stop_ts internal: true - platform: homeassistant id: appliance_ryobi_right_duration entity_id: sensor.appliance_monitor_ryobicharger_right_monitor attribute: cycle_duration_seconds internal: true - platform: homeassistant id: appliance_pool_pump_stop_ts entity_id: sensor.appliance_monitor_pool_pump_monitor attribute: cycle_stop_ts internal: true - platform: homeassistant id: appliance_pool_pump_duration entity_id: sensor.appliance_monitor_pool_pump_monitor attribute: cycle_duration_seconds internal: true - platform: homeassistant id: appliance_3d_printer_stop_ts entity_id: sensor.appliance_monitor_3d_printer_monitor attribute: cycle_stop_ts internal: true - platform: homeassistant id: appliance_3d_printer_duration entity_id: sensor.appliance_monitor_3d_printer_monitor attribute: cycle_duration_seconds internal: true #:########################################################################################:# # OUTPUTS: # Onboard green LED # https://esphome.io/components/output/gpio.html #:########################################################################################:# output: - platform: gpio id: onboard_led_output pin: GPIO6 inverted: true #:########################################################################################:# # LIGHTS: # https://esphome.io/components/light/binary.html #:########################################################################################:# light: - platform: binary id: onboard_led name: "${friendly_name} Onboard LED" output: onboard_led_output restore_mode: ALWAYS_OFF entity_category: diagnostic #:########################################################################################:# # PHYSICAL BUTTONS: # Buttons reset the post-interaction timeout. During deep sleep the white buttons are also # EXT1 wake sources; the wake-status mask selects the page before Home Assistant data loads. # https://esphome.io/components/binary_sensor/gpio.html #:########################################################################################:# binary_sensor: - platform: gpio id: button_refresh name: "${friendly_name} Green Refresh Button" pin: number: GPIO3 mode: input: true pullup: true inverted: true filters: - delayed_on: 20ms - delayed_off: 20ms on_press: then: - lambda: |- id(panel_awake_indicator) = true; id(last_activity_ms) = millis(); id(awake_timeout_ms) = ${button_wake_awake_seconds} * 1000UL; - script.execute: id: refresh_display_if_changed force: true - platform: gpio id: button_right name: "${friendly_name} Right Button - Appliances" pin: number: GPIO4 allow_other_uses: true mode: input: true pullup: true inverted: true filters: - delayed_on: 20ms - delayed_off: 20ms on_press: then: - lambda: |- id(panel_awake_indicator) = true; id(last_activity_ms) = millis(); id(awake_timeout_ms) = ${button_wake_awake_seconds} * 1000UL; - if: condition: lambda: |- return id(current_page) != 1; then: - globals.set: id: current_page value: "1" - display.page.show: page_appliances - script.execute: id: refresh_display_if_changed force: true else: - script.execute: id: refresh_display_if_changed force: false - platform: gpio id: button_left name: "${friendly_name} Left Button - Jobs" pin: number: GPIO5 allow_other_uses: true mode: input: true pullup: true inverted: true filters: - delayed_on: 20ms - delayed_off: 20ms on_press: then: - lambda: |- id(panel_awake_indicator) = true; id(last_activity_ms) = millis(); id(awake_timeout_ms) = ${button_wake_awake_seconds} * 1000UL; - if: condition: lambda: |- return id(current_page) != 0; then: - globals.set: id: current_page value: "0" - display.page.show: page_jobs - script.execute: id: refresh_display_if_changed force: true else: - script.execute: id: refresh_display_if_changed force: false #:########################################################################################:# # HOME ASSISTANT CONTROLS: # Manual refresh and page-selection buttons # https://esphome.io/components/button/template.html #:########################################################################################:# button: - platform: template id: refresh_display_button name: "${friendly_name} Refresh Display" icon: "mdi:refresh" on_press: then: - lambda: |- id(panel_awake_indicator) = true; id(last_activity_ms) = millis(); id(awake_timeout_ms) = ${button_wake_awake_seconds} * 1000UL; - script.execute: id: refresh_display_if_changed force: true - platform: template id: show_jobs_page_button name: "${friendly_name} Show Jobs Page" icon: "mdi:clipboard-check-outline" on_press: then: - lambda: |- id(panel_awake_indicator) = true; id(last_activity_ms) = millis(); id(awake_timeout_ms) = ${button_wake_awake_seconds} * 1000UL; - if: condition: lambda: |- return id(current_page) != 0; then: - globals.set: id: current_page value: "0" - display.page.show: page_jobs - script.execute: id: refresh_display_if_changed force: true else: - script.execute: id: refresh_display_if_changed force: false - platform: template id: show_appliances_page_button name: "${friendly_name} Show Appliances Page" icon: "mdi:washing-machine" on_press: then: - lambda: |- id(panel_awake_indicator) = true; id(last_activity_ms) = millis(); id(awake_timeout_ms) = ${button_wake_awake_seconds} * 1000UL; - if: condition: lambda: |- return id(current_page) != 1; then: - globals.set: id: current_page value: "1" - display.page.show: page_appliances - script.execute: id: refresh_display_if_changed force: true else: - script.execute: id: refresh_display_if_changed force: false