2051 lines
73 KiB
YAML
2051 lines
73 KiB
YAML
#:########################################################################################:#
|
|
# 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<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:
|
|
# 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<int64_t>(due_ts) - now_ts;
|
|
const int hours = static_cast<int>((remaining + 3599) / 3600);
|
|
if (hours <= hour_limit) {
|
|
status = "Due: " + std::to_string(hours) + (hours == 1 ? " Hour" : " Hours");
|
|
} else {
|
|
const int days = static_cast<int>((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<int64_t>(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<long long>(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<int64_t>(due_ts) - now_ts;
|
|
const int hours = static_cast<int>((remaining + 3599) / 3600);
|
|
if (hours <= hour_limit) {
|
|
append_text("hours");
|
|
append_number(hours);
|
|
} else {
|
|
const int days = static_cast<int>((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<int64_t>(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<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)) {
|
|
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_LOGI(
|
|
"display",
|
|
"Page %d content unchanged (hash 0x%08lX); e-paper refresh skipped",
|
|
id(current_page) + 1,
|
|
static_cast<unsigned long>(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
|