Files
homeassistant_espscreen/screen_manager/app/tile_icons.py
T
Max Gramser bb25353089 Forty-eight tiles, a tile over the whole page and tiles that go to a page (0.2.74)
Max asked for many more tiles, a tile that takes the whole page as a light
switch you push without looking, and tiles that go to a page. The limit is
one tile per slot of the eight pages, 48, and the firmware keeps its tile
list on the heap sized to the layout (PSRAM on a Guition). A tile's size can
be `full`: one big button that lights up in the state colour while on, or the
double-width card's head with its small slider, direct controls or graph at
the bottom and the value the double-width card had no room for in between;
the clock, the forecast (with an hours strip) and the sun path fill the page.
Navigation tiles are the built-in entities screen.page_1 to screen.page_8:
an arrow or a chosen icon, the name and "Page n", a tap opens that page.
The app sends the three only to firmware 0.2.62+, the editor (the Vue port of
this round by the editor session, 00c9a4e) offers Normal / Double-width /
Full page and "Goes to page", and Claude in Home Assistant gets size: full and
the page tiles. Built-in tiles now send a chosen icon as its codepoint.

Found and fixed by the new host harness before release: the one-second tick
moved the analog clock's second hand (part 18) on a full forecast's hour label
during a page switch and corrupted it. Firmware 0.2.62; both C++ and Python
suites, host harness 37/37 on both boards, hardware pass on both bench boards
in docs/TEST_RESULTS_0274.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 08:44:44 +02:00

499 lines
22 KiB
Python

