da20782d3d
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>
114 lines
6.2 KiB
Python
114 lines
6.2 KiB
Python
"""The settings page on the screen itself (app 0.2.52, firmware 0.2.44).
|
|
|
|
Two halves have to agree without ever seeing each other: the row table in settings_screen.h writes keys
|
|
that only mean something if ESP Screens knows them, and the eleven-key `settings` block older firmware
|
|
insists on may never grow. Both are checked here, against the real files.
|
|
"""
|
|
import importlib.util
|
|
import json
|
|
import re
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / 'screen_manager/app'))
|
|
import test_portal
|
|
from core import (BUILTIN, SETTING_RULES, SETTINGS_BESIDE_BLOCK, entity_id, min_firmware, validate_layout,
|
|
validate_settings)
|
|
|
|
SCREEN = (ROOT / 'components/smart_display/settings_screen.h').read_text()
|
|
RUNTIME = (ROOT / 'components/smart_display/runtime_tiles.h').read_text()
|
|
# The keys the firmware's own block still carries; everything newer travels as its own key.
|
|
FROZEN = {'standby_enabled', 'standby_seconds', 'brightness', 'standby_brightness', 'night_enabled',
|
|
'night_start', 'night_end', 'night_brightness', 'show_clock', 'clock_24h', 'home_on_standby'}
|
|
assert FROZEN == set(SETTING_RULES) - set(SETTINGS_BESIDE_BLOCK)
|
|
|
|
|
|
class SettingsPage(unittest.TestCase):
|
|
def test_every_key_the_screen_reports_is_a_setting_the_app_knows(self):
|
|
keys = set(re.findall(r'changed\("(\w+)"', SCREEN))
|
|
self.assertTrue(keys, 'no rows found in settings_screen.h')
|
|
self.assertLessEqual(keys, set(SETTING_RULES), 'the screen writes a key ESP Screens would drop')
|
|
|
|
def test_the_frozen_settings_block_keeps_exactly_eleven_keys(self):
|
|
# Firmware before 0.2.44 refuses a `settings` object of any other size; that is why the newer
|
|
# settings travel as their own keys.
|
|
self.assertEqual(re.search(r'if \(obj\.size\(\) != (\d+)\) return false;', RUNTIME).group(1), '11')
|
|
self.assertEqual(FROZEN, set(SETTING_RULES) & FROZEN)
|
|
|
|
def test_new_settings_default_and_validate(self):
|
|
clean = validate_settings({})
|
|
self.assertEqual(clean['auto_home'], True)
|
|
self.assertEqual(clean['auto_home_seconds'], 120)
|
|
self.assertEqual(validate_settings({'auto_home_seconds': 30})['auto_home_seconds'], 30)
|
|
for bad in ({'auto_home_seconds': 29}, {'auto_home_seconds': 3601}, {'auto_home': 1},
|
|
{'auto_home_seconds': True}):
|
|
with self.assertRaises(ValueError):
|
|
validate_settings(bad)
|
|
|
|
def test_the_settings_tile_is_a_builtin_that_needs_new_firmware(self):
|
|
self.assertIn('screen.settings', BUILTIN)
|
|
self.assertTrue(entity_id('screen.settings'))
|
|
self.assertEqual(min_firmware({'tiles': [{'entity': 'screen.settings'}]}), (0, 2, 44))
|
|
self.assertLess(min_firmware({'tiles': [{'entity': 'screen.clock'}]}), (0, 2, 44))
|
|
layout = validate_layout({'title': 'Home', 'tiles': [{'entity': 'screen.settings', 'name': 'Settings'}]})
|
|
self.assertEqual(layout['tiles'][0]['entity'], 'screen.settings')
|
|
|
|
|
|
@unittest.skipUnless(importlib.util.find_spec('aiohttp'), 'Run using .venv-portal/bin/python for server tests')
|
|
class LayoutMessage(unittest.IsolatedAsyncioTestCase):
|
|
async def test_the_new_keys_ride_beside_the_frozen_block(self):
|
|
with tempfile.TemporaryDirectory() as temp:
|
|
manager = test_portal.ManagerTests().setup_manager(Path(temp) / 'screens.json')
|
|
layout = {'title': 'Home', 'tiles': [{'entity': 'light.a'}],
|
|
'settings': validate_settings({'auto_home': False, 'auto_home_seconds': 600})}
|
|
manager.save('text.screen', layout)
|
|
message = manager.layout_message('text.screen', manager.layouts['text.screen'], {})
|
|
self.assertEqual(set(message['settings']), FROZEN, 'older firmware refuses any other size')
|
|
self.assertEqual(message['auto_home'], False)
|
|
self.assertEqual(message['auto_home_seconds'], 600)
|
|
# A screen that never stored settings still gets the defaults, not a missing key.
|
|
plain = manager.layout_message('text.screen', {**manager.layouts['text.screen'],
|
|
'settings': validate_settings({})}, {})
|
|
self.assertEqual((plain['auto_home'], plain['auto_home_seconds']), (True, 120))
|
|
self.assertLess(len(json.dumps(message)), 4096, 'the screen refuses a message over 4 KB')
|
|
|
|
|
|
class Firmware(unittest.TestCase):
|
|
"""The board profiles have to carry the page, its way in, and the timer that closes it."""
|
|
|
|
def setUp(self):
|
|
self.profiles = {name: (ROOT / name).read_text()
|
|
for name in ('home-like-2432s028.yaml', 'guition-4848s040.yaml')}
|
|
|
|
def test_both_boards_wire_the_page_and_its_gesture(self):
|
|
for name, text in self.profiles.items():
|
|
self.assertIn('settings_screen::attach_hold', text, name)
|
|
self.assertIn('settings_screen::report', text, name)
|
|
self.assertIn('settings_screen::may_open', text, name)
|
|
self.assertIn('- action: open_settings', text, name)
|
|
self.assertIn('- id: go_home', text, name)
|
|
self.assertIn('runtime_tiles::auto_home', 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():
|
|
values = dict(re.findall(r'^ (\w+): "([^"]*)"', text, re.M))
|
|
width, scroll = int(values['DISPLAY_W']), int(values['SCROLL_Y'])
|
|
x = int(values['SETTINGS_HOLD_X'].replace('${EDGE_SWIPE_BAND_PX}', values.get('EDGE_SWIPE_BAND_PX', '0')))
|
|
hold = values['SETTINGS_HOLD_W'].replace('${DISPLAY_W}', values['DISPLAY_W'])
|
|
self.assertLessEqual(x + int(hold), width, name)
|
|
self.assertGreater(scroll, 20, name)
|
|
if 'EDGE_SWIPE_BAND_PX' in values:
|
|
# The page swipe owns both edge bands; the hold may not start there.
|
|
band = int(values['EDGE_SWIPE_BAND_PX'])
|
|
self.assertGreaterEqual(x, band, name)
|
|
self.assertLessEqual(x + int(hold), width - band, name)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|