Files
homeassistant_espscreen/tests/test_wake_sleep.py
T
Max Gramser d76461d774 Build every screen from one shared core plus a small file per board, the same firmware as before (0.2.84)
The two board profiles (5,000 lines each, 87 % the same text) become packages/core.yaml and one file per
board under packages/boards/: the hardware, a table of 84 sizes and font sizes, 18 hooks with the lines of C++
only that board adds inside a shared lambda, and the board's own sections. packages/cyd.yaml and
packages/guition.yaml keep their names and places (a screen installed from ESP Screens builds as before);
home-like-2432s028.yaml and guition-4848s040.yaml become the same entries for a checkout.

The firmware stays 0.2.70: the C++ ESPHome generates for each board, through the checkout profile and
through the GitHub package route, was compared line by line with the old profiles' (docs/TEST_RESULTS_0284.md).
tools/check_packages.py replaces tools/generate_packages.py; tools/profiles.py gives the tests and tools the
three files as one; docs/PROFILES.md says how the files fit together and how a board is added.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 22:57:14 +02:00

169 lines
11 KiB
Python

"""Wake and Sleep buttons for Home Assistant automations (app 0.2.53, firmware 0.2.45) and the standby
code around them (tidied in app 0.2.54, firmware 0.2.46).
Both board profiles have to wire them the same way: Wake lights the screen and restarts the standby time,
Sleep is the standby time running out right away, and a Sleep holds until a tap, Wake or an alert ends it,
whatever turns Auto standby off. Since app 0.2.65 (firmware 0.2.56) Wake is no tap: Back to page 1 counts
from the last touch, so an automation pressing Wake on every motion no longer holds a card or page 2 up.
"""
import re
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'tools'))
import profiles # noqa: E402
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."""
# The core and the board file may both carry the section (app 0.2.84+).
return '\n'.join(re.findall(rf'^{top}:\n(.*?)(?=^[a-z_]+:|\Z)', text, re.M | re.S))
def item(text, key, value):
"""The list item in a section that carries `key: value`, up to the next item at the same depth."""
chunks = re.split(r'^ - ', text, flags=re.M)[1:]
# The item that starts with `key: value` first: a script that names another one (`id: alert_dismiss` in
# alert_timeout) may come before that one's own block now that the board file adds scripts of its own.
for chunk in chunks:
if re.match(rf'{key}: "?{re.escape(value)}"?$', chunk, re.M):
return chunk
for chunk in chunks:
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: profiles.text(name) 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_light_not_a_tap(self):
for name, text in self.profiles.items():
wake = item(section(text, 'button'), 'name', 'Wake')
# A dimmed screen lights up the way the tap on its wake overlay does, then goes to page 1 at once when
# that time ran out during standby; one that is on only restarts the standby time.
self.assertRegex(wake, r"return id\(display_dimmed\);'\s+then:\s+- script\.execute: wake_display\s+"
r"(?:#[^\n]*\s+)?- script\.execute: back_to_page_1_when_due\s+else:\s+"
r"- lambda: 'id\(last_touch_ms\) = millis\(\);'", name)
self.assertNotIn('last_use_ms', wake, f'{name}: Wake must not postpone Back to page 1')
self.assertNotIn('last_use_ms', script(text, 'wake_display'), f'{name}: waking is no touch')
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_back_to_page_1_counts_from_the_last_touch(self):
for name, text in self.profiles.items():
clock = item(section(text, 'globals'), 'id', 'last_use_ms')
self.assertIn('type: uint32_t', clock, name)
self.assertIn('restore_value: no', clock, name)
check = script(text, 'back_to_page_1_when_due')
self.assertIn('if (!runtime_tiles::auto_home || id(touch_down) || id(calibration_active) || id(alert_active)) return false;', check, name)
self.assertIn('if (id(display_dimmed)) return false;', check, f'{name}: standby keeps its page ("Also on standby")')
self.assertRegex(check, r'return away && \(millis\(\) - id\(last_use_ms\)\) > \(uint32_t\) runtime_tiles::auto_home_seconds \* 1000UL;'
r'\s+then:\s+- script\.execute: go_home', name)
self.assertNotIn('last_touch_ms', check, f'{name}: the standby clock is not the page clock')
# The one-second interval and Wake run it; nothing else decides about page 1 by itself.
interval = section(text, 'interval')
self.assertIn('- script.execute: back_to_page_1_when_due', interval, name)
self.assertNotIn('auto_home', interval, f'{name}: one copy of the rule')
self.assertEqual(len(re.findall(r'script\.execute: back_to_page_1_when_due', text)), 2, name)
# Written by the boot, the three touchscreen triggers, Home Assistant opening the settings page (and, on the
# Guition, a camera) and the UI self test only; Wake, an alert ending and Auto standby restart the standby time alone.
self.assertEqual(len(re.findall(r'id\(last_use_ms\) = millis\(\);', text)), 7 if 'preview_camera' in text else 6, name)
touch = section(text, 'touchscreen')
for trigger in ('on_touch', 'on_update', 'on_release'):
body = re.search(rf'^ {trigger}:\n(.*?)(?=^ [a-z_]+:|\Z)', touch, re.M | re.S)[1]
self.assertIn('id(last_use_ms) = millis();', body, f'{name}: {trigger}')
self.assertIn('id(last_use_ms) = millis();', re.search(r'- action: open_settings\n(.*?)- action:', text, re.S)[1], name)
self.assertIn('id(last_use_ms) = millis();', script(text, 'ui_self_test'), name)
for quiet in (script(text, 'alert_dismiss'), item(section(text, 'switch'), 'name', 'Auto standby')):
self.assertIn('id(last_touch_ms) = millis();', quiet, name)
self.assertNotIn('last_use_ms', quiet, name)
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')
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_standby_has_one_way_in(self):
for name, text in self.profiles.items():
# dim_display decides nothing itself: the interval asks for Auto standby, the Sleep button sets the flag.
dim = script(text, 'dim_display')
self.assertRegex(dim, r"then:\s+- lambda: \|-\s+id\(display_dimmed\) = true;\s+id\(apply_screen_settings\)\.execute\(\);\s+- if:", name)
self.assertRegex(dim, r"return screen_settings::current\.home_on_standby;'\s+then:\s+- script\.execute: go_home\s+else:\s+"
r"- lambda: 'settings_screen::close\(\);'\s+- script\.execute: close_cards", name)
self.assertNotIn('lvgl.widget.hide', dim, f'{name}: cards close through close_cards')
callers = re.findall(r'script\.execute: dim_display', text)
self.assertEqual(len(callers), 2, f'{name}: the standby interval and the Sleep button')
interval = re.search(r'if \(!screen_settings::current\.standby_enabled\) return false;\s+'
r'if \(id\(display_dimmed\) \|\| id\(touch_down\) \|\| id\(calibration_active\) \|\| id\(alert_active\)\) return false;\s+'
r'return \(millis\(\) - id\(last_touch_ms\)\) > [^\n]+\n\s+then:\s+- script\.execute: dim_display', text)
self.assertTrue(interval, f'{name}: the standby time only dims with Auto standby on')
def test_every_card_closes_through_dismiss(self):
for name, text in self.profiles.items():
body = [line.strip() for line in script(text, 'close_cards').split('then:', 1)[1].splitlines()
if line.strip() and not line.strip().startswith('#')]
self.assertEqual(body, ["- lambda: 'if (runtime_tiles::dismiss) runtime_tiles::dismiss();'"], name)
dismiss = re.search(r'runtime_tiles::dismiss = \[\]\(\) \{(.*?)\};', text, re.S)[1]
for step in ('runtime_tiles::hide_detail();', 'id(active_entity).clear();', 'id(brightness_overlay)',
'id(color_detail_overlay)', 'id(climate_detail_overlay)', 'id(climate_mode_overlay)'):
self.assertIn(step, dismiss, name)
def test_the_backlight_belongs_to_the_firmware(self):
for name, text in self.profiles.items():
light = item(section(text, 'light'), 'id', 'back_light')
self.assertIn('platform: monochromatic', light, name)
self.assertIn('internal: true', light, f'{name}: Home Assistant would fight standby over it')
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_only_a_tap_wake_or_an_alert_end_a_sleep(self):
for name, text in self.profiles.items():
# wake_display is the one place that clears the flag; Auto standby, from any side, leaves it alone.
self.assertEqual(len(re.findall(r'id\(sleep_requested\) = false;', text)), 1, name)
self.assertIn('id(sleep_requested) = false;', script(text, 'wake_display'), name)
self.assertNotIn('sleep_requested', item(section(text, 'switch'), 'name', 'Auto standby'), name)
if __name__ == '__main__':
unittest.main()