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
+19 -17
View File
@@ -12,12 +12,14 @@ 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 DARK_MODE_MIN_FIRMWARE, FIRMWARE_VERSION, SETTING_ENTITIES, SETTING_RULES, SETTINGS_BESIDE_BLOCK # noqa: E402
COMPONENT = ROOT / 'components/smart_display'
THEME = (COMPONENT / 'theme.h').read_text()
PROFILES = [ROOT / 'home-like-2432s028.yaml', ROOT / 'guition-4848s040.yaml', ROOT / 'packages/cyd.yaml', ROOT / 'packages/guition.yaml']
PROFILES = list(profiles.NAMES)
HEX = re.compile(r'0x[0-9A-Fa-f]{6}\b')
# What may still carry a number that looks like a colour: the light's colour picker shows the colours a light can take,
# a mask and a codepoint check are no colours, and the value card's colour key is a wheel of fixed hues.
@@ -46,14 +48,14 @@ class OnePlace(unittest.TestCase):
def test_no_profile_writes_a_colour_of_its_own(self):
for path in PROFILES:
text = path.read_text()
text = profiles.text(path)
wheel = re.findall(r'^\s+arc_color: (0x[0-9A-F]{6})$', text, re.M)
self.assertEqual(set(wheel), COLOUR_WHEEL, path.name)
self.assertEqual(set(wheel), COLOUR_WHEEL, path)
everything = HEX.findall(text)
self.assertEqual(sorted(everything), sorted(wheel), f'{path.name}: only the colour wheel keeps fixed hues')
self.assertEqual(sorted(everything), sorted(wheel), f'{path}: only the colour wheel keeps fixed hues')
self.assertNotIn('BG_TOP_COLOR', text)
self.assertNotIn('ACCENT_AUTO', text)
self.assertNotRegex(text, r'^\s+(bg|text|border|bg_grad|line|outline|shadow)_color:', path.name)
self.assertNotRegex(text, r'^\s+(bg|text|border|bg_grad|line|outline|shadow)_color:', path)
def test_every_role_has_a_light_and_a_dark_value(self):
roles = re.search(r'enum Role : uint8_t \{(.*?)ROLE_COUNT', THEME, re.S)[1]
@@ -67,7 +69,7 @@ class Paints(unittest.TestCase):
def test_every_paint_is_defined_adopted_and_used_in_every_profile(self):
paints = paint_names()
for path in PROFILES:
text = path.read_text()
text = profiles.merged(path)
defined = re.findall(r'^ - id: (paint_\w+)$', text, re.M)
used = set(re.findall(r'styles: (paint_\w+)$', text, re.M))
filling = text.split('theme::paints = []() {', 1)[1].split('\n };', 1)[0]
@@ -75,33 +77,33 @@ class Paints(unittest.TestCase):
listed = re.search(r'for \(auto \*style : \{([^}]*)\}\) theme::fill\(style, Paint::(\w+)\);', filling)
looped = [(name, listed[2]) for name in re.findall(r'id\((\w+)\)', listed[1])]
adopted = dict(explicit + looped)
self.assertEqual(len(adopted), len(explicit) + len(looped), f'{path.name}: every style is filled once')
self.assertEqual(text.count('theme::fill('), len(explicit) + 1, f'{path.name}: styles are filled in theme::paints only')
self.assertEqual(set(defined), used, f'{path.name}: every paint style is used, every used one defined')
self.assertEqual(len(adopted), len(explicit) + len(looped), f'{path}: every style is filled once')
self.assertEqual(text.count('theme::fill('), len(explicit) + 1, f'{path}: styles are filled in theme::paints only')
self.assertEqual(set(defined), used, f'{path}: every paint style is used, every used one defined')
for style in defined + ['style_page', 'style_tile', 'style_icon_circle', 'style_room', 'style_time', 'style_title',
'style_value', 'style_value_big']:
self.assertIn(style, adopted, f'{path.name}: {style} takes its colours from theme.h')
self.assertIn(adopted[style], paints, f'{path.name}: {style}')
self.assertIn(style, adopted, f'{path}: {style} takes its colours from theme.h')
self.assertIn(adopted[style], paints, f'{path}: {style}')
for style, paint in adopted.items():
if style.startswith('paint_'):
self.assertEqual(style, f'paint_{paint}', f'{path.name}: a paint style is named after its paint')
self.assertEqual(style, f'paint_{paint}', f'{path}: a paint style is named after its paint')
# The styles that carry colours have none written down.
styles = re.search(r'^ style_definitions:\n(.*?)^ # ---------- ALERT', text, re.M | re.S)[1]
self.assertNotRegex(styles, r'_color:', path.name)
self.assertNotRegex(styles, r'_color:', path)
def test_the_look_is_applied_at_boot_after_a_change_and_drawn_again(self):
for path in PROFILES:
text = path.read_text()
text = profiles.text(path)
boot = text.split('runtime_tiles::load_settings();', 1)[1].split('runtime_tiles::settings_changed', 1)[0]
self.assertIn('theme::paints();\n theme::set_dark(settings_screen::dark_mode != 0);', boot,
f'{path.name}: the paints filled, then the saved look, before the first frame')
f'{path}: the paints filled, then the saved look, before the first frame')
apply = text.split(' - id: apply_screen_settings\n', 1)[1].split('\n - id: ', 1)[0]
self.assertIn('theme::set_dark(settings_screen::dark_mode != 0);', apply, f'{path.name}: a change of the setting')
self.assertIn('theme::set_dark(settings_screen::dark_mode != 0);', apply, f'{path}: a change of the setting')
redraw = text.split('theme::redraw = []() {', 1)[1].split('};', 1)[0]
for needle in ('runtime_tiles::restyle();', 'light_controls::restyle();', 'settings_screen::restyle();',
'theme::surface(id(alert_card_color))', 'id(climate_card_refresh).execute();',
'id(open_climate_mode_picker).execute();'):
self.assertIn(needle, redraw, f'{path.name}: {needle}')
self.assertIn(needle, redraw, f'{path}: {needle}')
self.assertIn('id(alert_card_color) = alert.color;', text)
self.assertIn('lv_color_hex(theme::surface(alert.color))', text)