Wake a screen or put it to sleep from Home Assistant (0.2.53)

Every screen gets two template buttons, Wake and Sleep, for automations
(button.press on button.<screen>_wake / _sleep). Wake does what a tap does:
a dimmed screen runs wake_display, a screen that is on only restarts the
standby count. Sleep dims right away through dim_display, also with Auto
standby off, and closes an alert that is up (reported as remote); it is
ignored during a touch calibration.

A new non-restored flag, sleep_requested, keeps that standby in place:
dim_display dims with standby off, and apply_screen_settings, which runs
every minute, no longer wakes such a screen. wake_display clears it (a tap,
Wake, an alert), and so does the Auto standby switch when it really turns
standby off. Nothing is saved, so the buttons cost no flash writes and may
run on every motion.

The Claude skill, README, add-on docs and RELEASING describe both buttons.
Checked on host builds of both boards over the native API, 24 checks each
(timer, Sleep with Auto standby off across a minute tick, alert,
calibration), and with ESP32 builds of both Easy profiles (+336 B RAM).
Firmware 0.2.45.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-16 10:35:06 +02:00
parent b83164d61a
commit da20782d3d
15 changed files with 499 additions and 23 deletions
+30 -1
View File
@@ -93,7 +93,9 @@ The firmware and built-in CLI are tested with **ESPHome 2026.6.2**.
- **Screen settings:** standby time, normal and dimmed brightness,
night hours, 24- or 12-hour clock, return to the home page, and optional swiping
between pages. Home Assistant automations can turn **Auto standby** off and on per
screen (firmware 0.2.41+), for example to keep a screen on while someone is home.
screen (firmware 0.2.41+), for example to keep a screen on while someone is home, and
wake a screen or put it to sleep with its **Wake** and **Sleep** buttons (firmware 0.2.45+).
See [Wake and sleep](#wake-and-sleep-from-an-automation).
- **Settings on the screen itself** (firmware 0.2.44+): hold the top bar for about a
second and a half and the screen opens its own settings page — brightness, night,
the clock, swiping, rotation, and what this screen is (name, IP address, firmware,
@@ -209,6 +211,33 @@ written until you press the button.
<img src="docs/images/editor-settings.png" width="80%" alt="Settings in ESP Screens: New screen and Firmware & USB, the firmware updates, the Alerts cheatsheet, and the Claude skill">
</p>
## Wake and sleep from an automation
Every screen has two buttons in Home Assistant (firmware 0.2.45+). An automation presses them
with the `button.press` action:
- **`button.<screen>_wake`** does what a tap does: a screen in standby lights up, and the standby
time starts counting again. On a screen that is already on, only the count starts again.
- **`button.<screen>_sleep`** puts the screen in standby right away, the same as when the standby
time runs out, and also works with **Auto standby** off. The screen stays in standby until someone
taps it, **Wake** is pressed or an alert comes in. An alert that is showing closes (reported as `remote`).
Neither button saves anything on the screen, so an automation may press them as often as it likes,
on every motion too. That is the difference with the **Auto standby** switch, which is a setting.
To reach several screens at once, list their buttons:
```yaml
actions:
- action: button.press
target:
entity_id:
- button.kitchen_screen_sleep
- button.living_room_screen_sleep
```
Don't target an area or a device with `button.press`: that presses every other button there too,
the Wake and Sleep of the same screen included.
## Installing from Home Assistant
### Do I need ESPHome?
+20
View File
@@ -176,6 +176,26 @@ icons sit off-center in the browser); run `generate_packages.py` afterward.
The `MDI_GLYPH_*` substitutions are retired: `TILEn_ICON` in manual
profiles must come from the set. No changed preferences or keys.
### Compatibility 0.2.53 / firmware 0.2.45
Firmware and board profiles; the app raises `FIRMWARE_VERSION` and extends the Claude skill. Storage,
tile protocol, preferences and keys are unchanged.
- Both profiles add two template buttons, `wake_button` ("Wake", `mdi:gesture-tap`) and `sleep_button`
("Sleep", `mdi:power-sleep`), without an entity category: they are controls. Wake runs `wake_display`
on a dimmed screen (the path the tap on `dim_wake_overlay` takes) and only sets `last_touch_ms` on a
screen that is on. Sleep is ignored during a touch calibration; otherwise it sets the new global
`sleep_requested`, runs `alert_dismiss` (reason `remote`, a no-op without an alert) and runs
`dim_display`. Both log a line with tag `standby` at INFO.
- `sleep_requested` (bool, not restored) lets `dim_display` dim with `standby_enabled` off and stops
`apply_screen_settings`, which runs every minute, from waking a dimmed screen while standby is off.
`wake_display` clears it (a tap, Wake, an alert, `open_settings`, `touch_diagnostics`), and so does
the Auto standby switch when it really turns standby off; a switch that was already off leaves it.
Nothing is saved, so the buttons cost no flash writes.
- `core.WAKE_SLEEP_MIN_FIRMWARE` is 0.2.45. `claude_skill.text()` lists both buttons in "Standby and
brightness" with one more YAML example (nine in total), so an installed skill shows as outdated until
it is installed again. The skill description stays under 200 bytes.
### Compatibility 0.2.52 / firmware 0.2.44
App and firmware. Storage version and tile protocol stay 1; everything is additive, so an old screen
+65
View File
@@ -0,0 +1,65 @@
# Test results app 0.2.53 / firmware 0.2.45 (2026-09-16)
Wake and Sleep buttons for Home Assistant automations. Firmware and board profiles change; the app only
raises `FIRMWARE_VERSION` and extends the Claude skill. Storage, tile protocol, preferences and keys are
unchanged. See CHANGELOG 0.2.53 and the compatibility note in docs/RELEASING.md.
## Automated
- Python (`.venv-portal`): 206 tests OK. New `tests/test_wake_sleep.py` (6 tests, run against both
board profiles and both generated packages): the Wake and Sleep buttons exist as template buttons
without an entity category and not internal; Wake runs `wake_display` on a dimmed screen (the same
script the tap on `dim_wake_overlay` runs) and only restarts `last_touch_ms` otherwise; Sleep is
ignored during a touch calibration and otherwise sets `sleep_requested`, closes an alert
(`remote`) and runs `dim_display`; `dim_display` and `apply_screen_settings` honour the flag;
`wake_display` clears it; the flag is a non-restored bool; the Auto standby switch clears it only when
it really turns standby off. `tests/test_claude_skill.py` checks both buttons, the minimum firmware
and the new YAML example (nine blocks). `tests/test_settings_page.py` now asks for firmware 0.2.44
or newer instead of exactly 0.2.44.
- C++ (`clang++ -std=c++17 -Wall -Wextra -Werror -I.`): 14/14 PASS (no C++ changed).
- `generate_packages.py --check` green. `generate_icons.py --check` not run: no glyphs changed, and
fontTools is not installed in this environment.
## Builds (ESPHome 2026.6.2)
Both Easy Setup profiles, compiled one after the other from packages generated out of the board profiles:
| Profile | RAM | Flash | vs 0.2.52 |
|---|---|---|---|
| easy-cyd-device.yaml (package, local components) | 23.1% (75,612 B) | 76.1% (1,396,427 B) | +336 B RAM, +1,284 B flash |
| easy-guition-device.yaml (package, local components) | 24.1% (78,844 B) | 18.4% (1,496,071 B) | +336 B RAM, +1,996 B flash |
## Behaviour on the real profile logic (host build)
The generated packages were built for the Mac with ESPHome's host platform (SDL display, template
backlight output, host time; the readme-render tooling) under their own device names, and driven over
the native API: button presses, the Auto standby switch, the profile's own actions (`show_alert`,
`dismiss_alert`, `touch_diagnostics`), plus three test-only actions that log `display_dimmed`,
`sleep_requested`, the wake overlay, the backlight target and the idle time, click the wake overlay,
and run `apply_screen_settings`. Result: 24/24 checks passed on the Guition package and 24/24 on the CYD package.
Checked on both boards:
1. Boot: awake, Auto standby on, full brightness. Buttons listed as `wake` / `sleep` with
`mdi:gesture-tap` / `mdi:power-sleep`, entity category none.
2. The standby timer still dims after the standby time, and a tap on the overlay wakes.
3. Sleep with Auto standby on dims at once to the standby level, overlay up, `Sleep from Home
Assistant` in the log; Wake brings full brightness back and clears the flag.
4. Wake on a screen that is on restarts the idle count (300 s -> under 3 s) and changes nothing else.
5. With Auto standby off, Sleep dims; an explicit `apply_screen_settings` and a real minute tick
(`on_time`) both keep it dimmed. Turning the already-off switch off again keeps it dimmed; a tap on
the overlay wakes.
6. Sleep with Auto standby on, then switching Auto standby off: the screen wakes.
7. Sleep during an alert closes the alert (`alert dismissed: remote` in the log) and dims; a new alert
wakes the screen out of Sleep.
8. During `touch_diagnostics` (calibration active) Sleep is ignored with a log line.
## Not tested / for the owner
- Hardware. The bench boards (192.168.146.134 / .136) were not reachable, and the only screen online in
Home Assistant, Studio 1 (Guition, firmware 0.2.43), is in daily use and gets its firmware through
ESP Screens; nothing was flashed. Still to see on real glass: the backlight fade into Sleep (1.5 s on
the Guition's LEDC fader), and pressing `button.<screen>_wake` / `_sleep` from Home Assistant after
the update.
- The `esphome.screen_alert` event with `remote` after Sleep closes an alert, as Home Assistant
receives it (the host run shows the dismiss path and its log line only).
+53 -3
View File
@@ -2,7 +2,7 @@
# Source profile for packages/guition.yaml (tools/generate_packages.py); see docs/GUITION.md.
# A screen gets its tiles from the ESP Screen Manager add-on while it runs. The CYD is its own profile.
substitutions:
SCREEN_FIRMWARE_VERSION: "0.2.44"
SCREEN_FIRMWARE_VERSION: "0.2.45"
# 0: no drift limit; LVGL decides, releasing inside the tile is a tap, leaving it is not
# (like iOS). The CYD keeps a limit because its resistive panel jumps.
TOUCH_MOVE_LIMIT_PX: "0"
@@ -571,6 +571,13 @@ globals:
restore_value: no
initial_value: 'false'
# Sleep from Home Assistant (0.2.45+): standby that also holds with Auto standby off,
# until a tap, Wake or an alert wakes the screen.
- id: sleep_requested
type: bool
restore_value: no
initial_value: 'false'
- id: active_label_off
type: std::string
restore_value: no
@@ -1058,6 +1065,8 @@ switch:
auto &s = screen_settings::current;
if (!s.standby_enabled) return;
s.standby_enabled = false;
// Switching it off wakes the screen, also out of Sleep; a switch that was already off leaves Sleep alone.
id(sleep_requested) = false;
if (runtime_tiles::enabled) runtime_tiles::settings_preference.save(&s);
runtime_tiles::setting_event("standby_enabled", 0);
id(apply_screen_settings).execute();
@@ -3541,7 +3550,8 @@ script:
if(lv_indev_get_act())lv_indev_wait_release(lv_indev_get_act());
id(screen_lvgl).set_rotation(static_cast<esphome::display::DisplayRotation>(runtime_tiles::rotation));
}
if (id(display_dimmed) && !settings.standby_enabled) {
// Auto standby off wakes the screen, but not out of a Sleep pressed in Home Assistant.
if (id(display_dimmed) && !settings.standby_enabled && !id(sleep_requested)) {
id(display_dimmed) = false;
}
auto now = id(homeassistant_time).now();
@@ -3690,6 +3700,7 @@ script:
- lambda: |-
id(last_touch_ms) = millis();
id(display_dimmed) = false;
id(sleep_requested) = false;
// Almost instant brightness-up. ESPHome's default is 1s, and that feels sluggish
// when you touch the screen and are waiting on light.
id(set_backlight).execute(screen_settings::current.brightness, 80);
@@ -3716,7 +3727,8 @@ script:
then:
- if:
condition:
lambda: 'return screen_settings::current.standby_enabled;'
# Sleep from Home Assistant dims with Auto standby off too.
lambda: 'return screen_settings::current.standby_enabled || id(sleep_requested);'
then:
- lambda: |-
id(display_dimmed) = true;
@@ -4494,6 +4506,44 @@ text:
name: "Tile settings"
internal: ${DASHBOARD_INBOX_INTERNAL}
button:
# Wake and Sleep for Home Assistant automations (0.2.45+). Wake is a tap: it lights a dimmed
# screen and counts the standby time from now. Sleep is the standby time running out, right away
# and also with Auto standby off; it holds until a tap, Wake or an alert, and closes an alert that
# is up. Neither saves anything, so an automation may press them on every motion. No
# entity_category: they are controls, not settings.
- platform: template
id: wake_button
name: "Wake"
icon: "mdi:gesture-tap"
on_press:
- lambda: 'ESP_LOGI("standby", "Wake from Home Assistant, screen %s", id(display_dimmed) ? "in standby" : "on");'
- if:
condition:
lambda: 'return id(display_dimmed);'
then:
- script.execute: wake_display
else:
- lambda: 'id(last_touch_ms) = millis();'
- platform: template
id: sleep_button
name: "Sleep"
icon: "mdi:power-sleep"
on_press:
- if:
condition:
lambda: 'return id(calibration_active);'
then:
- lambda: 'ESP_LOGI("standby", "Sleep from Home Assistant ignored: touch calibration is running");'
else:
- lambda: |-
ESP_LOGI("standby", "Sleep from Home Assistant");
id(sleep_requested) = true;
- script.execute:
id: alert_dismiss
reason: "remote"
- script.execute: dim_display
number:
- platform: template
id: setting_brightness
+51 -3
View File
@@ -17,7 +17,7 @@
############################################################
substitutions:
SCREEN_FIRMWARE_VERSION: "0.2.44"
SCREEN_FIRMWARE_VERSION: "0.2.45"
# A tap survives a finger that drifts up to this far (320 px over 58 mm: 1 cm); further is a swipe.
TOUCH_MOVE_LIMIT_PX: "56"
# Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not.
@@ -708,6 +708,13 @@ globals:
restore_value: no
initial_value: 'false'
# Sleep from Home Assistant (0.2.45+): standby that also holds with Auto standby off,
# until a tap, Wake or an alert wakes the screen.
- id: sleep_requested
type: bool
restore_value: no
initial_value: 'false'
- id: active_label_off
type: std::string
restore_value: no
@@ -1191,6 +1198,8 @@ switch:
auto &s = screen_settings::current;
if (!s.standby_enabled) return;
s.standby_enabled = false;
// Switching it off wakes the screen, also out of Sleep; a switch that was already off leaves Sleep alone.
id(sleep_requested) = false;
if (runtime_tiles::enabled) runtime_tiles::settings_preference.save(&s);
runtime_tiles::setting_event("standby_enabled", 0);
id(apply_screen_settings).execute();
@@ -3587,7 +3596,8 @@ script:
then:
- lambda: |-
const auto &settings = screen_settings::current;
if (id(display_dimmed) && !settings.standby_enabled) {
// Auto standby off wakes the screen, but not out of a Sleep pressed in Home Assistant.
if (id(display_dimmed) && !settings.standby_enabled && !id(sleep_requested)) {
id(display_dimmed) = false;
}
auto now = id(homeassistant_time).now();
@@ -3743,6 +3753,7 @@ script:
- lambda: |-
id(last_touch_ms) = millis();
id(display_dimmed) = false;
id(sleep_requested) = false;
- light.turn_on:
id: back_light
# Almost instant brightness-up. ESPHome's default is 1s, and that feels sluggish
@@ -3757,7 +3768,8 @@ script:
then:
- if:
condition:
lambda: 'return screen_settings::current.standby_enabled;'
# Sleep from Home Assistant dims with Auto standby off too.
lambda: 'return screen_settings::current.standby_enabled || id(sleep_requested);'
then:
- lambda: |-
id(display_dimmed) = true;
@@ -4507,6 +4519,42 @@ button:
entity_category: config
on_press:
- lambda: 'if (runtime_tiles::enabled) screen_calibration::begin();'
# Wake and Sleep for Home Assistant automations (0.2.45+). Wake is a tap: it lights a dimmed
# screen and counts the standby time from now. Sleep is the standby time running out, right away
# and also with Auto standby off; it holds until a tap, Wake or an alert, and closes an alert that
# is up. Neither saves anything, so an automation may press them on every motion. No
# entity_category: they are controls, not settings.
- platform: template
id: wake_button
name: "Wake"
icon: "mdi:gesture-tap"
on_press:
- lambda: 'ESP_LOGI("standby", "Wake from Home Assistant, screen %s", id(display_dimmed) ? "in standby" : "on");'
- if:
condition:
lambda: 'return id(display_dimmed);'
then:
- script.execute: wake_display
else:
- lambda: 'id(last_touch_ms) = millis();'
- platform: template
id: sleep_button
name: "Sleep"
icon: "mdi:power-sleep"
on_press:
- if:
condition:
lambda: 'return id(calibration_active);'
then:
- lambda: 'ESP_LOGI("standby", "Sleep from Home Assistant ignored: touch calibration is running");'
else:
- lambda: |-
ESP_LOGI("standby", "Sleep from Home Assistant");
id(sleep_requested) = true;
- script.execute:
id: alert_dismiss
reason: "remote"
- script.execute: dim_display
number:
- platform: template
+51 -3
View File
@@ -18,7 +18,7 @@
############################################################
substitutions:
SCREEN_FIRMWARE_VERSION: "0.2.44"
SCREEN_FIRMWARE_VERSION: "0.2.45"
# A tap survives a finger that drifts up to this far (320 px over 58 mm: 1 cm); further is a swipe.
TOUCH_MOVE_LIMIT_PX: "56"
# Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not.
@@ -697,6 +697,13 @@ globals:
restore_value: no
initial_value: 'false'
# Sleep from Home Assistant (0.2.45+): standby that also holds with Auto standby off,
# until a tap, Wake or an alert wakes the screen.
- id: sleep_requested
type: bool
restore_value: no
initial_value: 'false'
- id: active_label_off
type: std::string
restore_value: no
@@ -1180,6 +1187,8 @@ switch:
auto &s = screen_settings::current;
if (!s.standby_enabled) return;
s.standby_enabled = false;
// Switching it off wakes the screen, also out of Sleep; a switch that was already off leaves Sleep alone.
id(sleep_requested) = false;
if (runtime_tiles::enabled) runtime_tiles::settings_preference.save(&s);
runtime_tiles::setting_event("standby_enabled", 0);
id(apply_screen_settings).execute();
@@ -3576,7 +3585,8 @@ script:
then:
- lambda: |-
const auto &settings = screen_settings::current;
if (id(display_dimmed) && !settings.standby_enabled) {
// Auto standby off wakes the screen, but not out of a Sleep pressed in Home Assistant.
if (id(display_dimmed) && !settings.standby_enabled && !id(sleep_requested)) {
id(display_dimmed) = false;
}
auto now = id(homeassistant_time).now();
@@ -3732,6 +3742,7 @@ script:
- lambda: |-
id(last_touch_ms) = millis();
id(display_dimmed) = false;
id(sleep_requested) = false;
- light.turn_on:
id: back_light
# Almost instant brightness-up. ESPHome's default is 1s, and that feels sluggish
@@ -3746,7 +3757,8 @@ script:
then:
- if:
condition:
lambda: 'return screen_settings::current.standby_enabled;'
# Sleep from Home Assistant dims with Auto standby off too.
lambda: 'return screen_settings::current.standby_enabled || id(sleep_requested);'
then:
- lambda: |-
id(display_dimmed) = true;
@@ -4496,6 +4508,42 @@ button:
entity_category: config
on_press:
- lambda: 'if (runtime_tiles::enabled) screen_calibration::begin();'
# Wake and Sleep for Home Assistant automations (0.2.45+). Wake is a tap: it lights a dimmed
# screen and counts the standby time from now. Sleep is the standby time running out, right away
# and also with Auto standby off; it holds until a tap, Wake or an alert, and closes an alert that
# is up. Neither saves anything, so an automation may press them on every motion. No
# entity_category: they are controls, not settings.
- platform: template
id: wake_button
name: "Wake"
icon: "mdi:gesture-tap"
on_press:
- lambda: 'ESP_LOGI("standby", "Wake from Home Assistant, screen %s", id(display_dimmed) ? "in standby" : "on");'
- if:
condition:
lambda: 'return id(display_dimmed);'
then:
- script.execute: wake_display
else:
- lambda: 'id(last_touch_ms) = millis();'
- platform: template
id: sleep_button
name: "Sleep"
icon: "mdi:power-sleep"
on_press:
- if:
condition:
lambda: 'return id(calibration_active);'
then:
- lambda: 'ESP_LOGI("standby", "Sleep from Home Assistant ignored: touch calibration is running");'
else:
- lambda: |-
ESP_LOGI("standby", "Sleep from Home Assistant");
id(sleep_requested) = true;
- script.execute:
id: alert_dismiss
reason: "remote"
- script.execute: dim_display
number:
- platform: template
+53 -3
View File
@@ -3,7 +3,7 @@
# Source profile for packages/guition.yaml (tools/generate_packages.py); see docs/GUITION.md.
# A screen gets its tiles from the ESP Screen Manager add-on while it runs. The CYD is its own profile.
substitutions:
SCREEN_FIRMWARE_VERSION: "0.2.44"
SCREEN_FIRMWARE_VERSION: "0.2.45"
# 0: no drift limit; LVGL decides, releasing inside the tile is a tap, leaving it is not
# (like iOS). The CYD keeps a limit because its resistive panel jumps.
TOUCH_MOVE_LIMIT_PX: "0"
@@ -560,6 +560,13 @@ globals:
restore_value: no
initial_value: 'false'
# Sleep from Home Assistant (0.2.45+): standby that also holds with Auto standby off,
# until a tap, Wake or an alert wakes the screen.
- id: sleep_requested
type: bool
restore_value: no
initial_value: 'false'
- id: active_label_off
type: std::string
restore_value: no
@@ -1047,6 +1054,8 @@ switch:
auto &s = screen_settings::current;
if (!s.standby_enabled) return;
s.standby_enabled = false;
// Switching it off wakes the screen, also out of Sleep; a switch that was already off leaves Sleep alone.
id(sleep_requested) = false;
if (runtime_tiles::enabled) runtime_tiles::settings_preference.save(&s);
runtime_tiles::setting_event("standby_enabled", 0);
id(apply_screen_settings).execute();
@@ -3530,7 +3539,8 @@ script:
if(lv_indev_get_act())lv_indev_wait_release(lv_indev_get_act());
id(screen_lvgl).set_rotation(static_cast<esphome::display::DisplayRotation>(runtime_tiles::rotation));
}
if (id(display_dimmed) && !settings.standby_enabled) {
// Auto standby off wakes the screen, but not out of a Sleep pressed in Home Assistant.
if (id(display_dimmed) && !settings.standby_enabled && !id(sleep_requested)) {
id(display_dimmed) = false;
}
auto now = id(homeassistant_time).now();
@@ -3679,6 +3689,7 @@ script:
- lambda: |-
id(last_touch_ms) = millis();
id(display_dimmed) = false;
id(sleep_requested) = false;
// Almost instant brightness-up. ESPHome's default is 1s, and that feels sluggish
// when you touch the screen and are waiting on light.
id(set_backlight).execute(screen_settings::current.brightness, 80);
@@ -3705,7 +3716,8 @@ script:
then:
- if:
condition:
lambda: 'return screen_settings::current.standby_enabled;'
# Sleep from Home Assistant dims with Auto standby off too.
lambda: 'return screen_settings::current.standby_enabled || id(sleep_requested);'
then:
- lambda: |-
id(display_dimmed) = true;
@@ -4483,6 +4495,44 @@ text:
name: "Tile settings"
internal: ${DASHBOARD_INBOX_INTERNAL}
button:
# Wake and Sleep for Home Assistant automations (0.2.45+). Wake is a tap: it lights a dimmed
# screen and counts the standby time from now. Sleep is the standby time running out, right away
# and also with Auto standby off; it holds until a tap, Wake or an alert, and closes an alert that
# is up. Neither saves anything, so an automation may press them on every motion. No
# entity_category: they are controls, not settings.
- platform: template
id: wake_button
name: "Wake"
icon: "mdi:gesture-tap"
on_press:
- lambda: 'ESP_LOGI("standby", "Wake from Home Assistant, screen %s", id(display_dimmed) ? "in standby" : "on");'
- if:
condition:
lambda: 'return id(display_dimmed);'
then:
- script.execute: wake_display
else:
- lambda: 'id(last_touch_ms) = millis();'
- platform: template
id: sleep_button
name: "Sleep"
icon: "mdi:power-sleep"
on_press:
- if:
condition:
lambda: 'return id(calibration_active);'
then:
- lambda: 'ESP_LOGI("standby", "Sleep from Home Assistant ignored: touch calibration is running");'
else:
- lambda: |-
ESP_LOGI("standby", "Sleep from Home Assistant");
id(sleep_requested) = true;
- script.execute:
id: alert_dismiss
reason: "remote"
- script.execute: dim_display
number:
- platform: template
id: setting_brightness
+9
View File
@@ -1,3 +1,12 @@
## 0.2.53 (firmware 0.2.45)
Wake a screen or put it to sleep from Home Assistant.
- **Wake and Sleep buttons.** Every screen has `button.<screen>_wake` and `button.<screen>_sleep` in Home Assistant, pressed with `button.press`. Wake does what a tap does: a screen in standby lights up and the standby time counts again from that moment. Sleep puts the screen in standby right away, just like when the standby time runs out, and also works with Auto standby off: the screen stays in standby until someone taps it, Wake is pressed or an alert comes in. An alert that is showing closes, reported as `remote`.
- **As often as you like.** Neither button saves anything on the screen, unlike the Auto standby switch, so an automation may press Wake on every motion. To reach several screens at once, list their buttons under `entity_id`; don't target an area or a device, because that presses every other button there too.
- **The Claude skill knows them.** Settings → Claude explains both buttons with an example automation. Install the skill again to get it.
- Needs firmware 0.2.45: press **Update** on the screen. Screens on older firmware keep working, without the two buttons.
## 0.2.52 (firmware 0.2.44)
Change a screen's settings on the screen itself.
+4
View File
@@ -30,3 +30,7 @@ missing attributes or an offline screen. New cards require firmware 0.2.0.
The screen carries the same settings itself (firmware 0.2.44 or newer): hold the top bar for
about a second and a half, or put the built-in **Settings** card on a page. What is changed
there appears here within a second, and the other way around.
In Home Assistant every screen also has a **Wake** and a **Sleep** button (firmware 0.2.45 or
newer). Press them from an automation with `button.press`: Wake does what a tap does, Sleep puts
the screen in standby right away, also with Auto standby off.
+39 -4
View File
@@ -15,12 +15,12 @@ import tile_icons
from core import (ALERT_ENDINGS, ALERT_EVENT, ALERT_FALLBACK_ICON, ALERT_FIELDS, ALERT_LIMITS, ALERT_MAX_TIMEOUT,
ALERT_MIN_FIRMWARE, ALERT_SUGGESTED_ICONS, AUTO_STANDBY_MIN_FIRMWARE, BROADCAST_DISMISS, BROADCAST_SHOW,
CONTROLS, DISPLAYS, MAX_PAGES, SETTINGS_PAGE_MIN_FIRMWARE, SLOTS_PER_PAGE, TILE_BACKGROUNDS,
TILE_EVENTS, TILE_RESULT_EVENT)
TILE_EVENTS, TILE_RESULT_EVENT, WAKE_SLEEP_MIN_FIRMWARE)
NAME = 'esp-screens'
# claude.ai accepts at most 200 characters; Claude Code picks the skill by this sentence.
DESCRIPTION = ('ESP Screens (CYD and Guition touchscreens run from Home Assistant): put a tile on a screen, move or '
'order tiles, show an alert on one screen or all of them, and keep a screen awake.')
'order tiles, show an alert on one screen or all of them, and wake, sleep or keep a screen awake.')
TYPES = {'string': 'text', 'int': 'number', 'bool': 'on/off'}
def skill_dir(config=None):
@@ -58,7 +58,7 @@ description: {DESCRIPTION}
Made by ESP Screen Manager (ESP Screens → Settings → Claude). Installing it again from there replaces this file, so changes made here get lost.
Three things Home Assistant can do with the screens: choose what a screen shows ([Tiles](#tiles-on-a-screen)), show an alert (below), and keep a screen awake or let it dim ([Standby and brightness](#standby-and-brightness)).
Three things Home Assistant can do with the screens: choose what a screen shows ([Tiles](#tiles-on-a-screen)), show an alert (below), and wake a screen, put it to sleep or keep it awake ([Standby and brightness](#standby-and-brightness)).
An alert is a card over the whole screen with an icon, a title, a subtitle and one button. It wakes the screen and stays until someone presses the button or the timeout runs out. A new alert replaces the one showing.
@@ -256,13 +256,48 @@ Every screen has these entities in Home Assistant, on its ESPHome device. `<scre
| Entity | What it does |
|---|---|
| `button.<screen>_wake` | Wakes the screen the way a tap does and counts the standby time from that moment; a screen that is already on only restarts that count. Firmware {WAKE_SLEEP_MIN_FIRMWARE} or newer. |
| `button.<screen>_sleep` | Puts the screen in standby right away, also with Auto standby off, and closes an alert that is showing. It stays in standby until someone taps it, Wake is pressed or an alert arrives. Firmware {WAKE_SLEEP_MIN_FIRMWARE} or newer. |
| `switch.<screen>_auto_standby` | On: the screen dims after the standby time without a touch. Off: the screen wakes up and stays on. Firmware {AUTO_STANDBY_MIN_FIRMWARE} or newer. |
| `number.<screen>_standby_after` | Seconds without a touch before standby, 60 to 86400. |
| `number.<screen>_normal_brightness` | Brightness while in use, 5 to 100 %. |
| `number.<screen>_standby_brightness` | Brightness in standby, 0 to 100 %, at most the normal brightness. |
| `number.<screen>_night_brightness` | Brightness in standby during the night hours set in ESP Screens, 0 to 100 %. |
These are the same settings as in ESP Screens: a change made from Home Assistant shows there too and stays after a restart. Turning Auto standby on again counts the standby time from that moment. Every change is saved on the screen, so switch on changes that happen a few times a day (someone comes home, a light goes on, a window opens), never on every motion.
Wake and Sleep are buttons: press them with the `button.press` action. They save nothing, so an automation may press them as often as it likes, on every motion too. To reach several screens at once, list their buttons under `entity_id`. Don't target an area or a device with `button.press`: that presses every other button there as well, the Wake and Sleep of the same screen included.
```yaml
alias: Screens wake on motion and sleep when the TV goes on
triggers:
- trigger: state
entity_id: binary_sensor.hallway_motion
to: "on"
id: motion
- trigger: state
entity_id: media_player.living_room_tv
to: "on"
id: tv
actions:
- choose:
- conditions:
- condition: trigger
id: motion
sequence:
- action: button.press
target:
entity_id: button.hallway_screen_wake
- conditions:
- condition: trigger
id: tv
sequence:
- action: button.press
target:
entity_id:
- button.living_room_screen_sleep
- button.kitchen_screen_sleep
```
The switch and the numbers are the same settings as in ESP Screens: a change made from Home Assistant shows there too and stays after a restart. Turning Auto standby on again counts the standby time from that moment. Every change is saved on the screen, so switch on changes that happen a few times a day (someone comes home, a light goes on, a window opens), never on every motion: press Wake for that.
To keep screens on while something is going on at home, turn Auto standby off when the situation starts and on when it ends, all from one automation. Example with placeholders for the user's own entities:
+3 -1
View File
@@ -17,11 +17,13 @@ WEEKDAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
REPO = 'https://github.com/MaxGramser/homeassistant_espscreen'
REFS = {'cyd': 'main', 'guition': 'main'}
# Firmware shipped with this app release; screens below it get an update offer.
FIRMWARE_VERSION = '0.2.44'
FIRMWARE_VERSION = '0.2.45'
# The Auto standby switch a screen offers Home Assistant automations.
AUTO_STANDBY_MIN_FIRMWARE = '0.2.41'
# The settings page the screen opens itself, and the screen.settings tile that opens it.
SETTINGS_PAGE_MIN_FIRMWARE = '0.2.44'
# The Wake and Sleep buttons a screen offers Home Assistant automations.
WAKE_SLEEP_MIN_FIRMWARE = '0.2.45'
ATTRS = frozenset('brightness percentage current_position current_temperature temperature current_humidity min_temp max_temp target_temp_step supported_color_modes hvac_modes hvac_action hs_color color_temp_kelvin min_color_temp_kelvin max_color_temp_kelvin fan_speed_list unit_of_measurement battery_level fan_speed volume_level is_volume_muted media_title options min max step temperature_unit supported_features device_class next_rising next_setting finishes_at duration remaining humidity wind_speed wind_speed_unit apparent_temperature fan_modes swing_modes fan_mode swing_mode'.split())
# Attributes whose boolean value the screen needs; every other bool stays behind.
BOOL_ATTRS = frozenset(['is_volume_muted'])
+1 -1
View File
@@ -1,5 +1,5 @@
name: ESP Screen Manager
version: "0.2.52"
version: "0.2.53"
slug: esp_screen_manager
description: Choose tiles for your CYD and Guition touchscreen, without reflashing.
url: https://github.com/MaxGramser/homeassistant_espscreen
+22 -3
View File
@@ -12,7 +12,8 @@ sys.path.insert(0, str(ROOT / 'screen_manager/app'))
import claude_skill # noqa: E402
import tile_icons # noqa: E402
from core import (ALERT_EVENT, ALERT_FIELDS, ALERT_LIMITS, ALERT_MIN_FIRMWARE, AUTO_STANDBY_MIN_FIRMWARE, BROADCAST_DISMISS, # noqa: E402
BROADCAST_SHOW, CONTROLS, DISPLAYS, TILE_BACKGROUNDS, TILE_EVENTS, TILE_RESULT_EVENT)
BROADCAST_SHOW, CONTROLS, DISPLAYS, TILE_BACKGROUNDS, TILE_EVENTS, TILE_RESULT_EVENT,
WAKE_SLEEP_MIN_FIRMWARE)
HAS_YAML = importlib.util.find_spec('yaml') is not None
HAS_AIOHTTP = importlib.util.find_spec('aiohttp') is not None
@@ -51,6 +52,11 @@ class SkillText(unittest.TestCase):
'number.<screen>_standby_brightness', 'number.<screen>_night_brightness'):
self.assertIn(f'`{entity}`', standby)
self.assertIn(AUTO_STANDBY_MIN_FIRMWARE, standby)
# Wake and Sleep (0.2.53): the buttons, when they arrived, and how to press several at once.
for entity in ('button.<screen>_wake', 'button.<screen>_sleep'):
self.assertIn(f'`{entity}`', standby)
self.assertIn(f'Firmware {WAKE_SLEEP_MIN_FIRMWARE} or newer', standby)
self.assertIn('`button.press`', standby)
self.assertIn('(#standby-and-brightness)', text, 'the intro links to the section')
# Tiles (0.2.51): the events, what a tile can do per domain, how to read a screen and the rule to ask first.
tiles = text.split('## Tiles on a screen', 1)[1].split('## All screens', 1)[0]
@@ -72,7 +78,7 @@ class SkillText(unittest.TestCase):
def test_every_yaml_example_parses(self):
import yaml
blocks = re.findall(r'```yaml\n(.*?)```', claude_skill.text(), re.S)
self.assertEqual(len(blocks), 8)
self.assertEqual(len(blocks), 9)
parsed = [yaml.safe_load(block) for block in blocks]
# Tiles first (0.2.51): putting one on a screen, and ordering a page.
self.assertEqual(parsed[0]['actions'][0]['event'], 'esp_screens_add_tile')
@@ -85,7 +91,20 @@ class SkillText(unittest.TestCase):
self.assertEqual(parsed[4][0]['wait_for_trigger'][0]['event_type'], ALERT_EVENT)
choose = parsed[5]['actions'][0]['choose']
self.assertEqual([option['sequence'][0]['event'] for option in choose], [BROADCAST_SHOW, BROADCAST_DISMISS])
awake = parsed[6]
# Wake on motion for one screen, Sleep for two, each branch on its own trigger.
buttons = parsed[6]
options = buttons['actions'][0]['choose']
self.assertEqual({trigger['id'] for trigger in buttons['triggers']}, {option['conditions'][0]['id'] for option in options})
pressed = []
for option in options:
step = option['sequence'][0]
self.assertEqual(step['action'], 'button.press')
targets = step['target']['entity_id']
pressed.append([targets] if isinstance(targets, str) else targets)
self.assertTrue(all(entity.startswith('button.') and entity.endswith('_wake') for entity in pressed[0]))
self.assertTrue(all(entity.startswith('button.') and entity.endswith('_sleep') for entity in pressed[1]))
self.assertGreater(len(pressed[1]), 1, 'shows how to reach several screens at once')
awake = parsed[7]
self.assertEqual(awake['mode'], 'restart')
branch = awake['actions'][0]
self.assertEqual((branch['then'][0]['action'], branch['else'][0]['action']), ('switch.turn_off', 'switch.turn_on'))
+2 -1
View File
@@ -91,7 +91,8 @@ class Firmware(unittest.TestCase):
self.assertIn('- action: open_settings', text, name)
self.assertIn('- id: go_home', text, name)
self.assertIn('runtime_tiles::auto_home', text, name)
self.assertIn('SCREEN_FIRMWARE_VERSION: "0.2.44"', text, name)
version = re.search(r'SCREEN_FIRMWARE_VERSION: "([0-9.]+)"', text)[1]
self.assertGreaterEqual(tuple(map(int, version.split('.'))), (0, 2, 44), name)
def test_the_hold_strip_covers_the_top_bar_and_no_more(self):
for name, text in self.profiles.items():
+96
View File
@@ -0,0 +1,96 @@
"""Wake and Sleep buttons for Home Assistant automations (app 0.2.53, firmware 0.2.45).
Both board profiles have to wire them the same way: Wake is a tap, Sleep is the standby time running out
right away, and a Sleep holds with Auto standby off until a tap, Wake or an alert ends it.
"""
import re
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'screen_manager/app'))
from core import FIRMWARE_VERSION, WAKE_SLEEP_MIN_FIRMWARE # noqa: E402
PROFILES = ('home-like-2432s028.yaml', 'guition-4848s040.yaml', 'packages/cyd.yaml', 'packages/guition.yaml')
def section(text, top):
"""One top-level YAML section of a profile, up to the next one."""
match = re.search(rf'^{top}:\n(.*?)(?=^[a-z_]+:|\Z)', text, re.M | re.S)
return match[1] if match else ''
def item(text, key, value):
"""The list item in a section that carries `key: value`, up to the next item at the same depth."""
for chunk in re.split(r'^ - ', text, flags=re.M)[1:]:
if re.search(rf'^\s*{key}: "?{re.escape(value)}"?$', chunk, re.M):
return chunk
return ''
def script(text, name):
return item(section(text, 'script'), 'id', name)
class Profiles(unittest.TestCase):
def setUp(self):
self.profiles = {name: (ROOT / name).read_text() for name in PROFILES}
def test_the_firmware_that_carries_the_buttons_is_this_release_or_older(self):
version = lambda text: tuple(int(part) for part in text.split('.'))
self.assertLessEqual(version(WAKE_SLEEP_MIN_FIRMWARE), version(FIRMWARE_VERSION))
def test_both_buttons_are_plain_controls_home_assistant_sees(self):
for name, text in self.profiles.items():
buttons = section(text, 'button')
for label in ('Wake', 'Sleep'):
button = item(buttons, 'name', label)
self.assertTrue(button, f'{name}: no {label} button')
self.assertIn('platform: template', button, name)
self.assertNotIn('entity_category', button, f'{name}: {label} is a control, not a setting')
self.assertNotIn('internal', button, f'{name}: {label} must reach Home Assistant')
def test_wake_is_a_tap(self):
for name, text in self.profiles.items():
wake = item(section(text, 'button'), 'name', 'Wake')
# A dimmed screen wakes the way the tap on its wake overlay does; one that is on only counts again.
self.assertRegex(wake, r"return id\(display_dimmed\);'\s+then:\s+- script\.execute: wake_display\s+else:\s+"
r"- lambda: 'id\(last_touch_ms\) = millis\(\);'", name)
self.assertIn('id(sleep_requested) = false;', script(text, 'wake_display'), f'{name}: waking ends a Sleep')
overlay = re.search(r'id: dim_wake_overlay\n.*?on_click:\s+- script\.execute: (\w+)', text, re.S)
self.assertEqual(overlay[1], 'wake_display', f'{name}: a tap and Wake share one path')
def test_sleep_dims_now_and_holds_with_auto_standby_off(self):
for name, text in self.profiles.items():
sleep = item(section(text, 'button'), 'name', 'Sleep')
# Not during a touch calibration (said in the log); otherwise the flag, a closed alert, then the dim.
self.assertRegex(sleep, r"return id\(calibration_active\);'\s+then:\s+- lambda: 'ESP_LOGI\(\"standby\", \"Sleep from Home Assistant ignored", name)
steps = re.findall(r'script\.execute:(?:\s+id:)?\s+(\w+)|(id\(sleep_requested\) = true;)', sleep.split('else:', 1)[1])
self.assertEqual([step[0] or 'flag' for step in steps], ['flag', 'alert_dismiss', 'dim_display'], name)
self.assertIn('reason: "remote"', sleep, f'{name}: HA hears the alert was closed from Home Assistant')
dim = script(text, 'dim_display')
self.assertIn("lambda: 'return screen_settings::current.standby_enabled || id(sleep_requested);'", dim, name)
apply = script(text, 'apply_screen_settings')
self.assertIn('if (id(display_dimmed) && !settings.standby_enabled && !id(sleep_requested)) {', apply,
f'{name}: the minute tick must not undo a Sleep')
self.assertEqual(len(re.findall(r'id\(sleep_requested\) = true;', text)), 1, f'{name}: only the button starts a Sleep')
def test_the_flag_starts_false_and_is_never_saved(self):
for name, text in self.profiles.items():
flag = item(section(text, 'globals'), 'id', 'sleep_requested')
self.assertIn('type: bool', flag, name)
self.assertIn('restore_value: no', flag, name)
self.assertIn("initial_value: 'false'", flag, name)
def test_switching_auto_standby_off_still_wakes_the_screen(self):
for name, text in self.profiles.items():
switch = item(section(text, 'switch'), 'name', 'Auto standby')
off = switch.split('turn_off_action:', 1)[1]
# Only a real change wakes: a switch that was already off returns before the flag is touched.
self.assertRegex(off, r'if \(!s\.standby_enabled\) return;\s+s\.standby_enabled = false;\s+(//[^\n]*\n\s+)?'
r'id\(sleep_requested\) = false;', name)
if __name__ == '__main__':
unittest.main()