"""Curated Material Design Icons for tiles, shared by the editor and both firmware boards.
This is the only list. `python3 tools/generate_icons.py` writes these glyphs into
the three icon fonts of both board profiles and builds the editor font
`static/tile-icons.woff`; `--check` verifies names and codepoints against
fonts/materialdesignicons-webfont.ttf. Firmware 0.2.18+ carries every glyph here.
"""
import re
# (group, ((MDI name, codepoint, label), ...)) in picker order.
GROUPS = (
('Lighting', (
('lightbulb', 'F0335', 'Light bulb'),
('lightbulb-group', 'F1253', 'Lights'),
('ceiling-light', 'F0769', 'Ceiling light'),
('chandelier', 'F1793', 'Chandelier'),
('lamp', 'F06B5', 'Table lamp'),
('floor-lamp', 'F08DD', 'Floor lamp'),
('desk-lamp', 'F095F', 'Desk lamp'),
('wall-sconce-flat', 'F091D', 'Wall light'),
('light-recessed', 'F179B', 'Recessed light'),
('spotlight-beam', 'F04C9', 'Spotlight'),
('led-strip-variant', 'F1051', 'LED strip'),
('outdoor-lamp', 'F1054', 'Outdoor light'),
)),
('Rooms', (
('home', 'F02DC', 'House'),
('sofa', 'F04B9', 'Living room'),
('bed', 'F02E3', 'Bedroom'),
('silverware-fork-knife', 'F0A70', 'Dining room'),
('chef-hat', 'F0B7C', 'Kitchen'),
('shower', 'F09A0', 'Shower'),
('bathtub', 'F1818', 'Bathroom'),
('toilet', 'F09AB', 'Toilet'),
('desk', 'F1239', 'Office'),
('garage', 'F06D9', 'Garage'),
('fireplace', 'F0E2E', 'Fireplace'),
)),
('Climate', (
('thermometer', 'F050F', 'Temperature'),
('thermostat', 'F0393', 'Thermostat'),
('air-conditioner', 'F001B', 'AC'),
('radiator', 'F0438', 'Radiator'),
('heat-pump', 'F1A43', 'Heat pump'),
('fire', 'F0238', 'Heating'),
('snowflake', 'F0717', 'Cooling'),
('water-percent', 'F058E', 'Humidity'),
('fan', 'F0210', 'Fan'),
('ceiling-fan', 'F1797', 'Ceiling fan'),
('air-purifier', 'F0D44', 'Air purifier'),
('molecule-co2', 'F07E4', 'CO₂'),
)),
('Weather', (
('weather-sunny', 'F0599', 'Sun'),
('weather-partly-cloudy', 'F0595', 'Partly cloudy'),
('weather-cloudy', 'F0590', 'Cloudy'),
('weather-rainy', 'F0597', 'Rain'),
('weather-snowy', 'F0598', 'Snow'),
('weather-windy', 'F059D', 'Wind'),
('weather-night', 'F0594', 'Night'),
('weather-sunset-up', 'F059C', 'Sunrise'),
)),
('Media and music', (
('music-note', 'F0387', 'Music'),
('music', 'F075A', 'Music notes'),
('playlist-music', 'F0CB8', 'Playlist'),
('album', 'F0025', 'Album'),
('record-player', 'F099A', 'Record player'),
('spotify', 'F04C7', 'Spotify'),
('radio', 'F0439', 'Radio'),
('podcast', 'F0994', 'Podcast'),
('speaker', 'F04C3', 'Speaker'),
('speaker-multiple', 'F0D38', 'Speakers'),
('soundbar', 'F17DB', 'Soundbar'),
('headphones', 'F02CB', 'Headphones'),
('piano', 'F067D', 'Piano'),
('guitar-acoustic', 'F0771', 'Guitar'),
('television', 'F0502', 'TV'),
('projector', 'F042E', 'Projector'),
('movie-open', 'F0FCE', 'Movie'),
('cast', 'F0118', 'Cast'),
('gamepad-variant', 'F0297', 'Game console'),
('remote-tv', 'F0EC5', 'Remote control'),
)),
('Security', (
('lock', 'F033E', 'Lock'),
('lock-open-variant', 'F0FC6', 'Lock open'),
('shield-home', 'F068A', 'Alarm'),
('alarm-light', 'F078F', 'Alarm light'),
('bell', 'F009A', 'Bell'),
('doorbell', 'F12E6', 'Doorbell'),
('cctv', 'F07AE', 'Camera'),
('motion-sensor', 'F0D91', 'Motion sensor'),
('door-closed', 'F081B', 'Door'),
('door-open', 'F081C', 'Door open'),
('window-closed-variant', 'F11DB', 'Window'),
('smoke-detector', 'F0392', 'Smoke detector'),
('water-alert', 'F1502', 'Water leak'),
)),
('Appliances', (
('washing-machine', 'F072A', 'Washing machine'),
('tumble-dryer', 'F0917', 'Dryer'),
('dishwasher', 'F0AAC', 'Dishwasher'),
('fridge', 'F0290', 'Fridge'),
('stove', 'F04DE', 'Stove'),
('microwave', 'F0C99', 'Microwave'),
('toaster-oven', 'F0CD3', 'Oven'),
('coffee-maker', 'F109F', 'Coffee'),
('kettle', 'F05FA', 'Kettle'),
('robot-vacuum', 'F070D', 'Robot vacuum'),
('robot-mower', 'F11F7', 'Robot mower'),
('printer-3d', 'F042B', '3D printer'),
('power-plug', 'F06A5', 'Plug'),
('power-socket-eu', 'F07E7', 'Socket'),
('laptop', 'F0322', 'Laptop'),
('router-wireless', 'F0469', 'Router'),
)),
('Energy', (
('flash', 'F0241', 'Power'),
('solar-power', 'F0A72', 'Solar power'),
('solar-panel', 'F0D9B', 'Solar panels'),
('home-battery', 'F1901', 'Home battery'),
('battery-high', 'F12A3', 'Battery'),
('ev-station', 'F05F1', 'Charging station'),
('car-electric', 'F0B6C', 'Electric car'),
('meter-electric', 'F1A57', 'Power meter'),
('meter-gas', 'F1A59', 'Gas meter'),
('water', 'F058C', 'Water'),
('water-boiler', 'F0F92', 'Boiler'),
('gauge', 'F029A', 'Meter'),
)),
('Window coverings', (
('window-shutter', 'F111C', 'Shutter'),
('window-shutter-open', 'F111E', 'Shutter open'),
('blinds', 'F00AC', 'Blinds'),
('blinds-open', 'F1011', 'Blinds open'),
('roller-shade-closed', 'F1A6C', 'Roller blind'),
('curtains', 'F1846', 'Curtains'),
('curtains-closed', 'F1847', 'Curtains closed'),
('awning-outline', 'F1B88', 'Awning'),
)),
('Garden and pets', (
('flower', 'F024A', 'Flower'),
('tree', 'F0531', 'Tree'),
('sprout', 'F0E66', 'Plant'),
('grass', 'F1510', 'Lawn'),
('sprinkler', 'F105F', 'Sprinkler'),
('pool', 'F0606', 'Pool'),
('grill', 'F0E45', 'Barbecue'),
('mailbox', 'F06EE', 'Mailbox'),
('trash-can', 'F0A79', 'Trash'),
('recycle', 'F044C', 'Recycling'),
('dog', 'F0A43', 'Dog'),
('cat', 'F011B', 'Cat'),
)),
('People and travel', (
('account', 'F0004', 'Person'),
('account-group', 'F0849', 'Family'),
('account-child', 'F0A89', 'Child'),
('home-account', 'F0826', 'Home'),
('sleep', 'F04B2', 'Sleeping'),
('run', 'F070E', 'Exercising'),
('briefcase', 'F00D6', 'Work'),
('car', 'F010B', 'Car'),
('bike', 'F00A3', 'Bike'),
('airplane', 'F001D', 'Vacation'),
('map-marker', 'F034E', 'Location'),
)),
('Other', (
('power', 'F0425', 'On/off'),
('toggle-switch', 'F0521', 'Switch'),
('gesture-tap-button', 'F12A8', 'Button'),
('play', 'F040A', 'Start'),
('script-text', 'F0BC2', 'Script'),
('palette', 'F03D8', 'Scene'),
('alarm', 'F0020', 'Alarm clock'),
('clock-outline', 'F0150', 'Clock'),
('timer-outline', 'F051B', 'Timer'),
('calendar', 'F00ED', 'Calendar'),
('bell-ring', 'F009E', 'Notification'),
('heart', 'F02D1', 'Favorite'),
('alert-outline', 'F002A', 'Warning'),
('cog', 'F0493', 'Settings'),
('broom', 'F00E2', 'Cleaning'),
)),
)
# Glyphs the firmware draws itself (weather conditions, sun, checkmark, direct controls) that the picker does not offer.
FIXED = (
('alert-circle-outline', 'F05D6'),
('check', 'F012C'),
# Direct controls on wide cards (firmware 0.2.19+).
('pause', 'F03E4'),
('stop', 'F04DB'),
('skip-next', 'F04AD'),
('skip-previous', 'F04AE'),
('volume-high', 'F057E'),
('volume-off', 'F0581'),
('arrow-up', 'F005D'),
('arrow-down', 'F0045'),
('arrow-expand-horizontal', 'F084E'),
('arrow-collapse-horizontal', 'F084C'),
('home-map-marker', 'F05F8'),
('plus', 'F0415'),
('minus', 'F0374'),
('chevron-left', 'F0141'),
('chevron-right', 'F0142'),
('close', 'F0156'),
# Back button of every overlay (firmware 0.2.37+).
('arrow-left', 'F004D'),
('sun-snowflake-variant', 'F1A79'),
('thermostat-auto', 'F1B17'),
('weather-fog', 'F0591'),
('weather-hail', 'F0592'),
('weather-lightning', 'F0593'),
('weather-lightning-rainy', 'F067E'),
('weather-pouring', 'F0596'),
('weather-snowy-rainy', 'F067F'),
('weather-sunset-down', 'F059B'),
# "More" key on the CYD climate card: fan and swing (firmware 0.2.39+).
('dots-horizontal', 'F01D8'),
# Swing row on the Guition climate card, Home Assistant's icon for swing modes (firmware 0.2.40+).
('arrow-oscillating', 'F1C91'),
# The settings page on the screen itself (firmware 0.2.44+): its menu and the restart row.
('monitor', 'F0379'),
('information-outline', 'F02FD'),
('restart', 'F0709'),
# A light that is off, as Home Assistant shows one without an icon of its own (firmware 0.2.53+).
('lightbulb-off', 'F0E4F'),
)
# Home Assistant's own default icons for the domains a tile or the top bar shows (frontend/get_icons, `entity_component`,
# Home Assistant 2026.9), with their state, range and device class variants and the few its frontend picks in code: a
# closed blind, a playing speaker, a battery at 40 %, a phone's tracker on the router (app 0.2.67). The firmware fonts carry them, so a tile shows the icon Home Assistant shows; a name missing here, such as
# one a later Home Assistant adds, keeps the screen's own default. Not pickable in the editor.
HA_DEFAULTS = (
('account-arrow-right', 'F0B53'),
('air-filter', 'F0D43'),
('air-humidifier', 'F1099'),
('air-humidifier-off', 'F1466'),
('alert-circle', 'F0028'),
('angle-acute', 'F0937'),
('arrow-bottom-left', 'F0042'),
('arrow-bottom-right', 'F0043'),
('arrow-down-box', 'F06C0'),
('arrow-left-right', 'F0E73'),
('arrow-right', 'F0054'),
('arrow-split-vertical', 'F093C'),
('arrow-top-left', 'F005B'),
('arrow-top-right', 'F005C'),
('arrow-up-box', 'F06C3'),
('audio-video', 'F093D'),
('audio-video-off', 'F11B6'),
('battery', 'F0079'),
('battery-10', 'F007A'),
('battery-20', 'F007B'),
('battery-30', 'F007C'),
('battery-40', 'F007D'),
('battery-50', 'F007E'),
('battery-60', 'F007F'),
('battery-70', 'F0080'),
('battery-80', 'F0081'),
('battery-90', 'F0082'),
('battery-alert', 'F0083'),
('battery-charging', 'F0084'),
('battery-outline', 'F008E'),
('battery-unknown', 'F0091'),
('blinds-horizontal', 'F1A2B'),
('blinds-horizontal-closed', 'F1A2C'),
('bluetooth', 'F00AF'),
('bluetooth-connect', 'F00B1'),
('brightness-5', 'F00DE'),
('brightness-7', 'F00E0'),
('button-pointer', 'F1B50'),
('car-battery', 'F010C'),
('car-coolant-level', 'F1019'),
('cash', 'F0114'),
('cast-connected', 'F0119'),
('cast-off', 'F078A'),
('check-circle', 'F05E0'),
('check-circle-outline', 'F05E1'),
('check-network-outline', 'F0C54'),
('checkbox-marked-circle', 'F0133'),
('circle-slice-8', 'F0AA5'),
('clock', 'F0954'),
('clock-start', 'F0155'),
('close-circle-outline', 'F015A'),
('close-network-outline', 'F0C5F'),
('compass-rose', 'F1382'),
('crop-portrait', 'F01A1'),
('crosshairs-question', 'F1136'),
('current-ac', 'F1480'),
('database', 'F01BC'),
('ear-hearing', 'F07C5'),
('eye', 'F0208'),
('eye-check', 'F0D04'),
('fan-off', 'F081D'),
('format-list-bulleted', 'F0279'),
('garage-open', 'F06DA'),
('gate', 'F0299'),
('gate-open', 'F116A'),
('home-outline', 'F06A1'),
('lan-connect', 'F0318'),
('lan-disconnect', 'F0319'),
('lightning-bolt', 'F140B'),
('lock-alert', 'F08EE'),
('lock-clock', 'F097F'),
('lock-open', 'F033F'),
('molecule', 'F0BAC'),
('molecule-co', 'F12FE'),
('motion-sensor-off', 'F1435'),
('music-note-off', 'F038A'),
('octagon', 'F03C3'),
('package', 'F03D3'),
('package-up', 'F03D5'),
('ph', 'F17C5'),
('pipe-valve', 'F184D'),
('power-plug-off', 'F06A6'),
('progress-clock', 'F0996'),
('projector-off', 'F1A23'),
('radioactive', 'F043C'),
('ray-vertex', 'F0445'),
('roller-shade', 'F1A6B'),
('script-text-play', 'F1727'),
('security', 'F0483'),
('shield', 'F0498'),
('shield-airplane', 'F06BB'),
('shield-lock', 'F099D'),
('shield-moon', 'F1828'),
('shield-off', 'F099E'),
('shield-outline', 'F0499'),
('sine-wave', 'F095B'),
('smoke-detector-alert', 'F192E'),
('smoke-detector-variant', 'F180B'),
('smoke-detector-variant-alert', 'F1930'),
('speaker-off', 'F04C4'),
('speaker-pause', 'F1B73'),
('speaker-play', 'F1B72'),
('speedometer', 'F04C5'),
('spoon-sugar', 'F1429'),
('sprout-outline', 'F0E67'),
('square', 'F0764'),
('square-outline', 'F0763'),
('storage-tank', 'F1A75'),
('sun-wireless', 'F17FE'),
('television-off', 'F083B'),
('television-pause', 'F0F89'),
('television-play', 'F0ECF'),
('texture-box', 'F0FE6'),
('thermometer-lines', 'F0510'),
('toggle-switch-variant', 'F1A25'),
('toggle-switch-variant-off', 'F1A26'),
('transmission-tower', 'F0D3E'),
('vibrate', 'F0566'),
('water-boiler-off', 'F11B4'),
('water-off', 'F058D'),
('water-opacity', 'F1855'),
('weight', 'F05A1'),
('wifi', 'F05A9'),
('window-closed', 'F05AE'),
('window-open', 'F05B1'),
)
# Pickable icons by name, and every glyph the firmware fonts contain.
ICONS = {name: (codepoint, label) for _, icons in GROUPS for name, codepoint, label in icons}
GLYPHS = {**{name: codepoint for name, (codepoint, _) in ICONS.items()}, **dict(FIXED), **dict(HA_DEFAULTS)}
# Mirrors runtime_tiles::icon_for() so the editor mockup shows what the screen draws.
DEFAULTS = {'light': 'lightbulb', 'climate': 'air-conditioner', 'vacuum': 'robot-vacuum', 'fan': 'fan',
'cover': 'window-shutter', 'scene': 'sofa', 'script': 'sofa', 'sensor': 'gauge', 'binary_sensor': 'gauge',
'timer': 'timer-outline', 'person': 'account', 'camera': 'cctv', 'image': 'cctv', 'screen': 'clock-outline'}
# The cards the screen brings itself: one icon per entity, not per domain.
BUILTIN_TILES = {'screen.clock': 'clock-outline', 'screen.settings': 'cog', **{f'screen.page_{n}': 'arrow-right' for n in range(1, 9)}}
FALLBACK = 'power'
# The large icon font of a card that takes the whole page (firmware 0.2.62+): what the screen draws on its own for a
# domain, a state or a built-in card, at 64 px on the Guition and 40 px on the CYD. Kept to these so the CYD's flash
# stays free; a chosen icon outside this set shows at its usual size in the big circle.
BIG_GLYPHS = tuple(dict.fromkeys(list(DEFAULTS.values()) + list(BUILTIN_TILES.values()) + ['lightbulb-off', FALLBACK] +
['weather-sunny', 'weather-night', 'weather-cloudy', 'weather-partly-cloudy', 'weather-rainy',
'weather-pouring', 'weather-snowy', 'weather-snowy-rainy', 'weather-fog', 'weather-hail',
'weather-lightning', 'weather-lightning-rainy', 'weather-windy', 'alert-circle-outline',
'weather-sunset-up', 'weather-sunset-down']))
CONTROL_GLYPHS = ('play', 'pause', 'stop', 'skip-next', 'skip-previous', 'volume-high', 'volume-off', 'arrow-up', 'arrow-down',
'arrow-expand-horizontal', 'arrow-collapse-horizontal', 'home-map-marker', 'plus', 'minus', 'chevron-left',
'chevron-right', 'close', 'power', 'fire', 'snowflake')
WEATHER = {'sunny': 'weather-sunny', 'clear-night': 'weather-night', 'cloudy': 'weather-cloudy',
'partlycloudy': 'weather-partly-cloudy', 'rainy': 'weather-rainy', 'pouring': 'weather-pouring',
'snowy': 'weather-snowy', 'snowy-rainy': 'weather-snowy-rainy', 'fog': 'weather-fog', 'hail': 'weather-hail',
'lightning': 'weather-lightning', 'lightning-rainy': 'weather-lightning-rainy', 'windy': 'weather-windy',
'windy-variant': 'weather-windy', 'exceptional': 'alert-circle-outline'}
# Domains whose icon the screen draws from the state itself: a weather condition, the sun above or below the horizon.
OWN_ICON_DOMAINS = frozenset(('weather', 'sun', 'screen'))
# Home Assistant's icon resources ({'entity_component': ..., 'entity': ...}), set by the app when it reads them; empty keeps
# the tables of earlier releases.
HA_ICONS = {}
def use_ha_icons(resources):
"""The icon resources Home Assistant answered frontend/get_icons with, for every icon lookup of this app."""
global HA_ICONS
HA_ICONS = resources if isinstance(resources, dict) else {}
# A number as JavaScript's Number() reads a state: Home Assistant's frontend picks a range icon by it.
JS_NUMBER = re.compile(r'[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?|[+-]?Infinity')
def js_number(text):
"""The number JavaScript's Number() reads from a state or a range key, None where it reads NaN (a blank text is 0)."""
if not isinstance(text, str):
return None
text = text.strip()
if not text:
return 0.0
return float(text.replace('Infinity', 'inf')) if JS_NUMBER.fullmatch(text) else None
def icon_for_state(state, spec):
"""Home Assistant's getIconFromTranslations: the icon for this state, else the range's icon for a number at or above its
lowest step (the highest step not above it), else the default."""
if not isinstance(spec, dict):
return None
states, ranges = spec.get('state'), spec.get('range')
if state and isinstance(states, dict) and states.get(state):
return states[state]
value = js_number(state) if isinstance(ranges, dict) else None
if value is not None:
steps = sorted(step for step in map(js_number, ranges) if step is not None)
below = [step for step in steps if step <= value]
if below:
step = below[-1]
icon = ranges.get(str(int(step)) if step.is_integer() else repr(step))
if icon:
return icon
return spec.get('default')
def state_icon(domain, state, attributes):
"""Home Assistant's stateIcon: the icons its frontend picks in code, before those of the integration."""
if domain == 'update':
return 'mdi:package-down' if attributes.get('in_progress') else 'mdi:package-up' if state == 'on' else 'mdi:package'
if domain == 'device_tracker':
source = attributes.get('source_type')
if source == 'router':
return 'mdi:lan-connect' if state == 'home' else 'mdi:lan-disconnect'
if source in ('bluetooth', 'bluetooth_le'):
return 'mdi:bluetooth-connect' if state == 'home' else 'mdi:bluetooth'
return 'mdi:account-arrow-right' if state == 'not_home' else 'mdi:account'
if domain == 'sun':
return 'mdi:white-balance-sunny' if state == 'above_horizon' else 'mdi:weather-night'
if domain == 'input_datetime':
if not attributes.get('has_date'):
return 'mdi:clock'
if not attributes.get('has_time'):
return 'mdi:calendar'
return None
def ha_default_icon(entity_id, state, attributes, entry, icons):
"""The icon Home Assistant's frontend shows for an entity without one of its own (frontend/get_icons, app 0.2.67), in
its order: the integration's icon for the entity's translation key, then the icon its frontend picks in code, then the
domain's icon for the state's device class or else the domain's. The MDI name, or None when Home Assistant has none;
`state` None is an entity without a state, which gets the defaults."""
if not isinstance(icons, dict):
return None
domain = entity_id.split('.', 1)[0]
entry = entry if isinstance(entry, dict) else {}
attributes = attributes if isinstance(attributes, dict) else None
icon = None
if entry.get('platform') and entry.get('translation_key'):
platform = ((icons.get('entity') or {}).get(entry['platform']) or {}).get(domain) or {}
icon = icon_for_state(state, platform.get(entry['translation_key']))
if not icon and attributes is not None and state is not None:
icon = state_icon(domain, state, attributes)
if not icon:
component = (icons.get('entity_component') or {}).get(domain) or {}
device_class = (attributes or {}).get('device_class')
icon = icon_for_state(state, (isinstance(device_class, str) and component.get(device_class)) or component.get('_'))
return icon[4:] if isinstance(icon, str) and icon.startswith('mdi:') else None
def default_glyph(entity_id, state, attributes, entry=None, icons=None):
"""Codepoint of Home Assistant's default icon when the firmware fonts carry it; None keeps the screen's own icon."""
if entity_id.split('.', 1)[0] in OWN_ICON_DOMAINS:
return None
name = ha_default_icon(entity_id, state, attributes, entry, HA_ICONS if icons is None else icons)
return GLYPHS.get(name) if name else None
def ha_icon(attributes):
"""Codepoint of Home Assistant's own `mdi:` icon when the firmware carries it, else None."""
icon = attributes.get('icon') if isinstance(attributes, dict) else None
return GLYPHS.get(icon[4:]) if isinstance(icon, str) and icon.startswith('mdi:') else None
def editor():
"""Picker groups plus what the mockup needs to predict the screen's own icons."""
return {'groups': [{'label': group, 'icons': [{'name': name, 'cp': codepoint, 'label': label} for name, codepoint, label in icons]}
for group, icons in GROUPS],
'defaults': {domain: GLYPHS[name] for domain, name in DEFAULTS.items()}, 'fallback': GLYPHS[FALLBACK],
'builtin': {entity: GLYPHS[name] for entity, name in BUILTIN_TILES.items()},
'weather': {state: GLYPHS[name] for state, name in WEATHER.items()},
'sun': {'above_horizon': GLYPHS['weather-sunset-down'], 'below_horizon': GLYPHS['weather-sunset-up']},
# Glyphs of the direct controls on wide cards, so the mockup previews them.
'controls': {name: GLYPHS[name] for name in CONTROL_GLYPHS}}