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
+37 -37
View File
@@ -1,9 +1,9 @@
"""Top bar of a screen ("bovenbalk"): what the firmware draws right of the screen name.
"""Top bar of a screen: what the firmware draws right of the screen name.
Firmware 0.2.32+ only draws. This module decides everything that needs Home Assistant: an
entity's text in Dutch (number format, units, translated states), its icon, its accent colour
entity's text (number format, units, translated states), its icon, its accent colour
and whether it shows at all. Clock, analog clock and date tick on the screen itself, and a
relative time ("5 min geleden") travels as a unix time, so the screen counts the minutes
relative time ("5 min ago") travels as a unix time, so the screen counts the minutes
without any traffic.
Wire format, one message after the layout: {"v":1,"op":"header","items":[...]} with per item
@@ -21,33 +21,33 @@ from core import HEADER_BUILTIN, HEADER_CONTENTS, HEADER_MAX_ITEMS, HEADER_MIN_F
# tests/test_header_bar.py keeps them equal). Anything else folds to its base letter or goes.
GLYPHS = frozenset("<>—&@!,.?\"%()+-_:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyzäöüÄÖÜß/…·'#*=;²³µéèëêïîóôàáâçñúû–")
TEXT_BYTES = 40
MONTHS = ('jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec')
MONTHS = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')
UNAVAILABLE = '—'
CONTENT_LABELS = {'state': 'Status', 'last_changed': 'Laatst gewijzigd'}
SHOW_LABELS = {'always': 'Altijd', 'active': 'Alleen als actief'}
CONTENT_LABELS = {'state': 'Status', 'last_changed': 'Last changed'}
SHOW_LABELS = {'always': 'Always', 'active': 'Only when active'}
# Home Assistant's own words where they fit a small bar; the screen shows the rest as they come.
STATES = {
'on': 'Aan', 'off': 'Uit', 'home': 'Thuis', 'not_home': 'Weg', 'open': 'Open', 'closed': 'Dicht',
'opening': 'Gaat open', 'closing': 'Gaat dicht', 'locked': 'Op slot', 'unlocked': 'Open', 'locking': 'Sluiten',
'unlocking': 'Openen', 'jammed': 'Vast', 'playing': 'Speelt', 'paused': 'Pauze', 'idle': 'Inactief',
'standby': 'Stand-by', 'buffering': 'Laden', 'cleaning': 'Bezig', 'docked': 'In dock', 'returning': 'Naar dock',
'error': 'Fout', 'heat': 'Verwarmen', 'cool': 'Koelen', 'heat_cool': 'Auto', 'auto': 'Auto', 'dry': 'Drogen',
'fan_only': 'Ventileren', 'disarmed': 'Uit', 'armed_home': 'Thuis', 'armed_away': 'Afwezig', 'armed_night': 'Nacht',
'armed_vacation': 'Vakantie', 'armed_custom_bypass': 'Aan', 'arming': 'Inschakelen', 'pending': 'Wachten',
'triggered': 'Alarm', 'active': 'Loopt', 'above_horizon': 'Op', 'below_horizon': 'Onder',
'on': 'On', 'off': 'Off', 'home': 'Home', 'not_home': 'Away', 'open': 'Open', 'closed': 'Closed',
'opening': 'Opening', 'closing': 'Closing', 'locked': 'Locked', 'unlocked': 'Unlocked', 'locking': 'Locking',
'unlocking': 'Unlocking', 'jammed': 'Jammed', 'playing': 'Playing', 'paused': 'Paused', 'idle': 'Idle',
'standby': 'Standby', 'buffering': 'Buffering', 'cleaning': 'Cleaning', 'docked': 'Docked', 'returning': 'Returning',
'error': 'Error', 'heat': 'Heat', 'cool': 'Cool', 'heat_cool': 'Auto', 'auto': 'Auto', 'dry': 'Dry',
'fan_only': 'Fan only', 'disarmed': 'Disarmed', 'armed_home': 'Armed home', 'armed_away': 'Armed away', 'armed_night': 'Armed night',
'armed_vacation': 'Armed vacation', 'armed_custom_bypass': 'Armed', 'arming': 'Arming', 'pending': 'Pending',
'triggered': 'Triggered', 'active': 'Active', 'above_horizon': 'Up', 'below_horizon': 'Down',
}
# device_class: (on, off)
BINARY_STATES = {
'battery': ('Laag', 'Normaal'), 'battery_charging': ('Laden', 'Niet laden'), 'carbon_monoxide': ('Gevaar', 'Veilig'),
'cold': ('Koud', 'Normaal'), 'connectivity': ('Verbonden', 'Verbroken'), 'door': ('Open', 'Dicht'),
'garage_door': ('Open', 'Dicht'), 'gas': ('Gevaar', 'Veilig'), 'heat': ('Heet', 'Normaal'), 'light': ('Licht', 'Donker'),
'lock': ('Open', 'Op slot'), 'moisture': ('Nat', 'Droog'), 'motion': ('Beweging', 'Geen beweging'),
'moving': ('Beweegt', 'Stil'), 'occupancy': ('Bezet', 'Vrij'), 'opening': ('Open', 'Dicht'), 'plug': ('Aangesloten', 'Los'),
'power': ('Aan', 'Uit'), 'presence': ('Thuis', 'Weg'), 'problem': ('Probleem', 'OK'), 'running': ('Actief', 'Inactief'),
'safety': ('Onveilig', 'Veilig'), 'smoke': ('Rook', 'Geen rook'), 'sound': ('Geluid', 'Stil'), 'tamper': ('Sabotage', 'OK'),
'update': ('Update', 'Bijgewerkt'), 'vibration': ('Trilling', 'Stil'), 'window': ('Open', 'Dicht'),
'battery': ('Low', 'Normal'), 'battery_charging': ('Charging', 'Not charging'), 'carbon_monoxide': ('Danger', 'Safe'),
'cold': ('Cold', 'Normal'), 'connectivity': ('Connected', 'Disconnected'), 'door': ('Open', 'Closed'),
'garage_door': ('Open', 'Closed'), 'gas': ('Danger', 'Safe'), 'heat': ('Hot', 'Normal'), 'light': ('Light', 'Dark'),
'lock': ('Open', 'Locked'), 'moisture': ('Wet', 'Dry'), 'motion': ('Motion', 'No motion'),
'moving': ('Moving', 'Still'), 'occupancy': ('Occupied', 'Clear'), 'opening': ('Open', 'Closed'), 'plug': ('Plugged in', 'Unplugged'),
'power': ('On', 'Off'), 'presence': ('Home', 'Away'), 'problem': ('Problem', 'OK'), 'running': ('Active', 'Inactive'),
'safety': ('Unsafe', 'Safe'), 'smoke': ('Smoke', 'No smoke'), 'sound': ('Sound', 'Silent'), 'tamper': ('Tampering', 'OK'),
'update': ('Update', 'Up to date'), 'vibration': ('Vibration', 'Still'), 'window': ('Open', 'Closed'),
}
# Icon names from tile_icons; the fonts of both boards carry every one of them.
BINARY_ICONS = { # device_class: (on, off)
@@ -101,7 +101,7 @@ def clean_text(text):
return short(''.join(out).strip(), TEXT_BYTES)
def number_text(value, precision=None):
"""A number as Home Assistant writes it in Dutch: decimal comma, dots between thousands."""
"""A number as Home Assistant writes it: decimal point, commas between thousands."""
if precision is None:
# Without a display precision HA keeps integers whole and shows at most three decimals.
digits = 0 if float(value).is_integer() else 3
@@ -112,13 +112,13 @@ def number_text(value, precision=None):
text = f'{value:.{precision}f}'
negative = text.startswith('-')
whole, _, fraction = text.lstrip('-').partition('.')
grouped = f'{int(whole):,}'.replace(',', '.')
result = grouped + (',' + fraction if fraction else '')
# "-0" and "-0,0" are just zero.
grouped = f'{int(whole):,}'
result = grouped + ('.' + fraction if fraction else '')
# "-0" and "-0.0" are just zero.
return '-' + result if negative and any(c not in '0.,' for c in result) else result
def with_unit(text, unit):
"""Home Assistant's spacing in Dutch: "21,3 °C", "65%", "18°"."""
"""Home Assistant's spacing: "21.3 °C", "65%", "18°"."""
if not unit:
return text
return text + unit if unit in ('%', '°') else f'{text} {unit}'
@@ -152,7 +152,7 @@ def value(entity, state, entry=None, units=None, tz=None):
return (None, epoch(raw)) if epoch(raw) else (UNAVAILABLE, None)
if domain == 'script':
moment = epoch(attrs.get('last_triggered'))
return ('Bezig', None) if raw == 'on' else (None, moment) if moment else ('Nog niet gestart', None)
return ('Running', None) if raw == 'on' else (None, moment) if moment else ('Never run', None)
if domain == 'sun':
# The next event: sunset while the sun is up, sunrise while it is down.
clock = local_clock(attrs.get('next_setting' if raw == 'above_horizon' else 'next_rising'), tz)
@@ -169,7 +169,7 @@ def value(entity, state, entry=None, units=None, tz=None):
if domain in ('person', 'device_tracker'):
return STATES.get(raw, raw), None
if domain == 'binary_sensor':
pair = BINARY_STATES.get(attrs.get('device_class'), ('Aan', 'Uit'))
pair = BINARY_STATES.get(attrs.get('device_class'), ('On', 'Off'))
return (pair[0] if raw == 'on' else pair[1] if raw == 'off' else raw), None
if domain == 'input_datetime':
# "2026-09-15 07:30:00", "2026-09-15" or just "07:30:00".
@@ -197,7 +197,7 @@ def value(entity, state, entry=None, units=None, tz=None):
return raw[:1].upper() + raw[1:], None
def active(entity, state):
""""Alleen als actief": on, open, home, detected, playing, heating, or a number other than zero."""
""""Only when active": on, open, home, detected, playing, heating, or a number other than zero."""
raw = state.get('state') if state else None
domain = entity.split('.')[0]
if raw in (None, '', 'unavailable', 'unknown'):
@@ -324,13 +324,13 @@ def suggestions(screen, entities, states, registry=None, limit=8):
def sensor(device_class):
return lambda e: e['id'].startswith('sensor.') and attrs(e).get('device_class') == device_class and numeric(states[e['id']]['state']) is not None
picks = []
pick('Temperatuur', sensor('temperature'))
pick('Luchtvochtigheid', sensor('humidity'))
pick('Stroomverbruik', sensor('power'))
pick('Laatste beweging', lambda e: e['id'].startswith('binary_sensor.') and attrs(e).get('device_class') in ('motion', 'occupancy', 'presence'), 'last_changed')
pick('Weer', lambda e: e['id'].startswith('weather.'))
pick('Aantal thuis', lambda e: e['id'] == 'zone.home')
pick('Zon op en onder', lambda e: e['id'] == 'sun.sun')
pick('Temperature', sensor('temperature'))
pick('Humidity', sensor('humidity'))
pick('Power usage', sensor('power'))
pick('Last motion', lambda e: e['id'].startswith('binary_sensor.') and attrs(e).get('device_class') in ('motion', 'occupancy', 'presence'), 'last_changed')
pick('Weather', lambda e: e['id'].startswith('weather.'))
pick('People home', lambda e: e['id'] == 'zone.home')
pick('Sunrise and sunset', lambda e: e['id'] == 'sun.sun')
pick('CO₂', sensor('carbon_dioxide'))
return picks[:limit]