Show the album cover on a media tile, in the same strip as the live camera pictures (0.2.92)
A single or double-width media player tile takes Display -> Album cover on a Guition: the cover of what plays in the icon's place, like Home Assistant's own tile. It rides in the page's strip of 0.2.91: the request and the answer are the same, a media player is simply among the tiles. The app fetches a player's picture only when its address changes; a page with a camera loads at the camera's pace with the cover reused, a page of media tiles alone loads once, and a new track (the picture's mark in the state, part of the page's wish) asks again at once. A player without a picture keeps its icon and is not asked for; the double-width tile keeps its controls; the tile over the whole page keeps the card's big cover. Firmware 0.2.78. Host harness 31 checks (docs/TEST_RESULTS_0292.md). CYD firmware 1,633,760 B, 89.0 %. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,10 @@ COVER_RADIUS_SHARE = 12 # the corner is a twelfth of the size, at least 4 px (m
|
||||
# the corners rounded over that tile's own colour. A camera is fetched again only when its tile's pace (15 or 30 s)
|
||||
# has passed since the last fetch, so a page loading every 15 s leaves a 30 s camera alone in between.
|
||||
LIVE_MIN_FIRMWARE = (0, 2, 77)
|
||||
# A media tile's album cover in the same strip (app 0.2.92, firmware 0.2.78): "display": "cover" on a single or wide
|
||||
# media tile. A cover is fetched when the player's picture address changes, never on a clock; the screen asks for the
|
||||
# page again when the picture's mark in the player's state changes.
|
||||
COVER_TILE_MIN_FIRMWARE = (0, 2, 78)
|
||||
LIVE_SIZES = (24, 160) # a square's side, in pixels
|
||||
LIVE_MAX_TILES = 6 # one page
|
||||
LIVE_REFRESH = (15, 30) # the paces a tile may choose, in seconds
|
||||
@@ -103,7 +107,7 @@ def live_request(request):
|
||||
return None
|
||||
if not 1 <= len(entities) <= LIVE_MAX_TILES or len(colours) != len(entities) or len(set(entities)) != len(entities):
|
||||
return None
|
||||
if not LIVE_SIZES[0] <= size <= LIVE_SIZES[1] or not all(supported(e) for e in entities) or not all(re.fullmatch(r'[0-9A-Fa-f]{6}', c) for c in colours):
|
||||
if not LIVE_SIZES[0] <= size <= LIVE_SIZES[1] or not all(supported(e) or cover_supported(e) for e in entities) or not all(re.fullmatch(r'[0-9A-Fa-f]{6}', c) for c in colours):
|
||||
return None
|
||||
return entities, size, [int(c, 16) for c in colours]
|
||||
|
||||
@@ -352,6 +356,8 @@ class CameraFeed:
|
||||
# serving it starts the next fetch of the cameras whose pace has passed: a 15 s page fetches a 30 s camera every
|
||||
# other load. The first load waits for the cameras that have no snapshot yet, all at once.
|
||||
async def live_one(self, entity, pace, wait):
|
||||
if cover_supported(entity):
|
||||
return await self.cover_raw(entity, wait)
|
||||
watch = self.watch(entity)
|
||||
if watch.raw is None:
|
||||
self.refresh(entity, watch)
|
||||
@@ -408,9 +414,9 @@ class CameraFeed:
|
||||
if watch.failures in (1, 30):
|
||||
LOG.info('No cover from %s (%s)', entity, type(error).__name__)
|
||||
|
||||
async def cover(self, entity, size, background, wait=FIRST_FRAME_SECONDS):
|
||||
"""(etag, BMP) of the player's cover at `size` with `background` behind its corners, or None when the player
|
||||
shows no picture or it cannot be fetched."""
|
||||
async def cover_raw(self, entity, wait=FIRST_FRAME_SECONDS):
|
||||
"""The player's picture as Home Assistant hands it out, fetched again only when its address changed; None when
|
||||
the player shows no picture or it cannot be fetched (yet)."""
|
||||
if self.fetch_cover is None or self.picture is None:
|
||||
return None
|
||||
picture = self.picture(entity)
|
||||
@@ -429,6 +435,14 @@ class CameraFeed:
|
||||
return None
|
||||
if watch.picture != picture or watch.raw is None:
|
||||
return None
|
||||
return watch.raw
|
||||
|
||||
async def cover(self, entity, size, background, wait=FIRST_FRAME_SECONDS):
|
||||
"""(etag, BMP) of the player's cover at `size` with `background` behind its corners, or None when the player
|
||||
shows no picture or it cannot be fetched."""
|
||||
if await self.cover_raw(entity, wait) is None:
|
||||
return None
|
||||
watch = self.watch(entity)
|
||||
key = ('cover', size, background)
|
||||
cached = watch.frames.get(key)
|
||||
if cached is None or cached[0] != watch.digest:
|
||||
|
||||
@@ -15,12 +15,13 @@ from i18n import t
|
||||
import tile_icons
|
||||
from core import (ALERT_ACTION_FIELD, 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, FULL_PAGE_MIN_FIRMWARE, LIVE_MIN_FIRMWARE, MAX_PAGES, MAX_SLOTS, MAX_TILES, PAGE_TILE_REPEAT_MIN_FIRMWARE,
|
||||
CONTROLS, COVER_TILE_MIN_FIRMWARE, DISPLAYS, FULL_PAGE_MIN_FIRMWARE, LIVE_MIN_FIRMWARE, MAX_PAGES, MAX_SLOTS, MAX_TILES, PAGE_TILE_REPEAT_MIN_FIRMWARE,
|
||||
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, PAGE_BUTTONS_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)
|
||||
LIVE_VERSION = '.'.join(str(part) for part in LIVE_MIN_FIRMWARE)
|
||||
COVER_TILE_VERSION = '.'.join(str(part) for part in COVER_TILE_MIN_FIRMWARE)
|
||||
# The same navigation tile on several pages (app 0.2.78).
|
||||
PAGE_TILE_REPEAT_VERSION = '.'.join(str(part) for part in PAGE_TILE_REPEAT_MIN_FIRMWARE)
|
||||
|
||||
@@ -100,7 +101,7 @@ actions:
|
||||
| `from_page`, `from_slot` | Only for `esp_screens_move_tile`: which copy of a navigation tile that is on several pages to move (see below). |
|
||||
| `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). A camera or image tile on a Guition takes `live` (firmware {LIVE_VERSION} or newer): a small live picture in the icon's place, with `refresh` 15 or 30 (seconds). |
|
||||
| `display` | How the tile draws itself (see below). On a Guition a camera or image tile takes `live` (firmware {LIVE_VERSION} or newer): a small live picture in the icon's place, with `refresh` 15 or 30 (seconds); a media player tile takes `cover` (firmware {COVER_TILE_VERSION} or newer): the album cover of what plays in the icon's place. |
|
||||
| `icon`, `color` | An icon from the list further down, and one of the pastel colors. |
|
||||
| `tap` | What a tap does: `auto`, `detail` (open the card), `toggle`, `action` or `none`. `toggle` works for anything Home Assistant can toggle for that entity, such as a light, a cover (open, close, or stop while it moves) or a speaker that turns on and off. Holding the tile still opens its card. |
|
||||
| `action`, `data` | Perform action: an action Home Assistant offers for the tile's own entity, such as `cover.set_cover_position`, with `data` for its fields (`position: 50`). Giving `action` sets `tap` to `action`. The target is always the tile's entity. Only actions and fields Home Assistant lists for that entity are accepted; the answer says what is missing. |
|
||||
|
||||
@@ -37,6 +37,8 @@ CAMERA_MIN_FIRMWARE = (0, 2, 57)
|
||||
# A live picture on a camera tile ("display": "live", app 0.2.91): firmware from here asks for the page's strip.
|
||||
LIVE_MIN_FIRMWARE = (0, 2, 77)
|
||||
LIVE_REFRESH = (15, 30) # the paces a live tile may choose, in seconds; the first is the default
|
||||
# A media tile's album cover in the icon's place ("display": "cover", app 0.2.92): the same strip, firmware from here.
|
||||
COVER_TILE_MIN_FIRMWARE = (0, 2, 78)
|
||||
# The media card with its cover (app 0.2.77): firmware from here draws it and asks for the cover.
|
||||
COVER_MIN_FIRMWARE = (0, 2, 64)
|
||||
# Forty-eight tiles (one per slot), a tile over the whole page and the screen.page tile (firmware 0.2.62+).
|
||||
@@ -49,7 +51,7 @@ FIRST_MAX_TILES = 10
|
||||
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.77'
|
||||
FIRMWARE_VERSION = '0.2.78'
|
||||
# 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.
|
||||
@@ -89,7 +91,7 @@ def backgrounds():
|
||||
|
||||
# Display modes per domain; everything else offers standard and watch (large value).
|
||||
DISPLAYS = {'weather': ('standard', 'watch', 'forecast'), 'sensor': ('standard', 'watch', 'graph'), 'screen': ('digital', 'analog'), 'sun': ('standard', 'watch', 'sunpath'),
|
||||
'camera': ('standard', 'live'), 'image': ('standard', 'live')}
|
||||
'camera': ('standard', 'live'), 'image': ('standard', 'live'), 'media_player': ('standard', 'watch', 'cover')}
|
||||
# Displays that only work on a double-width card.
|
||||
WIDE_ONLY = ('forecast', 'sunpath')
|
||||
|
||||
@@ -188,7 +190,8 @@ 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') not in ('wide', 'full') or options.get('display', 'standard') != 'standard' or options.get('inline') == 'slider':
|
||||
# The album cover in the icon's place (app 0.2.92) is the standard layout with a picture: the controls stay.
|
||||
if domain not in CONTROLS or options.get('size') not in ('wide', 'full') or options.get('display', 'standard') not in ('standard', 'cover') or options.get('inline') == 'slider':
|
||||
return None
|
||||
# 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])
|
||||
@@ -486,6 +489,8 @@ def min_firmware(layout):
|
||||
return PAGE_TILE_REPEAT_MIN_FIRMWARE
|
||||
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.get('options', {}).get('display') == 'cover' for t in layout['tiles']):
|
||||
return COVER_TILE_MIN_FIRMWARE
|
||||
if any(t.get('options', {}).get('display') == 'live' for t in layout['tiles']):
|
||||
return LIVE_MIN_FIRMWARE
|
||||
if any(t['entity'].split('.')[0] in CAMERA_DOMAINS for t in layout['tiles']):
|
||||
|
||||
@@ -173,6 +173,9 @@ def capabilities(entity_id, actions, state, services):
|
||||
elif domain in ('camera', 'image'):
|
||||
# A live picture on the tile (app 0.2.91): the editor offers it on a Guition, the only board that draws images.
|
||||
displays.append('live')
|
||||
elif domain == 'media_player':
|
||||
# The album cover in the icon's place (app 0.2.92), a Guition again.
|
||||
displays.append('cover')
|
||||
return {
|
||||
'toggle': TOGGLE.format(domain=domain) in actions,
|
||||
'inline': domain in INLINE and _fits(INLINE[domain], actions, attributes, services),
|
||||
|
||||
@@ -1661,10 +1661,13 @@ class Manager:
|
||||
return
|
||||
entities, size, grounds = live
|
||||
tiles = {tile['entity']: tile.get('options') or {} for tile in self.layouts.get(inbox, {}).get('tiles', [])}
|
||||
if any(entity not in tiles or tiles[entity].get('display') != 'live' for entity in entities):
|
||||
LOG.info('Live pictures for %s: not the live camera tiles of %s', ', '.join(entities), screen['name'])
|
||||
# A camera tile set to a live picture, or a media tile set to its cover (app 0.2.92).
|
||||
wanted = {'live', 'cover'}
|
||||
if any(entity not in tiles or tiles[entity].get('display') not in wanted
|
||||
or (tiles[entity].get('display') == 'cover') != camera_feed.cover_supported(entity) for entity in entities):
|
||||
LOG.info('Live pictures for %s: not the pictured tiles of %s', ', '.join(entities), screen['name'])
|
||||
return
|
||||
paces = [tiles[entity].get('refresh', camera_feed.LIVE_REFRESH[0]) for entity in entities]
|
||||
paces = [tiles[entity].get('refresh', camera_feed.LIVE_REFRESH[0]) if tiles[entity].get('display') == 'live' else 0 for entity in entities]
|
||||
url, listing = '', ','.join(entities)
|
||||
base = await camera_feed.base_url(self.ha.request)
|
||||
if base:
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<title>ESP Screens</title>
|
||||
<script type="module" crossorigin src="./assets/index-BwlmhXLI.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-plSjhIie.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-DQxVdm_5.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user