Edit esp-reterminal-e1001.yaml
This commit is contained in:
@@ -6,6 +6,13 @@
|
||||
# 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
|
||||
@@ -35,20 +42,34 @@
|
||||
# - Display busy: GPIO13, inverted
|
||||
#:########################################################################################:#
|
||||
# OPERATION:
|
||||
# - The left white button selects the View Road Jobs page.
|
||||
# - The right white button selects the View Road Appliances page.
|
||||
# - The display is checked every 30 seconds, but the e-paper panel is refreshed only when
|
||||
# the visible page content has actually changed.
|
||||
# - Page changes and manual refreshes intentionally force one physical display refresh.
|
||||
# - Job countdown wording changes only when its displayed hour/day value changes.
|
||||
# - Job tiles use large job icons, two-line names and clear state icons.
|
||||
# - OVERDUE and MISSED job tiles use a black background with white text and icons.
|
||||
# - Appliance tiles show OPERATING while active.
|
||||
# - A completed appliance shows inverse FINISHED for one hour after cycle completion.
|
||||
# - After one hour, the appliance tile shows Completed at HH:MM with a normal background.
|
||||
# - 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 not required on this ESPHome device.
|
||||
# - Deep sleep remains disabled during dashboard development and OTA testing.
|
||||
# - 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.
|
||||
#:########################################################################################:#
|
||||
|
||||
#:########################################################################################:#
|
||||
@@ -60,16 +81,23 @@ 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 icon-based job tiles. (Layout V1.1)"
|
||||
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.4"
|
||||
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"
|
||||
@@ -77,6 +105,15 @@ substitutions:
|
||||
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
|
||||
@@ -98,11 +135,12 @@ packages:
|
||||
local_api_key: "${api_key}"
|
||||
|
||||
#### MQTT ####
|
||||
# Home Assistant entity states are imported through the ESPHome native API.
|
||||
#common_mqtt: !include
|
||||
# file: common/mqtt_common.yaml
|
||||
# vars:
|
||||
# local_device_name: "${device_name}"
|
||||
# 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
|
||||
@@ -131,17 +169,170 @@ esphome:
|
||||
version: "${project_version}"
|
||||
|
||||
on_boot:
|
||||
priority: -100
|
||||
then:
|
||||
# Allow Wi-Fi, API entities and Home Assistant time to initialise.
|
||||
- delay: 15s
|
||||
- display.page.show: page_jobs
|
||||
- globals.set:
|
||||
id: current_page
|
||||
value: "0"
|
||||
- script.execute:
|
||||
id: refresh_display_if_changed
|
||||
force: true
|
||||
#:####################################################################################:#
|
||||
# EARLY WAKE-SOURCE DETECTION #
|
||||
#:####################################################################################:#
|
||||
- priority: 800
|
||||
then:
|
||||
- lambda: |-
|
||||
id(wakeup_cause) = static_cast<int>(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<unsigned long long>(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<unsigned long>(id(awake_timeout_ms) / 1000UL)
|
||||
);
|
||||
|
||||
#:########################################################################################:#
|
||||
# ESP PLATFORM AND FRAMEWORK:
|
||||
@@ -199,25 +390,80 @@ time:
|
||||
|
||||
#:########################################################################################:#
|
||||
# GLOBALS:
|
||||
# Display page and change-detection state
|
||||
# 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: no
|
||||
restore_value: yes
|
||||
initial_value: "0"
|
||||
|
||||
- id: last_display_signature
|
||||
type: std::string
|
||||
restore_value: no
|
||||
initial_value: '""'
|
||||
# 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.
|
||||
@@ -409,6 +655,22 @@ display:
|
||||
}
|
||||
};
|
||||
|
||||
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";
|
||||
|
||||
@@ -462,7 +724,7 @@ display:
|
||||
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") {
|
||||
} 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);
|
||||
@@ -504,13 +766,16 @@ display:
|
||||
}
|
||||
} else if (state == "Due") {
|
||||
status = "DUE NOW";
|
||||
border = emphasis_border;
|
||||
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;
|
||||
}
|
||||
@@ -559,6 +824,7 @@ display:
|
||||
|
||||
// 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,
|
||||
@@ -651,6 +917,22 @@ display:
|
||||
}
|
||||
};
|
||||
|
||||
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";
|
||||
|
||||
@@ -765,6 +1047,7 @@ display:
|
||||
|
||||
// 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,
|
||||
@@ -860,7 +1143,7 @@ display:
|
||||
#:########################################################################################:#
|
||||
# DISPLAY CHANGE DETECTION:
|
||||
# Builds a signature from only the content that is currently visible. The display is not
|
||||
# physically refreshed when the visible wording and style would remain unchanged.
|
||||
# physically refreshed when the visible wording, style and eye indicator would remain unchanged.
|
||||
# https://esphome.io/components/script.html
|
||||
#:########################################################################################:#
|
||||
script:
|
||||
@@ -892,6 +1175,8 @@ script:
|
||||
};
|
||||
|
||||
// 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);
|
||||
@@ -1036,13 +1321,41 @@ script:
|
||||
);
|
||||
}
|
||||
|
||||
id(display_update_required) = force || signature != id(last_display_signature);
|
||||
// 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<uint32_t>(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)) {
|
||||
id(last_display_signature) = signature;
|
||||
ESP_LOGI("display", "Visible display content changed; refreshing page %d", id(current_page) + 1);
|
||||
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<unsigned long>(signature_hash)
|
||||
);
|
||||
} else {
|
||||
ESP_LOGD("display", "Visible display content unchanged; e-paper refresh skipped");
|
||||
ESP_LOGI(
|
||||
"display",
|
||||
"Page %d content unchanged (hash 0x%08lX); e-paper refresh skipped",
|
||||
id(current_page) + 1,
|
||||
static_cast<unsigned long>(signature_hash)
|
||||
);
|
||||
}
|
||||
|
||||
- if:
|
||||
@@ -1059,17 +1372,134 @@ script:
|
||||
value: "false"
|
||||
|
||||
#:########################################################################################:#
|
||||
# PERIODIC DISPLAY CHECK:
|
||||
# This checks in-memory values only; it does not refresh the e-paper unless the signature
|
||||
# differs from the last rendered content.
|
||||
# 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:
|
||||
- script.execute:
|
||||
id: refresh_display_if_changed
|
||||
force: false
|
||||
- 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:
|
||||
@@ -1446,7 +1876,8 @@ light:
|
||||
|
||||
#:########################################################################################:#
|
||||
# PHYSICAL BUTTONS:
|
||||
# Left white selects Jobs; right white selects Appliances; green performs a forced refresh.
|
||||
# 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:
|
||||
@@ -1464,6 +1895,10 @@ binary_sensor:
|
||||
- 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
|
||||
@@ -1473,6 +1908,7 @@ binary_sensor:
|
||||
name: "${friendly_name} Right Button - Appliances"
|
||||
pin:
|
||||
number: GPIO4
|
||||
allow_other_uses: true
|
||||
mode:
|
||||
input: true
|
||||
pullup: true
|
||||
@@ -1482,6 +1918,10 @@ binary_sensor:
|
||||
- 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: |-
|
||||
@@ -1494,12 +1934,17 @@ binary_sensor:
|
||||
- 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
|
||||
@@ -1509,6 +1954,10 @@ binary_sensor:
|
||||
- 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: |-
|
||||
@@ -1521,6 +1970,10 @@ binary_sensor:
|
||||
- script.execute:
|
||||
id: refresh_display_if_changed
|
||||
force: true
|
||||
else:
|
||||
- script.execute:
|
||||
id: refresh_display_if_changed
|
||||
force: false
|
||||
|
||||
#:########################################################################################:#
|
||||
# HOME ASSISTANT CONTROLS:
|
||||
@@ -1534,6 +1987,10 @@ button:
|
||||
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
|
||||
@@ -1544,6 +2001,10 @@ button:
|
||||
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: |-
|
||||
@@ -1556,6 +2017,10 @@ button:
|
||||
- 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
|
||||
@@ -1563,6 +2028,10 @@ button:
|
||||
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: |-
|
||||
@@ -1575,3 +2044,7 @@ button:
|
||||
- script.execute:
|
||||
id: refresh_display_if_changed
|
||||
force: true
|
||||
else:
|
||||
- script.execute:
|
||||
id: refresh_display_if_changed
|
||||
force: false
|
||||
|
||||
Reference in New Issue
Block a user