Custom icon per tile: 150 MDI icons in firmware and editor

- screen_manager/app/tile_icons.py is the single icon list (12 groups, 20 media
  and music icons); tools/generate_icons.py writes it into the three icon fonts
  of both boards and builds the editor's subset font.
- Firmware 0.2.18 shows o.icon when its fonts carry the glyph, else the domain
  icon. The app sends the chosen icon, or Home Assistant's own mdi icon on auto.
- Tile sheet gets an icon picker with search; the mockup shows the icon the
  screen actually draws.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-13 11:16:57 +02:00
parent efe82c9b09
commit ef0ca8b350
23 changed files with 1371 additions and 498 deletions
+7
View File
@@ -1,3 +1,10 @@
## 0.2.18 (firmware 0.2.18)
- **Eigen icoon per tegel**: in het tegelpaneel staat onder de naam het veld **Icoon**. Kies uit 150 iconen in twaalf groepen (verlichting, ruimtes, klimaat, weer, media en muziek, beveiliging, apparaten, energie, zonwering, tuin en huisdieren, mensen en onderweg, overig), met zoeken op Nederlandse naam, Engelse MDI-naam of groep. De mockup en de kop van het paneel tonen het gekozen icoon direct.
- **Automatisch** gebruikt het icoon dat je in Home Assistant aan de entiteit gaf (`mdi:…`), als het in de set zit; anders het standaardicoon zoals voorheen. De mockup toont nu overal het icoon dat het scherm werkelijk tekent.
- Firmware 0.2.18 bevat alle 150 iconen in de drie icoonfonts van beide borden (CYD +22 KB, Guition +36 KB). Oudere firmware negeert de keuze en houdt het standaardicoon; het paneel meldt dat een update nodig is.
- De weersvoorspelling blijft het icoon van het actuele weer tonen; klok, voorspelling en zonnebaan hebben geen icoonkeuze.
## 0.2.17 (firmware 0.2.17 blijft actueel)
- Het vinkje **Elke nacht automatisch bijwerken** kreeg de algemene invoerstijl (100% breed met padding), waardoor de tekst buiten de zijkolom onder het editorpaneel viel. Het checkboxje heeft nu een vaste maat. Alleen de app verandert.
+24 -5
View File
@@ -6,6 +6,8 @@ import math
import re
import secrets
import tile_icons
DOMAINS = frozenset('light switch input_boolean scene script climate vacuum fan cover sensor binary_sensor input_select select number input_number weather media_player button input_button sun timer person screen'.split())
# Built-in cards without a Home Assistant entity; firmware 0.2.14+ renders them.
BUILTIN = {'screen.clock': 'Klok'}
@@ -14,7 +16,7 @@ WEEKDAYS = ['ma', 'di', 'wo', 'do', 'vr', 'za', 'zo']
REPO = 'https://github.com/MaxGramser/homeassistant_espscreen'
REFS = {'cyd': 'main', 'guition': 'main'}
# Firmware shipped with this app release; screens below it get an update offer.
FIRMWARE_VERSION = '0.2.17'
FIRMWARE_VERSION = '0.2.18'
ATTRS = frozenset('brightness percentage current_position current_temperature temperature current_humidity min_temp max_temp target_temp_step supported_color_modes hvac_modes hs_color color_temp_kelvin min_color_temp_kelvin max_color_temp_kelvin fan_speed_list unit_of_measurement battery_level fan_speed volume_level media_title options min max step temperature_unit supported_features next_rising next_setting finishes_at duration remaining'.split())
@@ -112,10 +114,12 @@ def validate_layout(data):
item = {'entity': tile['entity'], 'name': name.strip()}
if 'options' in tile:
options = tile['options']
if not isinstance(options, dict) or set(options) - {'tap', 'display', 'inline', 'history_hours', 'background', 'size'}:
if not isinstance(options, dict) or set(options) - {'tap', 'display', 'inline', 'history_hours', 'background', 'size', 'icon'}:
raise ValueError('Onbekende tegelinstellingen.')
if 'background' in options and (not isinstance(options['background'],str) or options['background'] not in TILE_BACKGROUNDS):
raise ValueError('Kies een pastel achtergrondkleur uit het palet.')
if 'icon' in options and not (options['icon'] == 'auto' or isinstance(options['icon'], str) and options['icon'] in tile_icons.ICONS):
raise ValueError('Kies een icoon uit de lijst.')
domain = tile['entity'].split('.')[0]
displays = DISPLAYS.get(domain, ('standard', 'watch'))
choices = {'tap': ('auto', 'detail', 'toggle', 'none'), 'display': displays, 'inline': ('none', 'slider'), 'size': ('single', 'wide')}
@@ -186,6 +190,19 @@ def extras(tile, states, forecast=None, tz=None):
return result or None
return None
def tile_icon(tile, attrs):
"""Codepoint the screen shows: the chosen icon, else HA's own mdi icon; None keeps the firmware default."""
choice = tile.get('options', {}).get('icon', 'auto')
return tile_icons.ICONS[choice][0] if choice in tile_icons.ICONS else tile_icons.ha_icon(attrs)
def screen_options(tile, attrs):
"""Stored options on the wire; `icon` travels as the resolved codepoint (firmware 0.2.18+, ignored before)."""
options = {k: v for k, v in tile.get('options', {}).items() if k != 'icon'}
icon = tile_icon(tile, attrs)
if icon:
options['icon'] = icon
return options if options or 'options' in tile else None
def state_message(index, tile, states, extra=None):
if tile['entity'] in BUILTIN:
message = {'v': 1, 'op': 'state', 'i': index, 'entity': tile['entity'],
@@ -210,10 +227,11 @@ def state_message(index, tile, states, extra=None):
limit = 2 if key == 'hs_color' else 4 if key == 'fan_speed_list' else 8
bounded[key] = [short(v, 48) if isinstance(v, str) else v for v in value[:limit]
if isinstance(v, str) or isinstance(v, (float, int)) and math.isfinite(v) and abs(v) <= 1000000]
options = screen_options(tile, attrs)
return {'v': 1, 'op': 'state', 'i': index, 'entity': tile['entity'],
'name': short(tile['name'] or attrs.get('friendly_name') or tile['entity'], 80),
'state': short(state.get('state', 'unavailable'), 160), 'a': bounded,
**({'o': tile['options']} if 'options' in tile else {}), **({'x': extra} if extra else {})}
**({'o': options} if options is not None else {}), **({'x': extra} if extra else {})}
def packets(message, token=None):
raw = json.dumps(message, ensure_ascii=False, separators=(',', ':'), allow_nan=False).encode()
@@ -259,12 +277,13 @@ def discover(registry, states, devices, areas):
continue
entities.append({'id': eid, 'name': state.get('attributes', {}).get('friendly_name') or item.get('name') or item.get('original_name') or eid,
'device': device.get('name_by_user') or device.get('name') or '', 'area': area,
'state': state.get('state', 'unavailable')})
'state': state.get('state', 'unavailable'), 'icon': tile_icons.ha_icon(state.get('attributes'))})
# YAML entities may not have an entity-registry entry.
registered = {e['id'] for e in entities}
for eid, state in states.items():
if entity_id(eid) and eid not in registered and not any(r['entity_id'] == eid for r in registry):
entities.append({'id': eid, 'name': state.get('attributes', {}).get('friendly_name', eid), 'device': '', 'area': '', 'state': state['state']})
entities.append({'id': eid, 'name': state.get('attributes', {}).get('friendly_name', eid), 'device': '', 'area': '', 'state': state['state'],
'icon': tile_icons.ha_icon(state.get('attributes'))})
return screens, sorted(entities, key=lambda e: e['name'].casefold())
def installation_yaml(data):
+5 -3
View File
@@ -10,6 +10,7 @@ import time
from datetime import datetime, timedelta, timezone
import math
from firmware import Firmware
import tile_icons
from updates import Updater
from aiohttp import ClientSession, ClientTimeout, WSMsgType, web
@@ -220,8 +221,9 @@ class Manager:
old_tiles = {t['entity']:t for t in self.layouts.get(inbox,{}).get('tiles',[])}
for tile in layout['tiles']:
old_options=old_tiles.get(tile['entity'],{}).get('options',{})
if 'options' in tile and 'background' not in tile['options'] and 'background' in old_options:
tile['options']['background']=old_options['background']
for key in ('background', 'icon'):
if 'options' in tile and key not in tile['options'] and key in old_options:
tile['options'][key]=old_options[key]
if 'options' not in tile and 'options' in old_tiles.get(tile['entity'],{}):
tile['options'] = old_tiles[tile['entity']]['options'].copy()
known = {e['id'] for e in self.inventory()[1]} | set(BUILTIN)
@@ -361,7 +363,7 @@ def create_app(manager, development=False):
screen['update'] = manager.updates.state_for(screen)
builtin = [{'id': key, 'name': name, 'device': 'Ingebouwd op het scherm', 'area': '', 'state': 'ok'} for key, name in BUILTIN.items()]
return web.json_response({'csrf': csrf, 'connected': manager.ha.online, 'screens': screens, 'entities': entities,
'backgrounds': TILE_BACKGROUNDS, 'builtin': builtin, 'updates': manager.updates.summary()})
'backgrounds': TILE_BACKGROUNDS, 'icons': tile_icons.editor(), 'builtin': builtin, 'updates': manager.updates.summary()})
async def save(request):
manager.save(request.match_info['inbox'], await request.json())
return web.json_response({'saved': True})
+114 -3
View File
@@ -405,6 +405,29 @@ function domainBadge(id) {
badge.style.background = background;
return badge;
}
// The screen draws Material Design Icons; static/tile-icons.woff holds the same glyphs.
const glyph = (cp) => String.fromCodePoint(parseInt(cp, 16));
let iconIndex = { source: null, byName: {} };
function iconNamed(name) {
if (iconIndex.source !== inventory.icons)
iconIndex = { source: inventory.icons, byName: Object.fromEntries((inventory.icons?.groups || []).flatMap((g) => g.icons.map((i) => [i.name, i]))) };
return iconIndex.byName[name];
}
// What the firmware draws without a choice: Home Assistant's own icon, else the domain icon.
function automaticIcon(id) {
const icons = inventory.icons, entity = inventory.entities.find((e) => e.id === id), domain = id.split(".")[0];
if (entity?.icon) return entity.icon;
if (domain === "weather") return icons.weather[entity?.state] || icons.weather.partlycloudy;
if (domain === "sun") return icons.sun[entity?.state] || icons.sun.below_horizon;
return icons.defaults[domain] || icons.fallback;
}
function tileBadge(tile) {
const badge = domainBadge(tile.entity);
if (!inventory.icons) return badge;
badge.textContent = glyph(iconNamed(tile.options?.icon)?.cp || automaticIcon(tile.entity));
badge.classList.add("mdi");
return badge;
}
function tileLimit() {
const version=inventory.screens.find(s=>s.id===selected)?.firmware || "";
const match=/^(\d+)\.(\d+)\.(\d+)$/.exec(version);
@@ -437,7 +460,7 @@ function renderPreview() {
// "Geen": no card on the screen; the mockup keeps a dashed outline as drop target.
if (tile.options?.background === "none") card.classList.add("bare");
if (place.wide) card.classList.add("wide");
card.append(domainBadge(tile.entity), node("strong", name));
card.append(tileBadge(tile), node("strong", name));
card.dataset.index = index;
enableDrag(card, { kind: "tile", index });
card.classList.toggle("chosen", selectedTile === tile.entity);
@@ -487,8 +510,9 @@ function removeTile(index) {
}
// Tile settings open in a sheet above the mockup; every change applies live,
// so the card behind it shows the result while you pick.
let sheetIndex = -1;
let sheetIndex = -1, iconPickerOpen = false;
function openTileSheet(index) {
if (index !== sheetIndex) iconPickerOpen = false;
sheetIndex = index;
selectedTile = layout.tiles[index].entity;
renderTileSheet();
@@ -520,6 +544,89 @@ function segmented(choices, value, onChange) {
}
return group;
}
// A choice applies live like the palette: only pressed states, the summary and the
// badges update, so search text and scroll position survive.
function iconField(tile, onChange) {
const wrap = node("div", undefined, "sheet-field");
wrap.append(node("span", "Icoon"));
const fromHA = Boolean(inventory.entities.find((e) => e.id === tile.entity)?.icon);
const autoLabel = `Automatisch (${fromHA ? "uit Home Assistant" : "standaard"})`;
const summary = node("button", undefined, "icon-current");
summary.type = "button";
summary.setAttribute("aria-expanded", String(iconPickerOpen));
const current = node("span", undefined, "mdi"), text = node("span"), action = node("small", iconPickerOpen ? "Sluiten" : "Wijzigen");
summary.append(current, text, action);
const describe = () => {
const chosen = iconNamed(tile.options?.icon);
current.textContent = glyph(chosen?.cp || automaticIcon(tile.entity));
text.textContent = chosen?.label || autoLabel;
};
describe();
const panel = node("div", undefined, "icon-picker");
panel.hidden = !iconPickerOpen;
summary.onclick = () => {
iconPickerOpen = panel.hidden;
panel.hidden = !iconPickerOpen;
summary.setAttribute("aria-expanded", String(iconPickerOpen));
action.textContent = iconPickerOpen ? "Sluiten" : "Wijzigen";
};
const choice = (name, cp, label, cls = "") => {
const b = node("button", undefined, `icon-choice ${cls}`);
b.type = "button";
b.dataset.icon = name;
b.dataset.search = label.toLocaleLowerCase();
b.title = label;
b.setAttribute("aria-label", `Icoon: ${label}`);
b.setAttribute("aria-pressed", String((tile.options?.icon || "auto") === name));
b.append(node("span", glyph(cp), "mdi"));
b.onclick = () => {
tile.options = { ...tile.options, icon: name };
for (const other of panel.querySelectorAll(".icon-choice")) other.setAttribute("aria-pressed", String(other === b));
describe();
markDirty();
renderPreview();
onChange();
};
return b;
};
const search = node("input");
search.type = "search";
search.placeholder = "Zoek, bijvoorbeeld lamp, muziek of deur";
search.setAttribute("aria-label", "Zoek een icoon");
const auto = choice("auto", automaticIcon(tile.entity), autoLabel, "icon-auto");
auto.append(node("span", autoLabel));
const list = node("div", undefined, "icon-list"), empty = node("p", "Geen icoon gevonden.", "hint");
empty.hidden = true;
const sections = inventory.icons.groups.map((group) => {
const section = node("section"), grid = node("div", undefined, "icon-grid");
for (const icon of group.icons) {
const b = choice(icon.name, icon.cp, icon.label);
b.dataset.search += ` ${icon.name.replaceAll("-", " ")} ${group.label.toLocaleLowerCase()}`;
grid.append(b);
}
section.append(node("small", group.label), grid);
list.append(section);
return section;
});
search.oninput = () => {
const query = search.value.trim().toLocaleLowerCase();
let found = 0;
for (const section of sections) {
let visible = 0;
for (const b of section.querySelectorAll(".icon-choice")) {
b.hidden = !b.dataset.search.includes(query);
if (!b.hidden) visible++;
}
section.hidden = !visible;
found += visible;
}
empty.hidden = found > 0;
};
panel.append(search, auto, list, empty);
wrap.append(summary, panel);
if (!supportsFirmware(0, 2, 18)) wrap.append(node("small", "Het scherm toont een gekozen icoon vanaf firmware 0.2.18.", "icon-hint"));
return wrap;
}
function renderTileSheet() {
const sheet = $("#tile-sheet"), tile = layout.tiles[sheetIndex];
if (!tile) return;
@@ -531,7 +638,8 @@ function renderTileSheet() {
close.type = "button";
close.setAttribute("aria-label", "Sluiten");
close.onclick = closeTileSheet;
head.append(domainBadge(tile.entity), titles, close);
let badge = tileBadge(tile);
head.append(badge, titles, close);
const body = node("div", undefined, "sheet-body");
const nameInput = node("input");
nameInput.value = tile.name;
@@ -539,6 +647,9 @@ function renderTileSheet() {
nameInput.maxLength = 60;
nameInput.oninput = () => { tile.name = nameInput.value; markDirty(); renderPreview(); };
body.append(field("Naam op het scherm", nameInput));
// The clock, forecast and sun path cards draw no tile icon.
if (inventory.icons && domain !== "screen" && !["forecast", "sunpath"].includes(tile.options?.display))
body.append(iconField(tile, () => { const next = tileBadge(tile); badge.replaceWith(next); badge = next; }));
const displays = domain === "screen"
? [["digital", "Digitale klok"], ["analog", "Analoge klok"]]
: [["standard", "Naam en status"], ["watch", "Grote waarde"]];
+22
View File
@@ -957,3 +957,25 @@ pre {
.tile-sheet { width: 100%; max-width: 100%; margin: auto 0 0; border-radius: 20px 20px 0 0; max-height: 90vh; }
.sheet-body { max-height: 52vh; }
}
/* Tile icons: the same Material Design Icons subset the screen's fonts carry. */
@font-face { font-family: "Tile Icons"; src: url("tile-icons.woff") format("woff"); font-display: block; }
.mdi { font-family: "Tile Icons"; font-style: normal; font-weight: normal; line-height: 1; }
.domain-icon.mdi { font-size: 22px; }
.preview-tile .domain-icon.mdi { font-size: 19px; }
.icon-current { display: flex; align-items: center; gap: 10px; width: 100%; padding: 7px 12px 7px 7px; background: white; color: var(--ink); border: 1px solid #cdd5df; border-radius: 12px; font-size: 14px; font-weight: 500; text-align: left; }
.icon-current .mdi { display: grid; place-items: center; width: 36px; height: 36px; flex: none; border-radius: 9px; background: #f3f5f8; font-size: 24px; }
.icon-current > span:nth-child(2) { flex: 1; min-width: 0; }
.icon-current small { color: var(--accent); font-weight: 600; }
.icon-picker { display: grid; gap: 10px; padding: 10px; border: 1px solid var(--line); border-radius: 12px; background: #fafbfc; }
.icon-picker input { font-size: 14px; font-weight: 400; }
.icon-list { display: grid; gap: 12px; max-height: 300px; overflow: auto; padding-right: 2px; }
.icon-list small { display: block; margin-bottom: 6px; }
.icon-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(44px, 1fr)); gap: 6px; }
.icon-choice { display: grid; place-items: center; height: 44px; padding: 0; background: white; color: var(--ink); border: 1px solid var(--line); border-radius: 10px; }
.icon-choice .mdi { font-size: 25px; }
.icon-choice[aria-pressed="true"] { background: var(--soft); color: var(--accent); border-color: #9dbcf5; box-shadow: inset 0 0 0 1px #9dbcf5; }
.icon-choice.icon-auto { display: flex; justify-content: flex-start; gap: 10px; padding: 0 12px; font-size: 13px; font-weight: 500; }
.icon-choice.icon-auto .mdi { font-size: 22px; }
.icon-picker .hint { margin: 0; }
.icon-hint { font-weight: 400; }
Binary file not shown.
+226
View File
@@ -0,0 +1,226 @@
"""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.
"""
# (group, ((MDI name, codepoint, label), ...)) in picker order.
GROUPS = (
('Verlichting', (
('lightbulb', 'F0335', 'Lamp'),
('lightbulb-group', 'F1253', 'Lampen'),
('ceiling-light', 'F0769', 'Plafondlamp'),
('chandelier', 'F1793', 'Kroonluchter'),
('lamp', 'F06B5', 'Tafellamp'),
('floor-lamp', 'F08DD', 'Vloerlamp'),
('desk-lamp', 'F095F', 'Bureaulamp'),
('wall-sconce-flat', 'F091D', 'Wandlamp'),
('light-recessed', 'F179B', 'Inbouwspot'),
('spotlight-beam', 'F04C9', 'Spot'),
('led-strip-variant', 'F1051', 'Ledstrip'),
('outdoor-lamp', 'F1054', 'Buitenlamp'),
)),
('Ruimtes', (
('home', 'F02DC', 'Huis'),
('sofa', 'F04B9', 'Woonkamer'),
('bed', 'F02E3', 'Slaapkamer'),
('silverware-fork-knife', 'F0A70', 'Eetkamer'),
('chef-hat', 'F0B7C', 'Keuken'),
('shower', 'F09A0', 'Douche'),
('bathtub', 'F1818', 'Badkamer'),
('toilet', 'F09AB', 'Toilet'),
('desk', 'F1239', 'Werkkamer'),
('garage', 'F06D9', 'Garage'),
('fireplace', 'F0E2E', 'Open haard'),
)),
('Klimaat', (
('thermometer', 'F050F', 'Temperatuur'),
('thermostat', 'F0393', 'Thermostaat'),
('air-conditioner', 'F001B', 'Airco'),
('radiator', 'F0438', 'Radiator'),
('heat-pump', 'F1A43', 'Warmtepomp'),
('fire', 'F0238', 'Verwarming'),
('snowflake', 'F0717', 'Koelen'),
('water-percent', 'F058E', 'Luchtvochtigheid'),
('fan', 'F0210', 'Ventilator'),
('ceiling-fan', 'F1797', 'Plafondventilator'),
('air-purifier', 'F0D44', 'Luchtreiniger'),
('molecule-co2', 'F07E4', 'CO₂'),
)),
('Weer', (
('weather-sunny', 'F0599', 'Zon'),
('weather-partly-cloudy', 'F0595', 'Half bewolkt'),
('weather-cloudy', 'F0590', 'Bewolkt'),
('weather-rainy', 'F0597', 'Regen'),
('weather-snowy', 'F0598', 'Sneeuw'),
('weather-windy', 'F059D', 'Wind'),
('weather-night', 'F0594', 'Nacht'),
('weather-sunset-up', 'F059C', 'Zonsopkomst'),
)),
('Media en muziek', (
('music-note', 'F0387', 'Muziek'),
('music', 'F075A', 'Muzieknoten'),
('playlist-music', 'F0CB8', 'Afspeellijst'),
('album', 'F0025', 'Album'),
('record-player', 'F099A', 'Platenspeler'),
('spotify', 'F04C7', 'Spotify'),
('radio', 'F0439', 'Radio'),
('podcast', 'F0994', 'Podcast'),
('speaker', 'F04C3', 'Speaker'),
('speaker-multiple', 'F0D38', 'Speakers'),
('soundbar', 'F17DB', 'Soundbar'),
('headphones', 'F02CB', 'Koptelefoon'),
('piano', 'F067D', 'Piano'),
('guitar-acoustic', 'F0771', 'Gitaar'),
('television', 'F0502', 'Televisie'),
('projector', 'F042E', 'Beamer'),
('movie-open', 'F0FCE', 'Film'),
('cast', 'F0118', 'Casten'),
('gamepad-variant', 'F0297', 'Gameconsole'),
('remote-tv', 'F0EC5', 'Afstandsbediening'),
)),
('Beveiliging', (
('lock', 'F033E', 'Slot'),
('lock-open-variant', 'F0FC6', 'Slot open'),
('shield-home', 'F068A', 'Alarm'),
('alarm-light', 'F078F', 'Alarmlicht'),
('bell', 'F009A', 'Bel'),
('doorbell', 'F12E6', 'Deurbel'),
('cctv', 'F07AE', 'Camera'),
('motion-sensor', 'F0D91', 'Bewegingsmelder'),
('door-closed', 'F081B', 'Deur'),
('door-open', 'F081C', 'Deur open'),
('window-closed-variant', 'F11DB', 'Raam'),
('smoke-detector', 'F0392', 'Rookmelder'),
('water-alert', 'F1502', 'Waterlek'),
)),
('Apparaten', (
('washing-machine', 'F072A', 'Wasmachine'),
('tumble-dryer', 'F0917', 'Droger'),
('dishwasher', 'F0AAC', 'Vaatwasser'),
('fridge', 'F0290', 'Koelkast'),
('stove', 'F04DE', 'Fornuis'),
('microwave', 'F0C99', 'Magnetron'),
('toaster-oven', 'F0CD3', 'Oven'),
('coffee-maker', 'F109F', 'Koffie'),
('kettle', 'F05FA', 'Waterkoker'),
('robot-vacuum', 'F070D', 'Robotstofzuiger'),
('robot-mower', 'F11F7', 'Robotmaaier'),
('printer-3d', 'F042B', '3D-printer'),
('power-plug', 'F06A5', 'Stekker'),
('power-socket-eu', 'F07E7', 'Stopcontact'),
('laptop', 'F0322', 'Laptop'),
('router-wireless', 'F0469', 'Router'),
)),
('Energie', (
('flash', 'F0241', 'Stroom'),
('solar-power', 'F0A72', 'Zonne-energie'),
('solar-panel', 'F0D9B', 'Zonnepanelen'),
('home-battery', 'F1901', 'Thuisbatterij'),
('battery-high', 'F12A3', 'Batterij'),
('ev-station', 'F05F1', 'Laadpaal'),
('car-electric', 'F0B6C', 'Elektrische auto'),
('meter-electric', 'F1A57', 'Stroommeter'),
('meter-gas', 'F1A59', 'Gasmeter'),
('water', 'F058C', 'Water'),
('water-boiler', 'F0F92', 'Boiler'),
('gauge', 'F029A', 'Meter'),
)),
('Zonwering', (
('window-shutter', 'F111C', 'Rolluik'),
('window-shutter-open', 'F111E', 'Rolluik open'),
('blinds', 'F00AC', 'Jaloezie'),
('blinds-open', 'F1011', 'Jaloezie open'),
('roller-shade-closed', 'F1A6C', 'Rolgordijn'),
('curtains', 'F1846', 'Gordijnen'),
('curtains-closed', 'F1847', 'Gordijnen dicht'),
('awning-outline', 'F1B88', 'Zonnescherm'),
)),
('Tuin en huisdieren', (
('flower', 'F024A', 'Bloem'),
('tree', 'F0531', 'Boom'),
('sprout', 'F0E66', 'Plant'),
('grass', 'F1510', 'Gazon'),
('sprinkler', 'F105F', 'Sproeier'),
('pool', 'F0606', 'Zwembad'),
('grill', 'F0E45', 'Barbecue'),
('mailbox', 'F06EE', 'Brievenbus'),
('trash-can', 'F0A79', 'Afval'),
('recycle', 'F044C', 'Recycling'),
('dog', 'F0A43', 'Hond'),
('cat', 'F011B', 'Kat'),
)),
('Mensen en onderweg', (
('account', 'F0004', 'Persoon'),
('account-group', 'F0849', 'Gezin'),
('account-child', 'F0A89', 'Kind'),
('home-account', 'F0826', 'Thuis'),
('sleep', 'F04B2', 'Slapen'),
('run', 'F070E', 'Sporten'),
('briefcase', 'F00D6', 'Werk'),
('car', 'F010B', 'Auto'),
('bike', 'F00A3', 'Fiets'),
('airplane', 'F001D', 'Vakantie'),
('map-marker', 'F034E', 'Locatie'),
)),
('Overig', (
('power', 'F0425', 'Aan/uit'),
('toggle-switch', 'F0521', 'Schakelaar'),
('gesture-tap-button', 'F12A8', 'Knop'),
('play', 'F040A', 'Start'),
('script-text', 'F0BC2', 'Script'),
('palette', 'F03D8', 'Scène'),
('alarm', 'F0020', 'Wekker'),
('clock-outline', 'F0150', 'Klok'),
('timer-outline', 'F051B', 'Timer'),
('calendar', 'F00ED', 'Agenda'),
('bell-ring', 'F009E', 'Melding'),
('heart', 'F02D1', 'Favoriet'),
('alert-outline', 'F002A', 'Waarschuwing'),
('cog', 'F0493', 'Instellingen'),
('broom', 'F00E2', 'Schoonmaken'),
)),
)
# Glyphs the firmware draws itself (weather conditions, sun, checkmark) that the picker does not offer.
FIXED = (
('alert-circle-outline', 'F05D6'),
('check', 'F012C'),
('weather-fog', 'F0591'),
('weather-hail', 'F0592'),
('weather-lightning', 'F0593'),
('weather-lightning-rainy', 'F067E'),
('weather-pouring', 'F0596'),
('weather-snowy-rainy', 'F067F'),
('weather-sunset-down', 'F059B'),
)
# 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)}
# 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', 'screen': 'clock-outline'}
FALLBACK = 'power'
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'}
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],
'weather': {state: GLYPHS[name] for state, name in WEATHER.items()},
'sun': {'above_horizon': GLYPHS['weather-sunset-down'], 'below_horizon': GLYPHS['weather-sunset-up']}}
+1 -1
View File
@@ -1,5 +1,5 @@
name: ESP Screen Manager
version: "0.2.17"
version: "0.2.18"
slug: esp_screen_manager
description: Kies tegels voor je CYD en Guition touchscreen, zonder opnieuw flashen.
url: https://github.com/MaxGramser/homeassistant_espscreen