Put ESP Screens in English and keep layouts when entity IDs change (0.2.40)

Everything a user sees or reads is now English: both boards' labels,
status replies and logs, the ESP Screens editor and Alerts cheatsheet,
and the README and guides (docs/TILES.md, CALIBRATING.md, ACCEPTANCE.md
and TROUBLESHOOTING.md replace the Dutch file names). Labels follow
Home Assistant's own English names (Heat/Cool modes, Heating/Cooling
actions, Away, Returning to dock, weather conditions), dates use English
abbreviations and the top bar writes numbers the English way. Docs use
HA 2026.9's menu names (Settings -> Apps, App store, Settings -> Tools).
Unused legacy files (home-like.yaml, buttons.yaml, a.out) are removed.

Firmware 0.2.34 names its entities in English. Home Assistant registers
a renamed ESPHome entity under a new entity ID, so the inbox
text.<screen>_tegelinstellingen becomes text.<screen>_tile_settings and
the layout stored under the old ID would be lost. The manager now
recognises the screen by its device, during a run or after a restart,
and moves its layout and update history along; the Update button and
the nightly round follow the screen through the rename. Screens that
still run 0.2.33 or older keep working: both languages are matched.

Main is the only release branch from now on.

Tested on a Home Assistant Yellow (HA 2026.9.1) with both bench boards:
see docs/TEST_RESULTS_0240.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-15 02:08:43 +02:00
parent f60eed3ff6
commit 3e959b3df9
87 changed files with 5049 additions and 11167 deletions
+10 -10
View File
@@ -26,7 +26,7 @@ class ControlChoices(unittest.TestCase):
self.assertNotIn('sensor', catalogue)
def test_validation_accepts_own_sets_and_rejects_others(self):
layout = validate_layout({'title': 'T', 'tiles': [{'entity': 'cover.gordijn', 'options': {'size': 'wide', 'controls': 'position'}}]})
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')):
@@ -44,25 +44,25 @@ class ControlChoices(unittest.TestCase):
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.gordijn': {'state': 'open', 'attributes': {'current_position': 80, 'device_class': 'curtain', 'supported_features': 15}},
'climate.airco': {'state': 'cool', 'attributes': {'hvac_action': 'cooling', 'hvac_modes': ['off', 'cool', 'heat'], 'temperature': 20, 'target_temp_step': 1.0}}}
'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.gordijn', 'name': '', 'options': {'size': 'wide', 'controls': 'none', 'background': 'blue'}}, states)
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.airco', 'name': '', 'options': {'size': 'wide', 'controls': 'mode'}}, states)
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.airco', 'name': '', 'options': {'controls': 'mode'}}, states)
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.airco', 'options': {'size': 'wide', 'controls': 'mode'}}]}), 'older firmware ignores the field')
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.bureau', 'name': '', 'options': {'size': 'wide', 'controls': 'toggle', 'icon': 'auto'}}
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'])
@@ -86,8 +86,8 @@ class ControlInventory(unittest.IsolatedAsyncioTestCase):
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': 'Thuis', 'tiles': [{'entity': 'light.a', 'options': {'size': 'wide', 'controls': 'brightness'}}]})
m.save('text.screen', {'title': 'Thuis', 'tiles': [{'entity': 'light.a', 'options': {'size': 'wide'}}]})
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'})