d76461d774
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>
105 lines
7.5 KiB
Python
105 lines
7.5 KiB
Python
"""Direct controls on wide cards: editor choices, validation and what goes on the wire."""
|
|
import importlib.util
|
|
from pathlib import Path
|
|
import re
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
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 CONTROLS, controls_catalogue, min_firmware, packets, resolve_controls, screen_options, state_message, validate_layout
|
|
import tile_icons
|
|
import test_portal
|
|
|
|
HAS_AIOHTTP = importlib.util.find_spec('aiohttp') is not None
|
|
|
|
|
|
class ControlChoices(unittest.TestCase):
|
|
def test_catalogue_offers_each_domain_its_sets_plus_none(self):
|
|
catalogue = controls_catalogue()
|
|
self.assertEqual(catalogue['media_player']['default'], 'volume')
|
|
self.assertEqual([c['key'] for c in catalogue['media_player']['choices']], ['volume', 'playback', 'none'])
|
|
self.assertEqual([c['key'] for c in catalogue['climate']['choices']], ['setpoint', 'mode', 'none'])
|
|
for domain in ('climate', 'switch', 'vacuum', 'cover', 'media_player'):
|
|
self.assertIn(domain, catalogue)
|
|
self.assertNotIn('sensor', catalogue)
|
|
|
|
def test_validation_accepts_own_sets_and_rejects_others(self):
|
|
layout = validate_layout({'title': 'T', 'tiles': [{'entity': 'cover.curtain', 'options': {'size': 'wide', 'controls': 'position'}}]})
|
|
self.assertEqual(layout['tiles'][0]['options']['controls'], 'position')
|
|
validate_layout({'title': 'T', 'tiles': [{'entity': 'light.a', 'options': {'controls': 'none'}}]})
|
|
for entity, choice in (('cover.a', 'volume'), ('sensor.a', 'toggle'), ('switch.a', 'brightness'), ('climate.a', 42), ('light.a', 'run')):
|
|
with self.assertRaises(ValueError):
|
|
validate_layout({'title': 'T', 'tiles': [{'entity': entity, 'options': {'size': 'wide', 'controls': choice}}]})
|
|
|
|
def test_wide_cards_get_the_default_set_only_in_the_standard_layout(self):
|
|
self.assertEqual(resolve_controls({'entity': 'climate.a', 'options': {'size': 'wide'}}), 'setpoint')
|
|
self.assertEqual(resolve_controls({'entity': 'media_player.a', 'options': {'size': 'wide', 'controls': 'playback'}}), 'playback')
|
|
self.assertIsNone(resolve_controls({'entity': 'media_player.a', 'options': {'size': 'wide', 'controls': 'none'}}))
|
|
self.assertIsNone(resolve_controls({'entity': 'climate.a'}), 'single cards stay as they are')
|
|
self.assertIsNone(resolve_controls({'entity': 'climate.a', 'options': {'size': 'wide', 'display': 'watch'}}))
|
|
self.assertIsNone(resolve_controls({'entity': 'light.a', 'options': {'size': 'wide', 'inline': 'slider'}}))
|
|
self.assertIsNone(resolve_controls({'entity': 'sensor.a', 'options': {'size': 'wide'}}))
|
|
|
|
def test_wire_carries_only_the_shown_set_and_the_attributes_the_panels_need(self):
|
|
states = {'media_player.sonos': {'state': 'playing', 'attributes': {'volume_level': 0.17, 'is_volume_muted': False, 'media_title': 'TV', 'supported_features': 8321599, 'assumed_state': True}},
|
|
'cover.curtain': {'state': 'open', 'attributes': {'current_position': 80, 'device_class': 'curtain', 'supported_features': 15}},
|
|
'climate.ac': {'state': 'cool', 'attributes': {'hvac_action': 'cooling', 'hvac_modes': ['off', 'cool', 'heat'], 'temperature': 20, 'target_temp_step': 1.0}}}
|
|
msg = state_message(0, {'entity': 'media_player.sonos', 'name': '', 'options': {'size': 'wide'}}, states)
|
|
self.assertEqual(msg['o'], {'size': 'wide', 'controls': 'volume'})
|
|
self.assertIs(msg['a']['is_volume_muted'], False)
|
|
self.assertNotIn('assumed_state', msg['a'])
|
|
msg = state_message(1, {'entity': 'cover.curtain', 'name': '', 'options': {'size': 'wide', 'controls': 'none', 'background': 'blue'}}, states)
|
|
self.assertEqual(msg['o'], {'size': 'wide', 'background': 'blue'}, 'an explicit none is not sent; the stored choice stays')
|
|
self.assertEqual(msg['a']['device_class'], 'curtain')
|
|
msg = state_message(2, {'entity': 'climate.ac', 'name': '', 'options': {'size': 'wide', 'controls': 'mode'}}, states)
|
|
self.assertEqual(msg['o'], {'size': 'wide', 'controls': 'mode'})
|
|
self.assertEqual(msg['a']['hvac_action'], 'cooling')
|
|
self.assertTrue(all(len(p) <= 255 for p in packets(msg)))
|
|
msg = state_message(3, {'entity': 'climate.ac', 'name': '', 'options': {'controls': 'mode'}}, states)
|
|
self.assertEqual(msg['o'], {}, 'a single card never shows a panel')
|
|
self.assertIsNone(min_firmware({'tiles': [{'entity': 'climate.ac', 'options': {'size': 'wide', 'controls': 'mode'}}]}), 'older firmware ignores the field')
|
|
|
|
def test_screen_options_keeps_the_choice_out_of_the_wire_but_in_storage(self):
|
|
tile = {'entity': 'switch.desk', 'name': '', 'options': {'size': 'wide', 'controls': 'toggle', 'icon': 'auto'}}
|
|
self.assertEqual(screen_options(tile, {}), {'size': 'wide', 'controls': 'toggle'})
|
|
self.assertEqual(validate_layout({'title': 'T', 'tiles': [tile]})['tiles'][0]['options'], tile['options'])
|
|
|
|
def test_control_glyphs_exist_in_the_icon_fonts_and_the_editor_font(self):
|
|
header = (ROOT / 'components/smart_display/tile_controls.h').read_text()
|
|
used = {code.upper() for code in re.findall(r'\\U000(F[0-9A-F]{4})', header)}
|
|
carried = {code for code in tile_icons.GLYPHS.values()}
|
|
self.assertTrue(used <= carried, used - carried)
|
|
self.assertTrue(set(tile_icons.CONTROL_GLYPHS) <= set(tile_icons.GLYPHS))
|
|
self.assertEqual(set(tile_icons.editor()['controls']), set(tile_icons.CONTROL_GLYPHS))
|
|
for name in ('guition-4848s040.yaml', 'home-like-2432s028.yaml', 'packages/guition.yaml', 'packages/cyd.yaml'):
|
|
text = profiles.text(name)
|
|
for code in used:
|
|
self.assertIn(f'"\\U000{code}"', text, (name, code))
|
|
self.assertIn('runtime_tiles::control_font = id(sublabel_big)->get_lv_font();', text, name)
|
|
|
|
|
|
@unittest.skipUnless(HAS_AIOHTTP, 'Run using .venv-portal/bin/python for server tests')
|
|
class ControlInventory(unittest.IsolatedAsyncioTestCase):
|
|
async def test_inventory_lists_the_catalogue_and_a_saved_choice_survives_an_old_editor(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
m = test_portal.ManagerTests().setup_manager(Path(tmp) / 'screens.json')
|
|
self.assertEqual(m.inventory()[0][0]['id'], 'text.screen')
|
|
m.save('text.screen', {'title': 'Home', 'tiles': [{'entity': 'light.a', 'options': {'size': 'wide', 'controls': 'brightness'}}]})
|
|
m.save('text.screen', {'title': 'Home', 'tiles': [{'entity': 'light.a', 'options': {'size': 'wide'}}]})
|
|
self.assertEqual(m.layouts['text.screen']['tiles'][0]['options'], {'size': 'wide', 'controls': 'brightness'})
|
|
await m.sync_one('text.screen', m.layouts['text.screen'])
|
|
self.assertEqual(m.ha.messages[1][1]['o'], {'size': 'wide', 'controls': 'brightness'})
|
|
from aiohttp.test_utils import TestClient, TestServer
|
|
from server import create_app
|
|
async with TestClient(TestServer(create_app(m, development=True))) as client:
|
|
data = await (await client.get('/api/inventory')).json()
|
|
self.assertEqual(data['controls']['cover']['default'], 'buttons')
|
|
self.assertIn('controls', data['icons'])
|
|
light = await (await client.get('/api/inventory?light=1')).json()
|
|
self.assertNotIn('controls', light)
|