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>
This commit is contained in:
Max Gramser
2026-09-18 08:44:44 +02:00
parent 00c9a4e4f3
commit bb25353089
29 changed files with 948 additions and 128 deletions
+10
View File
@@ -1,3 +1,13 @@
## 0.2.74 (firmware 0.2.62)
Forty-eight tiles, a tile over the whole page, and tiles that go to a page.
- **Up to 48 tiles per screen**, one for every slot of the eight pages (twenty before). A screen only pays for the tiles it has: the firmware keeps a list as long as the layout instead of a fixed twenty, and on a Guition that list lives in PSRAM.
- **Full page.** A tile's size can now be *Full page*: it takes all six slots of a page and is one big button, so a screen on the wall next to the door switches the light when you push anywhere on it, without looking. While the light is on the whole card lights up in the state colour (amber for a lamp, purple for a scene, blue for a blind), so you see from across the room whether it is on. Holding it still opens the card. A small slider, direct controls or a graph sit at the bottom of the page, big enough for a thumb; the room above them is still the button. The clock, the weather (now with the next hours under the days) and the sun path fill the page. A tile you make full-page keeps its page: the other tiles there move to the first free spots after it.
- **Go to page.** A new built-in tile, *Go to page*, opens the page you choose: an arrow (or an icon of your own), its name and the page number, with a chevron like the rows in Home Assistant's settings. Put a full-page light switch on page 1 and a menu of *Heating*, *Blinds* and *Vacuum* on page 2, each with its own page.
- Claude in Home Assistant can do the same: `size: full` on a tile, and `screen.page` with `to_page` for a navigation tile. The screen's sensor reports both.
- Needs firmware 0.2.62: press **Update** on the screen. Includes everything from 0.2.73.
## 0.2.73 (firmware 0.2.61)
A new editor, built as a Vue app, with the same firmware.
+1 -1
View File
@@ -1,6 +1,6 @@
# ESP Screen Manager
Find your Home Assistant entities, arrange up to twenty tiles, and push changes
Find your Home Assistant entities, arrange up to 48 tiles, and push changes
to a CYD or Guition screen without reflashing. Includes installation help,
a unique per-device YAML, and support for multiple screens.
+9 -5
View File
@@ -14,9 +14,11 @@ import zipfile
import tile_icons
from core import (ALERT_CAMERA_FIELD, ALERT_ENDINGS, ALERT_EVENT, ALERT_FALLBACK_ICON, ALERT_FIELDS, ALERT_LIMITS, ALERT_MAX_TIMEOUT,
ALERT_MIN_FIRMWARE, ALERT_SUGGESTED_ICONS, AUTO_STANDBY_MIN_FIRMWARE, BROADCAST_DISMISS, BROADCAST_SHOW,
CONTROLS, DISPLAYS, MAX_PAGES, SETTINGS_PAGE_MIN_FIRMWARE, SLOTS_PER_PAGE, TILE_BACKGROUNDS,
CONTROLS, DISPLAYS, FULL_PAGE_MIN_FIRMWARE, MAX_PAGES, MAX_TILES, SETTINGS_PAGE_MIN_FIRMWARE, SLOTS_PER_PAGE, TILE_BACKGROUNDS,
TILE_EVENTS, TILE_RESULT_EVENT, WAKE_SLEEP_MIN_FIRMWARE, SETTING_ENTITIES_MIN_FIRMWARE,
DARK_MODE_MIN_FIRMWARE)
# Full-page tiles, navigation tiles and forty-eight tiles per screen.
FULL_PAGE_VERSION = '.'.join(str(part) for part in FULL_PAGE_MIN_FIRMWARE)
NAME = 'esp-screens'
# claude.ai accepts at most 200 characters; Claude Code picks the skill by this sentence.
@@ -65,7 +67,7 @@ An alert is a card over the whole screen with an icon, a title, a subtitle and o
## Tiles on a screen
A screen shows tiles: two columns and {SLOTS_PER_PAGE // 2} rows per page, at most {MAX_PAGES} pages and twenty tiles. A tile is single or double-width; a double-width one starts in the left column and takes two spots.
A screen shows tiles: two columns and {SLOTS_PER_PAGE // 2} rows per page, at most {MAX_PAGES} pages and {MAX_TILES} tiles (twenty on firmware before {FULL_PAGE_VERSION}). A tile is single, double-width or full-page: a double-width one starts in the left column and takes two spots, a full-page one takes a whole page of its own and is one big button, so someone can switch a light by pushing anywhere on the screen without looking. A `controls` choice, a small slider or a graph sits at the bottom of that page (a full-page tile shows no control unless you choose one).
Fire one of these events and ESP Screens changes that screen and sends it right away, the same way its own editor does.
@@ -90,7 +92,7 @@ actions:
| `name` | A name of your own on the tile; leave it out to keep Home Assistant's. |
| `page` | Page, counted from 1. Without a spot the tile takes the first free one on that page. |
| `row`, `column` | An exact spot on that page: row 1 to {SLOTS_PER_PAGE // 2}, column `left` or `right`. |
| `size` | `single` or `wide`. |
| `size` | `single`, `wide` or `full` (the whole page; firmware {FULL_PAGE_VERSION} or newer). |
| `controls` | What you can operate on the tile itself (see below). |
| `display` | How the tile draws itself (see below). |
| `icon`, `color` | An icon from the list further down, and one of the pastel colors. |
@@ -100,6 +102,8 @@ actions:
A tile with a control, a forecast or a sun path is drawn double-width on its own; you don't have to ask for that.
A navigation tile goes to another page: add the entity `screen.page_3` (for page 3, `screen.page_1` to `screen.page_{MAX_PAGES}`) with a `name` such as "Heating"; it shows an arrow (or an `icon`), its name and the page number, and a tap opens that page. Handy as a menu on page 2 when page 1 is one full-page light switch. One such tile per page it goes to; it is single or double width, never full-page.
### What a tile can do, per kind of entity
| Entity | `controls` |
@@ -114,7 +118,7 @@ Everything else shows its name and state, and opens a card of its own on a long
### Reading a screen first
Every screen also publishes what it shows, as `sensor.esp_screens_<device name>`: the state is the number of tiles, and the attributes hold `title`, `pages` and `tiles` with `entity`, `name`, `page`, `row`, `column`, `size`, `controls` and `display` per tile. Read that before moving things around, so you know what is already there and where.
Every screen also publishes what it shows, as `sensor.esp_screens_<device name>`: the state is the number of tiles, and the attributes hold `title`, `pages` and `tiles` with `entity`, `name`, `page`, `row`, `column`, `size`, `controls` and `display` per tile (and `to_page` for a `screen.page_<n>` tile). Read that before moving things around, so you know what is already there and where.
Ordering a page means naming the tiles that are on it, in the order you want:
@@ -131,7 +135,7 @@ A tile from another page has to be moved there first (`esp_screens_move_tile` wi
### What comes back
ESP Screens answers every event with `{TILE_RESULT_EVENT}`, carrying `ok`, the `screen`, the `entity` and, when it refused, an `error` that says why ("No screen called ...", "That spot is taken by ...", "This screen already has twenty tiles"). The add-on log says the same. Nothing changes on a refused event.
ESP Screens answers every event with `{TILE_RESULT_EVENT}`, carrying `ok`, the `screen`, the `entity` and, when it refused, an `error` that says why ("No screen called ...", "That spot is taken by ...", "This screen already has {MAX_TILES} tiles", "No page is empty; a full-page tile needs a page of its own"). The add-on log says the same. Nothing changes on a refused event.
### How to work
+111 -45
View File
@@ -11,16 +11,26 @@ 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 camera image'.split())
# Built-in cards without a Home Assistant entity; firmware 0.2.14+ renders them.
BUILTIN = {'screen.clock': 'Clock', 'screen.settings': 'Settings'}
BUILTIN = {'screen.clock': 'Clock', 'screen.settings': 'Settings', **{f'screen.page_{n}': f'Go to page {n}' for n in range(1, 9)}}
# A navigation tile (firmware 0.2.62+): screen.page_<n> goes to page n; one per page it goes to, so an entity still appears once.
PAGE_TILE = 'screen.page_'
def page_target(entity):
"""The page a navigation tile opens, counted from one; 0 for any other entity."""
return int(entity[len(PAGE_TILE):]) if isinstance(entity, str) and entity in BUILTIN and entity.startswith(PAGE_TILE) else 0
NEW_DOMAINS = frozenset('sun timer person screen'.split())
# A camera or an image entity opens full screen on a Guition with firmware 0.2.57+ (camera_feed.py).
CAMERA_DOMAINS = frozenset(('camera', 'image'))
CAMERA_MIN_FIRMWARE = (0, 2, 57)
# Forty-eight tiles (one per slot), a tile over the whole page and the screen.page tile (firmware 0.2.62+).
MAX_TILES = 48
LEGACY_MAX_TILES = 20
FULL_PAGE_MIN_FIRMWARE = (0, 2, 62)
WEEKDAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
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.61'
FIRMWARE_VERSION = '0.2.62'
# The Auto standby switch a screen offers Home Assistant automations.
AUTO_STANDBY_MIN_FIRMWARE = '0.2.41'
# The settings page the screen opens itself, and the screen.settings tile that opens it.
@@ -63,22 +73,44 @@ SLOTS_PER_PAGE = 6
MAX_PAGES = 8
MAX_SLOTS = MAX_PAGES * SLOTS_PER_PAGE
def is_wide(tile):
return tile.get('options', {}).get('size') == 'wide'
TILE_SIZES_ON_SCREEN = ('single', 'wide', 'full')
def footprint(slot, wide):
return (slot, slot + 1) if wide else (slot,)
def tile_size(tile):
"""'single', 'wide' (a row) or 'full' (the whole page, firmware 0.2.62+)."""
size = tile.get('options', {}).get('size', 'single')
return size if size in TILE_SIZES_ON_SCREEN else 'single'
def is_wide(tile):
"""Double width or the whole page: the card spans both columns."""
return tile_size(tile) != 'single'
def is_full(tile):
return tile_size(tile) == 'full'
def cells_of(size):
return SLOTS_PER_PAGE if size == 'full' else 2 if size in ('wide', True) else 1
def page_start(slot):
return slot - slot % SLOTS_PER_PAGE
def footprint(slot, size):
"""The cells a tile of `size` takes from `slot` (True still means wide)."""
if size == 'full':
return tuple(range(page_start(slot), page_start(slot) + SLOTS_PER_PAGE))
return (slot, slot + 1) if size in ('wide', True) else (slot,)
def pack_slots(tiles):
"""In-order packing, the rule before explicit positions and what firmware without
`slots` still does: fill left to right, a wide card starts a new row."""
`slots` still does: fill left to right, a wide card starts a new row, a full one a new page."""
position, slots = 0, []
for tile in tiles:
wide = is_wide(tile)
if wide and position % 2:
size = tile_size(tile)
if size == 'full' and position % SLOTS_PER_PAGE:
position += SLOTS_PER_PAGE - position % SLOTS_PER_PAGE
elif size == 'wide' and position % 2:
position += 1
slots.append(position)
position += 2 if wide else 1
position += cells_of(size)
return slots
def has_gaps(tiles):
@@ -118,9 +150,10 @@ def resolve_controls(tile):
"""Control set a card shows on the screen, or None: only wide cards in the standard layout have room for one."""
options = tile.get('options', {})
domain = tile['entity'].split('.')[0]
if domain not in CONTROLS or options.get('size') != 'wide' or options.get('display', 'standard') != 'standard' or options.get('inline') == 'slider':
if domain not in CONTROLS or options.get('size') not in ('wide', 'full') or options.get('display', 'standard') != 'standard' or options.get('inline') == 'slider':
return None
choice = options.get('controls', CONTROLS[domain][0][0])
# A full-page card is one big button unless a control was chosen for it; a wide card shows its usual one.
choice = options.get('controls', 'none' if options.get('size') == 'full' else CONTROLS[domain][0][0])
return None if choice == 'none' else choice
# Diagnostic entities every ESP Screens firmware exposes; the manager watches them for screens.
@@ -353,6 +386,8 @@ def validate_header(data):
def min_firmware(layout):
"""Oldest firmware that still accepts this layout; None when any version works."""
if len(layout['tiles']) > LEGACY_MAX_TILES or any(is_full(t) or page_target(t['entity']) for t in layout['tiles']):
return FULL_PAGE_MIN_FIRMWARE
if any(t['entity'].split('.')[0] in CAMERA_DOMAINS for t in layout['tiles']):
return CAMERA_MIN_FIRMWARE
if any(t['entity'] == 'screen.settings' for t in layout['tiles']):
@@ -381,7 +416,8 @@ TILE_RESULT_EVENT = 'esp_screens_tile_result'
TILE_EVENT_OPTIONS = {'size': 'size', 'controls': 'controls', 'display': 'display', 'icon': 'icon',
'color': 'background', 'background': 'background', 'tap': 'tap', 'inline': 'inline',
'history_hours': 'history_hours'}
TILE_SIZES = {'wide': 'wide', 'double': 'wide', 'large': 'wide', 'big': 'wide',
TILE_SIZES = {'full': 'full', 'fullscreen': 'full', 'full screen': 'full', 'full-screen': 'full', 'page': 'full', 'whole page': 'full',
'wide': 'wide', 'double': 'wide', 'large': 'wide', 'big': 'wide',
'single': 'single', 'small': 'single', 'normal': 'single'}
def loose(text):
@@ -412,34 +448,43 @@ def occupied_cells(tiles, skip=None):
for tile in tiles:
if tile is skip or 'slot' not in tile:
continue
cells.update(footprint(tile['slot'], is_wide(tile)))
cells.update(footprint(tile['slot'], tile_size(tile)))
return cells
def free_slot(tiles, wide, page=None, skip=None):
"""The first cell a tile of this width fits in, on `page` or anywhere; None when there is no room."""
def free_slot(tiles, size, page=None, skip=None):
"""The first cell a tile of this size fits in, on `page` or anywhere; None when there is no room."""
cells = occupied_cells(tiles, skip)
size = 'wide' if size is True else size
first = 0 if page is None else page * SLOTS_PER_PAGE
last = MAX_SLOTS if page is None else min(MAX_SLOTS, first + SLOTS_PER_PAGE)
for slot in range(first, last):
if wide and (slot % 2 or slot + 1 >= last):
if size == 'full' and slot % SLOTS_PER_PAGE:
continue
if not set(footprint(slot, wide)) & cells:
if size == 'wide' and (slot % 2 or slot + 1 >= last):
continue
if not set(footprint(slot, size)) & cells:
return slot
return None
def place_tile(tile, tiles, page=None, slot=None):
"""Give a tile its cell: the one asked for when it is free, else the first free one (on `page`)."""
wide = is_wide(tile)
size = tile_size(tile)
if slot is not None:
if wide and slot % 2:
if size == 'full':
slot = page_start(slot)
elif size == 'wide' and slot % 2:
slot -= 1
taken = next((t for t in tiles if t is not tile and slot in footprint(t.get('slot', -9), is_wide(t))), None)
wanted = set(footprint(slot, size))
taken = next((t for t in tiles if t is not tile and 'slot' in t and wanted & set(footprint(t['slot'], tile_size(t)))), None)
if taken:
raise ValueError(f"That spot is taken by {taken['entity']}; give another spot or move that one first.")
tile['slot'] = slot
return
free = free_slot(tiles, wide, page, skip=tile)
free = free_slot(tiles, size, page, skip=tile)
if free is None:
if size == 'full':
raise ValueError(f'Page {page + 1} is not empty; a full-page tile needs a page of its own.' if page is not None
else 'No page is empty; a full-page tile needs a page of its own.')
raise ValueError(f'Page {page + 1} is full.' if page is not None else 'This screen has no room left.')
tile['slot'] = free
@@ -462,7 +507,7 @@ def tile_options(data, current=None):
options['action'] = {'action': str(data['action']).strip(), **({'data': data['data']} if isinstance(data.get('data'), dict) and data['data'] else {})}
if data.get('tap') in (None, ''):
options['tap'] = 'action'
if options.get('controls', 'none') != 'none' or options.get('display') in WIDE_ONLY:
if (options.get('controls', 'none') != 'none' or options.get('display') in WIDE_ONLY) and options.get('size') != 'full':
options['size'] = 'wide'
return {key: value for key, value in options.items() if value not in (None, '')}
@@ -501,13 +546,15 @@ def pack_page(tiles, page):
position = page * SLOTS_PER_PAGE
last = position + SLOTS_PER_PAGE
for tile in tiles:
wide = is_wide(tile)
if wide and position % 2:
size = tile_size(tile)
if size == 'full' and (position != page * SLOTS_PER_PAGE or len(tiles) > 1):
raise ValueError(f'A full-page tile takes all of page {page + 1}; nothing else fits there.')
if size == 'wide' and position % 2:
position += 1
if position + (2 if wide else 1) > last:
if position + cells_of(size) > last:
raise ValueError(f'That does not fit on page {page + 1}; a double-width tile takes two spots.')
tile['slot'] = position
position += 2 if wide else 1
position += cells_of(size)
def apply_tile_event(layout, action, data):
"""The layout after one tile event. Raises ValueError with the sentence the log and the answer show."""
@@ -559,7 +606,7 @@ def apply_tile_event(layout, action, data):
else:
if not entity_id(entity) and entity not in BUILTIN:
raise ValueError(f'{entity} cannot go on a screen.')
was_wide, had_slot = bool(found) and is_wide(found), (found or {}).get('slot')
was_size, had_slot = tile_size(found) if found else 'single', (found or {}).get('slot')
options = tile_options(data, (found or {}).get('options'))
tile = found or {'entity': entity, 'name': ''}
if data.get('name') not in (None, ''):
@@ -569,17 +616,25 @@ def apply_tile_event(layout, action, data):
elif found:
tile.pop('options', None)
if found is None:
if len(tiles) >= 20:
raise ValueError('This screen already has twenty tiles; remove one first.')
if len(tiles) >= MAX_TILES:
raise ValueError(f'This screen already has {MAX_TILES} tiles; remove one first.')
tiles.append(tile)
wide = is_wide(tile)
size = tile_size(tile)
move = found is None or had_slot is None or page is not None or slot is not None
if not move and wide != was_wide:
# A tile that just grew keeps its spot when the cell beside it is free.
move = (wide and had_slot % 2) or bool(set(footprint(had_slot, wide)) & occupied_cells([t for t in tiles if t is not tile]))
if not move and size != was_size:
# A tile that just grew keeps its spot when the cells it needs are free.
start = page_start(had_slot) if size == 'full' else had_slot - had_slot % 2 if size == 'wide' else had_slot
move = start != had_slot or bool(set(footprint(start, size)) & occupied_cells([t for t in tiles if t is not tile]))
if move:
tile.pop('slot', None)
place_tile(tile, tiles, page, slot)
if size == 'full' and had_slot is not None and page is None and slot is None:
# A tile that grew to the whole page stays on its page when the page is otherwise empty.
try:
place_tile(tile, tiles, had_slot // SLOTS_PER_PAGE)
except ValueError:
place_tile(tile, tiles)
else:
place_tile(tile, tiles, page, slot)
result['tiles'] = tiles
return result
@@ -594,7 +649,8 @@ def layout_snapshot(screen, layout):
'column': 'right' if slot % 2 else 'left', 'slot': slot,
'size': options.get('size', 'single'), 'controls': options.get('controls', ''),
'display': options.get('display', 'standard'), 'tap': options.get('tap', 'auto'),
**({'action': options['action']} if options.get('tap') == 'action' and 'action' in options else {})})
**({'action': options['action']} if options.get('tap') == 'action' and 'action' in options else {}),
**({'to_page': page_target(tile['entity'])} if page_target(tile['entity']) else {})})
return {'screen': screen.get('name', ''), 'node': screen.get('node') or '', 'title': layout.get('title', ''),
'pages': max([tile['page'] for tile in tiles], default=1), 'tiles': tiles}
@@ -653,8 +709,8 @@ def validate_layout(data, stored=False):
title, tiles = data.get('title'), data.get('tiles')
if not isinstance(title, str) or not title.strip() or len(title.encode()) > 96:
raise ValueError('Give the screen a title of at most 96 bytes.')
if not isinstance(tiles, list) or len(tiles) > 20:
raise ValueError('Choose at most 20 tiles.')
if not isinstance(tiles, list) or len(tiles) > MAX_TILES:
raise ValueError(f'Choose at most {MAX_TILES} tiles.')
clean, seen = [], set()
for tile in tiles:
if not isinstance(tile, dict) or not entity_id(tile.get('entity')):
@@ -678,18 +734,23 @@ def validate_layout(data, stored=False):
options = tile['options']
if not isinstance(options, dict) or set(options) - {'tap', 'display', 'inline', 'history_hours', 'background', 'size', 'icon', 'controls', 'action'}:
raise ValueError('Unknown tile settings.')
# A navigation tile (screen.page_<n>, firmware 0.2.62+) has a name, an icon, a colour and a width; never the page.
if page_target(tile['entity']):
if options.get('size') == 'full':
raise ValueError('A navigation tile is single or double width.')
options = {k: v for k, v in options.items() if k not in ('display', 'inline', 'controls', 'history_hours')}
if 'background' in options and (not isinstance(options['background'],str) or options['background'] not in TILE_BACKGROUNDS):
raise ValueError('Choose a pastel background color from the palette.')
if 'icon' in options and not (options['icon'] == 'auto' or isinstance(options['icon'], str) and options['icon'] in tile_icons.ICONS):
raise ValueError('Choose an icon from the list.')
domain = tile['entity'].split('.')[0]
displays = DISPLAYS.get(domain, ('standard', 'watch'))
choices = {'tap': ('auto', 'detail', 'toggle', 'none', 'action'), 'display': displays, 'inline': ('none', 'slider'), 'size': ('single', 'wide')}
choices = {'tap': ('auto', 'detail', 'toggle', 'none', 'action'), 'display': displays, 'inline': ('none', 'slider'), 'size': TILE_SIZES_ON_SCREEN}
for key, allowed in choices.items():
if key in options and options[key] not in allowed:
raise ValueError('Invalid tile setting: ' + key)
# The five-day strip and the sun path only fit a double-width card.
if options.get('display') in WIDE_ONLY:
# The five-day strip and the sun path only fit a double-width card (or the whole page).
if options.get('display') in WIDE_ONLY and options.get('size') != 'full':
options = {**options, 'size': 'wide'}
# On / off sends <domain>.toggle. Whether Home Assistant offers that for the entity is checked when saving
# (Manager.check_supported, app 0.2.67); the built-in cards have nothing to switch.
@@ -721,9 +782,12 @@ def validate_layout(data, stored=False):
for item, slot in zip(clean, given):
if type(slot) is not int or not 0 <= slot < MAX_SLOTS:
raise ValueError('Invalid tile position; refresh the management page.')
if is_wide(item) and slot % 2:
size = tile_size(item)
if size == 'full' and slot % SLOTS_PER_PAGE:
raise ValueError('A full-page tile starts at the top of its page.')
if size == 'wide' and slot % 2:
raise ValueError('A double-width tile starts in the left column.')
for cell in footprint(slot, is_wide(item)):
for cell in footprint(slot, size):
if cell in occupied:
raise ValueError('Two tiles are in the same spot.')
occupied.add(cell)
@@ -1040,8 +1104,10 @@ def state_message(index, tile, states, extra=None, precision=None, entry=None):
if tile['entity'] in BUILTIN:
message = {'v': 1, 'op': 'state', 'i': index, 'entity': tile['entity'],
'name': short(tile['name'] or BUILTIN[tile['entity']], 80), 'state': 'ok', 'a': {}}
if 'options' in tile:
message['o'] = tile['options']
# The same wire form as any tile, so a chosen icon travels as its codepoint (app 0.2.74+).
options = screen_options(tile, {})
if options is not None:
message['o'] = options
return message
state = states.get(tile['entity'], {})
attrs = state.get('attributes', {})
+9 -1
View File
@@ -371,8 +371,16 @@ DEFAULTS = {'light': 'lightbulb', 'climate': 'air-conditioner', 'vacuum': 'robot
'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'}
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')
+1 -1
View File
@@ -1,5 +1,5 @@
name: ESP Screen Manager
version: "0.2.73"
version: "0.2.74"
slug: esp_screen_manager
description: Choose tiles for your CYD and Guition touchscreen, without reflashing.
url: https://github.com/MaxGramser/homeassistant_espscreen