One-window install: profile, wifi, USB flash and pairing hints (0.2.27)

"Nieuw scherm" used to span two dialogs plus a copy/download of the
installation YAML. It is now one dialog: board, name (node name derived,
editable), wifi only when secrets.yaml lacks it, the detected USB port,
and Installeren. The add-on writes the profile with unique API/OTA keys,
merges just the missing wifi lines into secrets.yaml (comments and other
secrets untouched, result re-parsed before writing), then builds and
flashes with the live ESPHome log and stage in the same dialog; a failed
build offers a retry without a second profile. The YAML download
endpoint is gone: the profile already lives in the ESPHome folder.

Pairing happens in Home Assistant itself, so the done view and a sidebar
card per not-yet-paired profile say so, with a button to Apparaten &
diensten, the API key to copy and the reminder to allow HA actions.
One profile per screen; the dialog says that too.

Tests: wifi merge cases, install pre-checks, pending list, profile
metadata; the date-anchored hourly forecast test now follows the clock.
Firmware 0.2.22 stays current.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-14 11:11:23 +02:00
parent 5d6651d68b
commit 2327d31261
16 changed files with 848 additions and 287 deletions
+18 -7
View File
@@ -14,7 +14,7 @@ import tile_icons
from updates import Updater
from aiohttp import ClientSession, ClientTimeout, WSMsgType, web
from core import BUILTIN, TILE_BACKGROUNDS, controls_catalogue, discover, extras, installation_yaml, min_firmware, packets, state_message, validate_layout, validate_settings
from core import BUILTIN, TILE_BACKGROUNDS, controls_catalogue, discover, extras, min_firmware, packets, state_message, validate_layout, validate_settings
from zoneinfo import ZoneInfo
LOG = logging.getLogger('screen_manager')
@@ -291,6 +291,19 @@ class Manager:
for listener in self.listeners:
listener.set()
def pending_profiles(self, screens, profiles):
"""ESP Screens profiles without a paired screen: flashed but not yet added in Home Assistant, or not flashed yet.
Pairing happens in Home Assistant itself, outside this page; the sidebar shows these so nobody wonders
where the freshly flashed screen went."""
nodes = {s.get('node') for s in screens}
devices = {s.get('device') for s in screens}
installed = getattr(self.firmware, 'installed', set())
return [{'file': file, 'node': meta['node'], 'friendly': meta.get('friendly') or meta['node'] or file,
'installed': file in installed, 'api_key': meta.get('api_key')}
for file, meta in profiles.items()
if meta.get('screen') and meta.get('node') not in nodes and (meta.get('friendly') or None) not in devices]
def watched_entities(self):
"""Entities whose state changes matter: tiles on any layout plus the screens' own diagnostics."""
watched = {tile['entity'] for layout in self.layouts.values() for tile in layout['tiles']}
@@ -431,6 +444,7 @@ def create_app(manager, development=False):
screen['delivery'] = manager.status.get(screen['id'], 'Kies je eerste tegels')
screen['update'] = manager.updates.state_for(screen, profiles)
return {'csrf': csrf, 'connected': manager.ha.online, 'screens': screens,
'pending': manager.pending_profiles(screens, profiles),
'updates': manager.updates.summary(screens, profiles)}, entities
async def inventory(request):
payload, entities = light_payload()
@@ -478,10 +492,6 @@ def create_app(manager, development=False):
async def update_settings(request):
manager.updates.set_auto((await request.json()).get('auto'))
return web.json_response(manager.updates.summary())
async def download(request):
data = await request.json()
content = installation_yaml(data)
return web.Response(text=content, content_type='text/yaml', headers={'Content-Disposition': f'attachment; filename="{data["name"]}.yaml"'})
async def inspector(request):
inbox=request.match_info['inbox']
if inbox not in {s['id'] for s in manager.inventory()[0]}: raise ValueError('Onbekend scherm.')
@@ -493,7 +503,9 @@ def create_app(manager, development=False):
'options':t.get('options',{})} for i,t in enumerate(layout['tiles'])]})
async def firmware_status(request): return web.json_response(manager.firmware.status())
async def firmware_start(request): return web.json_response(manager.firmware.start(await request.json()))
async def firmware_create(request): return web.json_response(manager.firmware.create(await request.json()))
async def firmware_create(request):
# Profile, missing wifi secrets and (with a USB port) the build and flash in one request.
return web.json_response(manager.firmware.install(await request.json()))
async def shutdown(app):
for task in (manager.updates.task, manager.firmware.task):
if task and not task.done():
@@ -511,7 +523,6 @@ def create_app(manager, development=False):
app.router.add_get('/api/inventory', inventory)
app.router.add_get('/api/events', events)
app.router.add_put('/api/screens/{inbox}', save)
app.router.add_post('/api/install', download)
app.router.add_static('/static/', static)
return app