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>
This commit is contained in:
Max Gramser
2026-09-18 22:57:14 +02:00
parent e7e173d3b7
commit d76461d774
45 changed files with 6636 additions and 19954 deletions
+26 -23
View File
@@ -21,6 +21,8 @@ from pathlib import Path
from unittest.mock import patch
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'))
sys.path.insert(0, str(ROOT / 'tests'))
from core import (FIRMWARE_VERSION, SETTING_ENTITIES, SETTING_ENTITIES_MIN_FIRMWARE, SETTING_RULES, forecast_kinds, # noqa: E402
@@ -33,8 +35,8 @@ if HAS_AIOHTTP:
from aiohttp.test_utils import TestClient, TestServer
from server import ANSWER_RETRY_SECONDS, HomeAssistant, Manager, Refused, create_app
PROFILES = {'cyd': ROOT / 'home-like-2432s028.yaml', 'guition': ROOT / 'guition-4848s040.yaml'}
PACKAGES = {'cyd': ROOT / 'packages/cyd.yaml', 'guition': ROOT / 'packages/guition.yaml'}
PROFILES = {'cyd': 'home-like-2432s028.yaml', 'guition': 'guition-4848s040.yaml'}
PACKAGES = {'cyd': 'packages/cyd.yaml', 'guition': 'packages/guition.yaml'}
RUNTIME = (ROOT / 'components/smart_display/runtime_tiles.h').read_text()
SCREEN_PAGE = (ROOT / 'components/smart_display/settings_screen.h').read_text()
STATIC = ROOT / 'screen_manager/app/static'
@@ -114,18 +116,18 @@ class CoreSettings(unittest.TestCase):
def test_the_entities_are_named_as_in_both_board_profiles(self):
blocks = {'switch': 'switch', 'number': 'number', 'time': 'datetime', 'select': 'select'}
for board, path in [*PROFILES.items(), *PACKAGES.items()]:
text = path.read_text()
text = profiles.text(path)
for key, (domain, name) in SETTING_ENTITIES.items():
block = top_block(text, blocks[domain])
if key == 'rotation' and board == 'cyd':
self.assertNotIn(f'name: "{name}"', block, f'{path.name}: the CYD cannot turn')
self.assertNotIn(f'name: "{name}"', block, f'{path}: the CYD cannot turn')
continue
self.assertIn(f'name: "{name}"', block, f'{path.name}: {key}')
self.assertIn(f'name: "{name}"', block, f'{path}: {key}')
at = block.index(f'name: "{name}"')
entry = block[max(0, at - 200):at + 900]
self.assertIn('entity_category: config', entry, f'{path.name}: {key}')
self.assertRegex(entry, rf'settings_screen::set\("{key}"', f'{path.name}: {key} changes through set()')
self.assertIn('type: time', top_block(text, 'datetime'), path.name)
self.assertIn('entity_category: config', entry, f'{path}: {key}')
self.assertRegex(entry, rf'settings_screen::set\("{key}"', f'{path}: {key} changes through set()')
self.assertIn('type: time', top_block(text, 'datetime'), path)
def test_a_screen_owns_its_settings_only_with_firmware_0_2_49_entities(self):
old = [{'entity_id': ENTITY_IDS['brightness'], 'platform': 'esphome', 'original_name': 'Normal brightness'},
@@ -590,15 +592,15 @@ class Editor(unittest.TestCase):
class Firmware(unittest.TestCase):
def test_the_message_action_answers_a_caller_that_asks(self):
for path in [*PROFILES.values(), *PACKAGES.values()]:
text = path.read_text()
text = profiles.text(path)
action = text[text.index('- action: screen_message'):]
# Up to the next key of the api block or the next top-level block (the packages carry no keys).
action = action[:re.search(r'\n(?: [a-z_]+:|[a-z_]+:)', action).start()]
self.assertIn('supports_response: optional', action, path.name)
self.assertIn("lambda: 'return call_id != 0;'", action, f'{path.name}: no answer, no warning, for a caller without a call id')
self.assertIn('api.respond:', action, path.name)
self.assertIn('root["status"] = id(dashboard_inbox).state;', action, path.name)
self.assertIn('root["rev"] = runtime_tiles::layout_rev;', action, path.name)
self.assertIn('supports_response: optional', action, path)
self.assertIn("lambda: 'return call_id != 0;'", action, f'{path}: no answer, no warning, for a caller without a call id')
self.assertIn('api.respond:', action, path)
self.assertIn('root["status"] = id(dashboard_inbox).state;', action, path)
self.assertIn('root["rev"] = runtime_tiles::layout_rev;', action, path)
def test_the_settings_tile_opens_without_home_assistant(self):
event = RUNTIME[RUNTIME.index('inline void event(lv_event_t *event) {'):]
@@ -609,23 +611,24 @@ class Firmware(unittest.TestCase):
def test_home_assistant_sees_every_setting_change(self):
for board, path in PROFILES.items():
script = path.read_text()
script = script[script.index('- id: apply_screen_settings'):]
text = profiles.text(path)
script = text[text.index('- id: apply_screen_settings'):]
script = script[:script.index('\n - id: ', 10)]
for entity in ('setting_brightness', 'setting_standby_brightness', 'setting_night_brightness', 'setting_standby_seconds',
'setting_auto_home_seconds'):
# One loop publishes every number that lacks a state or changed (firmware 0.2.50, see test_cover_card).
self.assertIn(f'std::make_pair(id({entity}),', script, f'{path.name}: {entity}')
self.assertIn('if (!entity->has_state() || entity->state != value) entity->publish_state(value);', script, path.name)
self.assertIn('std::make_pair(id(setting_night_start), settings.night_start)', script, path.name)
self.assertIn('settings_screen::refresh();', script, f'{path.name}: an open settings page follows Home Assistant')
self.assertEqual('id(setting_rotation)->update();' in script, board == 'guition', path.name)
self.assertIn(f'std::make_pair(id({entity}),', script, f'{path}: {entity}')
self.assertIn('if (!entity->has_state() || entity->state != value) entity->publish_state(value);', script, path)
self.assertIn('std::make_pair(id(setting_night_start), settings.night_start)', script, path)
self.assertIn('settings_screen::refresh();', script, f'{path}: an open settings page follows Home Assistant')
# The Guition's board file extends the script with the rotation (docs/PROFILES.md).
self.assertEqual('id(setting_rotation)->update();' in text, board == 'guition', path)
def test_the_page_and_every_entity_change_settings_the_same_way(self):
self.assertRegex(SCREEN_PAGE, r'enum class SetResult : uint8_t \{ unknown, same, changed \};')
for path in PROFILES.values():
text = path.read_text()
self.assertNotIn('settings_preference.save(&s)', text, f'{path.name}: saving goes through settings_screen::set()')
text = profiles.text(path)
self.assertNotIn('settings_preference.save(&s)', text, f'{path}: saving goes through settings_screen::set()')
self.assertNotIn('update_interval: never\n lambda: return settings_screen::swipe_pages', text, 'a template switch has no update_interval')