diff --git a/README.md b/README.md index f2cfe16..6ed2520 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,15 @@ blijven afzonderlijk zichtbaar. | --- | --- | | Tegels, namen, kleuren, volgorde of scherminstellingen | Opslaan in ESP Screens; geen firmwareflash | | Nieuwe versie van de beheerpagina | ESP Screen Manager updaten in de HA-appwinkel | -| Nieuwe functie op het fysieke scherm | Bestaand profiel bijwerken via Firmware & USB → Wifi / OTA | +| Nieuwe functie op het fysieke scherm | Knop **Bijwerken** bij het scherm (badge *Update x.y.z*), of het vinkje **Elke nacht automatisch bijwerken** | + +Elke appversie hoort bij één firmwareversie. Na een app-update toont de lijst +per scherm of er nieuwere firmware is. **Bijwerken** bouwt het eigen profiel +met de ingebouwde CLI, installeert draadloos en wacht tot het scherm terug is. +Met het vinkje gebeurt dat 's nachts vanzelf, één scherm tegelijk; een +mislukking stopt de ronde en geeft een melding in Home Assistant. Firmware +0.2.17+ meldt daarvoor zelf apparaatnaam en IP-adres; een ouder scherm vraagt +eenmalig het IP-adres. Handmatig kan nog steeds via Firmware & USB → Wifi / OTA. De eigen YAML en wifi/API/OTA-instellingen blijven in de ESPHome-configmap. Tegelindelingen en opties staan in de permanente appdata. CYD-kalibratie en diff --git a/docs/EASY_SETUP.md b/docs/EASY_SETUP.md index dd88701..9b356b8 100644 --- a/docs/EASY_SETUP.md +++ b/docs/EASY_SETUP.md @@ -137,7 +137,7 @@ zodra het scherm terugkomt. De app moet blijven draaien voor actuele tegeldata. | --- | --- | --- | | Andere entiteiten, namen of volgorde | Opslaan in ESP Screens | Wifi, sleutels, kalibratie | | Nieuwe beheerpagina/appversie | Appwinkel → ESP Screen Manager → Update | Alle indelingen in `/data/screens.json` | -| Nieuwe schermfunctie/kaart | Firmware & USB → bestaand profiel → Wifi / OTA | Eigen YAML, sleutels en CYD-kalibratie; app stuurt tegels opnieuw | +| Nieuwe schermfunctie/kaart | Knop **Bijwerken** bij het scherm, of vinkje **Elke nacht automatisch bijwerken** (handmatig: Firmware & USB → Wifi / OTA) | Eigen YAML, sleutels en CYD-kalibratie; app stuurt tegels opnieuw | De eigen YAML verwijst naar de firmwarepakketten in `main`. Bij een nieuwe build haalt ESPHome de nieuwste gepubliceerde pakket- en componentcode op. Je vervangt diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 7498de5..e2d243a 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -140,6 +140,26 @@ pressed-feedback op de PRESSED-state blijven gelijk. De analoge klok wijzigt alleen de tekening (streepjes, cijfers, kalenderblok op enkele tegels); er is geen nieuw veld voor nodig. Geen gewijzigde preferences of sleutels. +### Compatibiliteit 0.2.16 / firmware 0.2.17 + +`FIRMWARE_VERSION` in `screen_manager/app/core.py` is de firmware die bij deze +app hoort; `tests/test_updates.py` eist dat hij gelijk is aan +`SCREEN_FIRMWARE_VERSION` in beide bordprofielen en pakketten. Verhoog ze samen. +Een scherm met een lagere `Schermfirmware` krijgt een update-aanbod; een build +haalt `main`, dus publiceer firmware en app in dezelfde commit. + +Firmware 0.2.17 voegt de diagnostische text sensors `Apparaatnaam` +(`${DEVICE_NAME}`) en `IP-adres` (`wifi_info`) toe. De app koppelt een scherm +via `Apparaatnaam` aan het profiel met dezelfde `esphome.name`; oudere firmware +valt terug op één profiel met gelijke `friendly_name` en een handmatig ingevuld +adres. Hernoem deze sensors niet zonder `core.discover` aan te passen. + +Updatestatus staat in `/data/updates.json` (`version: 1`: `auto`, `hosts`, +`results`, `last_round`), los van `screens.json`. Onbekende versies worden +geweigerd. De ronde draait één scherm tegelijk, wacht op `Schermfirmware >=` +doelversie en één minuut stabiliteit, en stopt bij de eerste fout. Alleen de +nachtelijke ronde schrijft een `persistent_notification` in HA. + ### Compatibiliteit 0.2.12 / firmware 0.2.14 Opslagversie en tegelprotocol blijven 1. Additief: `tiles[].options.size` diff --git a/docs/TEST_RESULTS_0217.md b/docs/TEST_RESULTS_0217.md new file mode 100644 index 0000000..c94fa89 --- /dev/null +++ b/docs/TEST_RESULTS_0217.md @@ -0,0 +1,54 @@ +# App 0.2.16 / firmware 0.2.17 — bijwerken met één knop en nachtelijke ronde + +## Wijzigingen + +- **Firmware**: twee diagnostische text sensors per scherm, `Apparaatnaam` + (`${DEVICE_NAME}`, elke 60 s) en `IP-adres` (`wifi_info`). Verder ongewijzigd; + geen preferences, protocol of layoutcode aangeraakt. +- **App** (`screen_manager/app/updates.py`): `Updater` met `FIRMWARE_VERSION` + als doelversie, profielkoppeling via `Apparaatnaam` → `esphome.name` in de + ESPHome-map (`Firmware.profile_names`, leest YAML zonder `!secret` uit te + voeren), terugval op één profiel met dezelfde `friendly_name` plus een + handmatig adres. Eén ronde tegelijk: `install` (bestaande `Firmware.start`), + wachten op `Schermfirmware >= doel` (max 240 s), 60 s stabiliteit, 120 s pauze + tot het volgende scherm; de eerste fout beëindigt de ronde. Nachtelijke ronde + tussen 03:00 en 06:00 in de HA-tijdzone, één keer per dag, alleen met het + vinkje aan; bij een fout een `persistent_notification` in HA. +- **UI**: schermkaart is nu een `div` met een selectieknop en een updaterij + (badge *Update x.y.z* + **Bijwerken**, spinner met fase, resultaat een dag + zichtbaar). Blok *Firmware-updates* met **Alle schermen bijwerken** (zichtbaar + bij twee of meer bij te werken schermen) en het vinkje voor de nachtronde. + Een open IP-formulier overleeft de periodieke verversing; polling versnelt naar + 3 s zolang er een update loopt. +- Opslag `/data/updates.json` (`version: 1`), naast `screens.json`. + +## Tests + +- `.venv-portal/bin/python -m unittest discover -s tests`: 62 tests OK, waaronder + de nieuwe `tests/test_updates.py` (versiebron gelijk in app en beide pakketten, + discovery van naam en adres, profielen lezen zonder secrets, aanbod en + handmatig adres, ronde stopt na fout en meldt alleen automatisch, scherm dat + niet terugkomt faalt, nachtvenster en persistentie, HTTP-endpoints met CSRF). +- `python3 tools/generate_packages.py --check`: beide pakketten actueel. +- `esphome compile` voor `easy-cyd-device.yaml` en `easy-guition-device.yaml` + (lokale pakketten met dezelfde inhoud als `packages/*.yaml`): geslaagd. +- USB-flash na `esptool chip_id` per poort (`usbserial-130` = ESP32/CYD, + `usbserial-210` = ESP32-S3/Guition). Uploadlog toont + `.esphome/build/cyd-2432s028/…/firmware.bin` resp. + `.esphome/build/guition-wallbox/…/firmware.bin`. +- `device_info` via de API: CYD `cyd-2432s028` project_version 0.2.17, + Guition `guition-wallbox` project_version 0.2.17, beide gecompileerd + 2026-09-13 10:03. Beide melden `Schermfirmware=0.2.17`, `Apparaatnaam` gelijk + aan de profielnaam en `IP-adres` 192.168.146.134 resp. 192.168.146.136. +- Lokale preview van de beheerpagina met een nep-HA en een trage nep-CLI: + badge en knop bij een scherm op 0.2.16, spinner met "Bouwen en installeren…", + daarna "Bijgewerkt naar firmware 0.2.17." bij het scherm; scherm zonder + gemeld IP vraagt eenmalig het adres en start daarna; scherm op 0.2.17 toont + geen aanbod; het vinkje bewaart `auto` via `PUT /api/updates`. + +## Nog te doen in productie + +- Add-on bijwerken vanuit de HA-appwinkel (0.2.16) en beide schermen (nu al + 0.2.17 via USB) moeten *geen* aanbod tonen. De eerste echte OTA-ronde volgt + bij de volgende firmwareversie; controleer dan het log onder Firmware & USB, + de spinnerfases en de melding bij een geforceerde fout. diff --git a/guition-4848s040.yaml b/guition-4848s040.yaml index f02f0f9..729e198 100644 --- a/guition-4848s040.yaml +++ b/guition-4848s040.yaml @@ -1,7 +1,7 @@ # Guition ESP32-S3-4848S040 / 4-inch EU wallbox, 480x480 IPS + GT911. # Use guition-device.yaml; see docs/GUITION.md. CYD hardware is a separate profile. substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.16" + SCREEN_FIRMWARE_VERSION: "0.2.17" DYNAMIC_TILES: "false" DASHBOARD_INBOX_INTERNAL: "true" DEVICE_NAME: "guition-wallbox" @@ -1464,6 +1464,16 @@ text_sensor: entity_category: diagnostic lambda: 'return {"${SCREEN_FIRMWARE_VERSION}"};' update_interval: 60s + # ESP Screen Manager matches the screen to its YAML profile and OTA address. + - platform: template + name: "Apparaatnaam" + entity_category: diagnostic + lambda: 'return {"${DEVICE_NAME}"};' + update_interval: 60s + - platform: wifi_info + ip_address: + name: "IP-adres" + entity_category: diagnostic - platform: template id: smartdisplay_action name: "SmartDisplay Action" diff --git a/home-like-2432s028.yaml b/home-like-2432s028.yaml index 5203e75..311585a 100644 --- a/home-like-2432s028.yaml +++ b/home-like-2432s028.yaml @@ -58,7 +58,7 @@ ############################################################ substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.16" + SCREEN_FIRMWARE_VERSION: "0.2.17" DYNAMIC_TILES: "false" DASHBOARD_INBOX_INTERNAL: "true" DEVICE_NAME: "cyd-new" @@ -1649,6 +1649,16 @@ text_sensor: entity_category: diagnostic lambda: 'return {"${SCREEN_FIRMWARE_VERSION}"};' update_interval: 60s + # ESP Screen Manager matches the screen to its YAML profile and OTA address. + - platform: template + name: "Apparaatnaam" + entity_category: diagnostic + lambda: 'return {"${DEVICE_NAME}"};' + update_interval: 60s + - platform: wifi_info + ip_address: + name: "IP-adres" + entity_category: diagnostic - platform: template id: smartdisplay_action name: "SmartDisplay Action" diff --git a/packages/cyd.yaml b/packages/cyd.yaml index 7621132..56e347e 100644 --- a/packages/cyd.yaml +++ b/packages/cyd.yaml @@ -59,7 +59,7 @@ ############################################################ substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.16" + SCREEN_FIRMWARE_VERSION: "0.2.17" DYNAMIC_TILES: "true" DASHBOARD_INBOX_INTERNAL: "false" DEVICE_NAME: "cyd-new" @@ -1602,6 +1602,16 @@ text_sensor: entity_category: diagnostic lambda: 'return {"${SCREEN_FIRMWARE_VERSION}"};' update_interval: 60s + # ESP Screen Manager matches the screen to its YAML profile and OTA address. + - platform: template + name: "Apparaatnaam" + entity_category: diagnostic + lambda: 'return {"${DEVICE_NAME}"};' + update_interval: 60s + - platform: wifi_info + ip_address: + name: "IP-adres" + entity_category: diagnostic - platform: template id: smartdisplay_action name: "SmartDisplay Action" diff --git a/packages/guition.yaml b/packages/guition.yaml index 16dea22..cf65b99 100644 --- a/packages/guition.yaml +++ b/packages/guition.yaml @@ -2,7 +2,7 @@ # Guition ESP32-S3-4848S040 / 4-inch EU wallbox, 480x480 IPS + GT911. # Use guition-device.yaml; see docs/GUITION.md. CYD hardware is a separate profile. substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.16" + SCREEN_FIRMWARE_VERSION: "0.2.17" DYNAMIC_TILES: "true" DASHBOARD_INBOX_INTERNAL: "false" DEVICE_NAME: "guition-new" @@ -1417,6 +1417,16 @@ text_sensor: entity_category: diagnostic lambda: 'return {"${SCREEN_FIRMWARE_VERSION}"};' update_interval: 60s + # ESP Screen Manager matches the screen to its YAML profile and OTA address. + - platform: template + name: "Apparaatnaam" + entity_category: diagnostic + lambda: 'return {"${DEVICE_NAME}"};' + update_interval: 60s + - platform: wifi_info + ip_address: + name: "IP-adres" + entity_category: diagnostic - platform: template id: smartdisplay_action name: "SmartDisplay Action" diff --git a/screen_manager/CHANGELOG.md b/screen_manager/CHANGELOG.md index da507be..cdb4018 100644 --- a/screen_manager/CHANGELOG.md +++ b/screen_manager/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.2.16 (firmware 0.2.17) + +- **Bijwerken met één knop**: een scherm met oudere firmware krijgt in de lijst een badge *Update 0.2.17* en een knop **Bijwerken**. De app bouwt het eigen profiel, installeert draadloos en toont een spinner tot het scherm terug is met de nieuwe versie en een minuut stabiel blijft. Het resultaat blijft een dag zichtbaar bij het scherm. +- **Elke nacht automatisch bijwerken**: vinkje onder *Firmware-updates*. Tussen 03:00 en 06:00 (tijdzone van HA) werkt de app schermen met oudere firmware één voor één bij, met twee minuten pauze ertussen. Mislukt een scherm, dan stopt de ronde en verschijnt een melding in Home Assistant; de overige schermen blijven op hun oude firmware. +- **Alle schermen bijwerken** doet dezelfde ronde direct, bijvoorbeeld na een app-update. +- Nieuwe firmware is er zodra deze app een nieuwe versie heeft: de app kent de bijbehorende firmwareversie en vergelijkt die met `Schermfirmware` per scherm. +- Firmware 0.2.17 meldt twee diagnostische sensors extra: **Apparaatnaam** (de ESPHome-naam, gelijk aan het YAML-profiel) en **IP-adres**. Daarmee vindt de app zelf het profiel en het OTA-adres. Een scherm met oudere firmware wordt op apparaatnaam aan een profiel gekoppeld en vraagt eenmalig het IP-adres. + ## 0.2.15 (firmware 0.2.16) - **Achtergrond: Geen** als extra keuze in het tegelpalet: de kaart en de rand vallen weg en de tegelinhoud staat direct op de schermachtergrond, in dezelfde maat en op dezelfde plek als mét kaart. Werkt voor elke tegel; de mockup toont zo'n tegel met een stippellijn. Vereist schermfirmware 0.2.16; de app bewaart de indeling en meldt het als het scherm ouder is. diff --git a/screen_manager/app/core.py b/screen_manager/app/core.py index 75f7245..61dc4a2 100644 --- a/screen_manager/app/core.py +++ b/screen_manager/app/core.py @@ -13,6 +13,8 @@ NEW_DOMAINS = frozenset('sun timer person screen'.split()) 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' 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()) @@ -229,6 +231,16 @@ def discover(registry, states, devices, areas): if item.get('platform') == 'esphome' and item.get('original_name') == 'Schermfirmware'} boards = {item.get("device_id"): "guition" for item in registry if item.get("platform") == "esphome" and item.get("original_name") == "Guition schermtype"} + def diagnostic(name, pattern): + found = {} + for item in registry: + if item.get('platform') == 'esphome' and item.get('original_name') == name: + value = states.get(item['entity_id'], {}).get('state', '') + if isinstance(value, str) and re.fullmatch(pattern, value): + found[item.get('device_id')] = value + return found + nodes = diagnostic('Apparaatnaam', r'[a-z0-9][a-z0-9-]{0,30}') + addresses = diagnostic('IP-adres', r'\d{1,3}(\.\d{1,3}){3}') screens, entities = [], [] for item in registry: eid = item['entity_id'] @@ -239,6 +251,8 @@ def discover(registry, states, devices, areas): screens.append({'id': eid, 'name': device.get('name_by_user') or device.get('name') or eid, 'firmware': versions.get(item.get('device_id'), 'onbekend'), 'board': boards.get(item.get('device_id'), 'unknown'), + 'node': nodes.get(item.get('device_id')), 'ip': addresses.get(item.get('device_id')), + 'device': device.get('name') or '', 'area': area, 'online': state.get('state') not in (None, 'unknown', 'unavailable'), 'status': state.get('state', 'Niet verbonden')}) if not entity_id(eid) or item.get('disabled_by'): diff --git a/screen_manager/app/firmware.py b/screen_manager/app/firmware.py index dc48439..d6bc41e 100644 --- a/screen_manager/app/firmware.py +++ b/screen_manager/app/firmware.py @@ -12,6 +12,10 @@ import time import yaml from core import installation_yaml +class LenientLoader(yaml.SafeLoader): + """Reads profile metadata without resolving !secret or !include.""" +LenientLoader.add_multi_constructor('!', lambda loader, suffix, node: None) + class Firmware: def __init__(self, root, data): self.root, self.data = Path(root).resolve(), Path(data) @@ -25,6 +29,25 @@ class Firmware: return [{'file': p.name} for p in sorted(self.root.glob('*.yaml')) if p.name != 'secrets.yaml' and p.is_file() and not p.is_symlink()] + def profile_names(self): + """{file: {'node': esphome name, 'friendly': friendly name}} for every readable profile.""" + found = {} + for entry in self.profiles(): + try: + data = yaml.load((self.root / entry['file']).read_text(), Loader=LenientLoader) + except (OSError, yaml.YAMLError, UnicodeError): + continue + block = data.get('esphome') if isinstance(data, dict) else None + if not isinstance(block, dict): + continue + substitutions = data.get('substitutions') if isinstance(data.get('substitutions'), dict) else {} + def resolve(value): + if isinstance(value, str) and value.startswith('${') and value.endswith('}'): + value = substitutions.get(value[2:-1]) + return value if isinstance(value, str) else None + found[entry['file']] = {'node': resolve(block.get('name')), 'friendly': resolve(block.get('friendly_name'))} + return found + def ports(self): return sorted(set(glob.glob('/dev/serial/by-id/*') or glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*'))) diff --git a/screen_manager/app/server.py b/screen_manager/app/server.py index 7a63d64..897dbc6 100644 --- a/screen_manager/app/server.py +++ b/screen_manager/app/server.py @@ -1,4 +1,4 @@ -"""HA Ingress app. The only HA write is text.set_value on discovered inboxes.""" +"""HA Ingress app. HA writes: text.set_value on discovered inboxes and one persistent notification when a nightly update stops.""" import asyncio import contextlib import json @@ -10,6 +10,7 @@ import time from datetime import datetime, timedelta, timezone import math from firmware import Firmware +from updates import Updater from aiohttp import ClientSession, ClientTimeout, WSMsgType, web from core import BUILTIN, TILE_BACKGROUNDS, discover, extras, installation_yaml, min_firmware, packets, state_message, validate_layout, validate_settings @@ -169,6 +170,7 @@ class Manager: self.histories = {} self.forecasts = {} self.firmware = Firmware(os.environ.get("ESPHOME_CONFIG", "/homeassistant/esphome"), self.path.parent) + self.updates = Updater(self, self.path.parent / 'updates.json') if self.path.exists(): raw = json.loads(self.path.read_text()) # Versioned persistent data. Never silently overwrite an unknown schema. @@ -356,11 +358,21 @@ def create_app(manager, development=False): for screen in screens: screen['layout'] = manager.layouts.get(screen['id'], {'title': 'Thuis', 'tiles': []}) screen['delivery'] = manager.status.get(screen['id'], 'Kies je eerste tegels') + 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}) + return web.json_response({'csrf': csrf, 'connected': manager.ha.online, 'screens': screens, 'entities': entities, + 'backgrounds': TILE_BACKGROUNDS, '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}) + async def update_screen(request): + data = await request.json() if request.can_read_body else {} + return web.json_response(manager.updates.start(request.match_info['inbox'], data.get('host'))) + async def update_all(request): + return web.json_response({'started': manager.updates.start_all()}) + 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) @@ -378,12 +390,15 @@ def create_app(manager, development=False): 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 shutdown(app): - task=manager.firmware.task - if task and not task.done(): - task.cancel() - with contextlib.suppress(asyncio.CancelledError): await task + for task in (manager.updates.task, manager.firmware.task): + if task and not task.done(): + task.cancel() + with contextlib.suppress(asyncio.CancelledError): await task app.on_cleanup.append(shutdown) app.router.add_get('/api/screens/{inbox}/inspect', inspector) + app.router.add_post('/api/screens/{inbox}/update', update_screen) + app.router.add_post('/api/updates/run', update_all) + app.router.add_put('/api/updates', update_settings) app.router.add_get('/api/firmware', firmware_status) app.router.add_post('/api/firmware/jobs', firmware_start) app.router.add_post('/api/firmware/profiles', firmware_create) @@ -408,7 +423,7 @@ async def main(): await runner.setup() await web.TCPSite(runner, '127.0.0.1' if development else '0.0.0.0', 8099).start() try: - await asyncio.gather(ha.run(), manager.run()) + await asyncio.gather(ha.run(), manager.run(), manager.updates.run()) finally: await runner.cleanup() diff --git a/screen_manager/app/static/app.js b/screen_manager/app/static/app.js index 7f68525..63fc39f 100644 --- a/screen_manager/app/static/app.js +++ b/screen_manager/app/static/app.js @@ -175,14 +175,107 @@ function renderSettingsSupport() { ? "Opslaan stuurt je instellingen direct naar dit scherm. Ze blijven ook na een herstart bewaard." : "Eenmalig firmware 0.1.2 of nieuwer installeren via ESPHome. Je kunt de instellingen alvast bewaren; oudere firmware gebruikt ze nog niet."; } +const updating = new Set(); +const PHASES = { + install: "Bouwen en installeren…", + verify: "Wachten tot het scherm terug is…", + settle: "Controleren of het stabiel blijft…", +}; +async function startUpdate(screen, host) { + updating.add(screen.id); + renderScreens(); + try { + await api(`screens/${encodeURIComponent(screen.id)}/update`, { + method: "POST", + body: JSON.stringify(host ? { host } : {}), + }); + await refresh(); + } catch (e) { + updating.delete(screen.id); + renderScreens(); + toast(e.message); + } +} +function renderUpdate(screen) { + const u = screen.update || {}; + const row = node("div", undefined, "screen-update"); + const running = u.state === "running" || updating.has(screen.id); + if (running) { + row.append( + node("span", undefined, "spin"), + node("small", PHASES[u.phase] || "Update starten…"), + ); + } else if (u.state === "queued") { + row.append(node("small", "In de wachtrij voor de update")); + } else if (u.available && screen.online) { + row.append(node("span", `Update ${u.target}`, "badge update")); + const button = node("button", "Bijwerken", "mini"); + button.type = "button"; + if (u.host && u.profile) { + button.onclick = (e) => { + e.stopPropagation(); + startUpdate(screen); + }; + } else if (!u.profile) { + button.disabled = true; + button.title = "Geen ESPHome-profiel met deze apparaatnaam gevonden."; + } else { + button.onclick = (e) => { + e.stopPropagation(); + const form = node("form", undefined, "screen-host"); + const input = node("input"); + input.placeholder = "IP-adres, bijv. 192.168.1.50"; + input.required = true; + input.pattern = "[A-Za-z0-9][A-Za-z0-9.\\-]*"; + const go = node("button", "Start", "mini"); + const cancel = node("button", "✕", "quiet"); + cancel.type = "button"; + cancel.setAttribute("aria-label", "Annuleren"); + cancel.onclick = () => { + form.remove(); + renderScreens(); + }; + form.append(input, go, cancel); + form.onsubmit = (ev) => { + ev.preventDefault(); + form.remove(); + startUpdate(screen, input.value.trim()); + }; + row.replaceChildren( + node("small", "Eenmalig het IP-adres; nieuwe firmware meldt het zelf."), + form, + ); + input.focus(); + }; + } + row.append(button); + } else if (u.result && Date.now() / 1000 - u.result.time < 86400) { + row.append( + node( + "small", + u.result.message, + u.result.state === "failed" ? "failed" : "", + ), + ); + } else return null; + return row; +} function renderScreens() { + // Keep an open address form alive across the periodic refresh. + if ($("#screens .screen-host")) { + renderUpdates(); + return; + } $("#screens").replaceChildren(); for (const screen of inventory.screens) { - const b = node( - "button", + if (screen.update?.state === "running") updating.delete(screen.id); + const item = node( + "div", undefined, `screen ${screen.id === selected ? "selected" : ""}`, ); + const b = node("button", undefined, "screen-main"); + b.type = "button"; b.append( node("strong", screen.name), node( @@ -191,7 +284,10 @@ function renderScreens() { ), ); b.onclick = () => select(screen.id); - $("#screens").append(b); + item.append(b); + const update = renderUpdate(screen); + if (update) item.append(update); + $("#screens").append(item); } const screen = inventory.screens.find((s) => s.id === selected); if (screen) { @@ -200,7 +296,47 @@ function renderScreens() { : "Offline · wijzigingen worden bewaard"; $("#delivery").classList.toggle("online", screen.online); } + renderUpdates(); } +function renderUpdates() { + const u = inventory.updates; + $("#updates").hidden = !u || !inventory.screens.length; + if (!u) return; + const outdated = inventory.screens.filter((s) => s.update?.available).length; + $("#updates-hint").textContent = u.busy + ? `Bezig met bijwerken naar firmware ${u.target}…` + : outdated + ? `Firmware ${u.target} is beschikbaar voor ${outdated} scherm${outdated === 1 ? "" : "en"}.` + : `Alle schermen hebben firmware ${u.target}.`; + $("#update-all").hidden = !u.pending || !!u.busy || u.pending < 2; + $("#update-all").textContent = `Alle ${u.pending} schermen bijwerken`; + if (document.activeElement !== $("#auto-update")) + $("#auto-update").checked = u.auto; +} +$("#update-all").onclick = async () => { + try { + await api("updates/run", { method: "POST" }); + await refresh(); + } catch (e) { + toast(e.message); + } +}; +$("#auto-update").onchange = async () => { + try { + await api("updates", { + method: "PUT", + body: JSON.stringify({ auto: $("#auto-update").checked }), + }); + toast( + $("#auto-update").checked + ? "Schermen worden voortaan 's nachts bijgewerkt." + : "Automatisch bijwerken staat uit.", + ); + } catch (e) { + $("#auto-update").checked = !$("#auto-update").checked; + toast(e.message); + } +}; function move(from, to) { if (to < 0 || to >= layout.tiles.length) return; const [tile] = layout.tiles.splice(from, 1); @@ -767,7 +903,12 @@ window.addEventListener("beforeunload", (e) => { } }); refresh(); -setInterval(refresh, 10000); +(function poll() { + setTimeout(async () => { + await refresh(); + poll(); + }, inventory.updates?.busy ? 3000 : 10000); +})(); // Shared firmware workspace; always select a concrete profile and upload target. let firmwarePoll; diff --git a/screen_manager/app/static/index.html b/screen_manager/app/static/index.html index 06dac38..1cae96c 100644 --- a/screen_manager/app/static/index.html +++ b/screen_manager/app/static/index.html @@ -74,6 +74,20 @@ seconden.
+