Let Wake only light the screen, and open ESP Screens without a chosen screen (0.2.65)
Wake from Home Assistant counted as a tap: it reset last_touch_ms, the one clock that standby and Back to page 1 both read. An automation pressing Wake on every motion therefore held an open card or page 2 up for as long as someone moved in the room. Back to page 1 now has its own clock, last_use_ms, written only by the boot, the touchscreen triggers, open_settings from Home Assistant and the UI self test. Wake, an alert ending and Auto standby on still restart the standby time, and Wake still brings back the brightness and the second hand. The rule moved from the interval into the script back_to_page_1_when_due. Wake runs it right after waking a dimmed screen, so a page whose time ran out during standby lights up on page 1 instead of jumping there a second later. ESP Screens no longer opens the first screen by itself. Until one is picked the page says "Choose a screen", and with no screens it still offers the install; both cards start hidden so neither flashes while the page loads. Checked with 48 host API checks per board, both ESP32 builds, 314 Python and 16 C++ tests, and the editor on the demo home. Firmware 0.2.56. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
"""Opening ESP Screens while the add-on is busy (app 0.2.58).
|
||||
"""Opening ESP Screens.
|
||||
|
||||
The page asks for the full inventory and opens the live stream at the same time. An add-on that is building
|
||||
firmware answers the stream first, whose light payload has screens but no entities, so the first screen was
|
||||
opened with an empty catalogue: the entity list said "No entities found" and the tiles showed entity ids until
|
||||
a filter chip was pressed. The first full inventory now draws those parts again.
|
||||
App 0.2.58: the page asks for the full inventory and opens the live stream at the same time. An add-on that is
|
||||
building firmware answers the stream first, whose light payload has screens but no entities, so a screen opened
|
||||
then had an empty catalogue: the entity list said "No entities found" and the tiles showed entity ids until a
|
||||
filter chip was pressed. The first full inventory now draws those parts again.
|
||||
|
||||
App 0.2.65: the page no longer opens the first screen by itself; the owner picks one. Until then a card asks for
|
||||
that, or offers the install when there are no screens yet. A screen picked before the full inventory arrives
|
||||
still gets its entity names from the redraw above.
|
||||
"""
|
||||
import re
|
||||
import unittest
|
||||
@@ -11,6 +15,7 @@ from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = (ROOT / 'screen_manager/app/static/app.js').read_text()
|
||||
PAGE = (ROOT / 'screen_manager/app/static/index.html').read_text()
|
||||
|
||||
|
||||
def function(name):
|
||||
@@ -19,10 +24,28 @@ def function(name):
|
||||
|
||||
|
||||
class Startup(unittest.TestCase):
|
||||
def test_the_live_stream_can_open_a_screen_before_the_catalogue(self):
|
||||
# The race exists: the light payload selects the first screen, and it carries no entities.
|
||||
self.assertIn('if (!selected && inventory.screens.length) select(inventory.screens[0].id);', function('applyLive'))
|
||||
self.assertRegex(function('select'), r'renderTopbar\(\);\s+renderTiles\(\);\s+renderResults\(\);')
|
||||
def test_no_screen_opens_by_itself(self):
|
||||
self.assertNotIn('inventory.screens[0]', SCRIPT)
|
||||
# The one way into a screen is its button in the list.
|
||||
self.assertEqual(re.findall(r'(?<![\w.])(?<!function )select\(([^)]*)\)', SCRIPT), ['screen.id'])
|
||||
for name in ('refresh', 'applyLive'):
|
||||
self.assertNotRegex(function(name), r'(?<![\w.])select\(', name)
|
||||
|
||||
def test_the_right_side_asks_for_a_screen_until_one_is_chosen(self):
|
||||
choose = re.search(r'<section id="choose" class="empty" hidden>(.*?)</section>', PAGE, re.S)
|
||||
self.assertTrue(choose, 'hidden until the first inventory')
|
||||
self.assertIn('<h2>Choose a screen</h2>', choose[1])
|
||||
self.assertNotIn('<button', choose[1], 'the list beside it is the choice')
|
||||
self.assertIn('<section id="empty" class="empty" hidden>', PAGE, 'no install card flashing while the page loads')
|
||||
self.assertIn('<section id="editor" hidden>', PAGE)
|
||||
body = function('renderScreens')
|
||||
self.assertIn('if (!selected) {\n $("#choose").hidden = !inventory.screens.length;\n'
|
||||
' $("#empty").hidden = !!inventory.screens.length;\n }', body)
|
||||
self.assertLess(body.index('$("#choose").hidden'), body.index('if ($("#screens .screen-host"))'),
|
||||
'also while an address form keeps the list')
|
||||
opened = function('select')
|
||||
for part in ('$("#editor").hidden = false;', '$("#empty").hidden = true;', '$("#choose").hidden = true;'):
|
||||
self.assertIn(part, opened)
|
||||
|
||||
def test_the_first_full_inventory_draws_what_needs_entities_again(self):
|
||||
body = function('refresh')
|
||||
@@ -31,7 +54,7 @@ class Startup(unittest.TestCase):
|
||||
redraw = re.search(r'if \(firstCatalogue && selected && layout && !drag\.active && !chipDrag\.active\) '
|
||||
r'\{ renderTopbar\(\); renderTiles\(\); renderResults\(\); \}', body)
|
||||
self.assertTrue(redraw, 'tiles, top bar and entity list are drawn again, never in the middle of a drag')
|
||||
self.assertLess(redraw.start(), body.index('if (!selected && inventory.screens.length) select('))
|
||||
self.assertRegex(function('select'), r'renderTopbar\(\);\s+renderTiles\(\);\s+renderResults\(\);')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""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 is a tap, 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.
|
||||
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
|
||||
@@ -52,16 +54,49 @@ class Profiles(unittest.TestCase):
|
||||
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):
|
||||
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 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+"
|
||||
# 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 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)), 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')
|
||||
|
||||
Reference in New Issue
Block a user