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:
@@ -109,20 +109,19 @@ Voor Home Assistant OS met Apps/Add-ons op **aarch64 of amd64**:
|
||||
`https://github.com/MaxGramser/homeassistant_espscreen`.
|
||||
2. Installeer **ESP Screen Manager**, start de app en open **ESP Screens**.
|
||||
ESPHome Device Builder is optioneel: de ESPHome-CLI zit al in deze app.
|
||||
3. Kies **Nieuw scherm**, selecteer CYD of Guition en geef een unieke naam.
|
||||
Gebruik **Bewaar profiel in ESP Screens**. Bestaande `wifi_ssid` en
|
||||
`wifi_password` in ESPHome `secrets.yaml` worden hergebruikt; bij een verse
|
||||
installatie vraagt de wizard de wifi eenmalig. API- en OTA-sleutels worden
|
||||
uniek aangemaakt en in je eigen apparaat-YAML bewaard.
|
||||
4. Sluit het scherm met een USB-datakabel aan op de **Home Assistant-machine**.
|
||||
Open **Firmware & USB**, kies het eigen profiel en de juiste USB-poort en
|
||||
start **Bouwen & installeren**. Een eerste build kan meerdere minuten duren.
|
||||
5. **CYD:** doorloop de kalibratie op het scherm. **Guition:** gebruikt GT911
|
||||
3. Sluit het scherm met een USB-datakabel aan op de **Home Assistant-machine**
|
||||
en kies **Nieuw scherm**: CYD of Guition, een naam, de USB-poort en
|
||||
**Installeren**. Ontbreekt wifi in de ESPHome `secrets.yaml`, dan vraagt het
|
||||
venster die eenmalig en zet ESP Screens alleen de ontbrekende regels erin.
|
||||
Het profiel met unieke API- en OTA-sleutels komt in de ESPHome-map; de build
|
||||
en de flash lopen in hetzelfde venster (een eerste build duurt op een
|
||||
Raspberry enkele minuten). Elk scherm krijgt zijn eigen profiel.
|
||||
4. **CYD:** doorloop de kalibratie op het scherm. **Guition:** gebruikt GT911
|
||||
zonder resistieve kalibratie. Koppel daarna het ontdekte ESPHome-apparaat in
|
||||
**Instellingen → Apparaten & diensten**. Gebruik bij een sleutelvraag de
|
||||
`api.encryption.key` uit je eigen YAML. Geef het apparaat toestemming om
|
||||
**Instellingen → Apparaten & diensten** met de API-sleutel die het venster
|
||||
na de installatie toont (kopieerknop). Geef het apparaat toestemming om
|
||||
Home Assistant-acties uit te voeren.
|
||||
6. Selecteer het scherm in ESP Screens, kies je tegels en klik
|
||||
5. Selecteer het scherm in ESP Screens, kies je tegels en klik
|
||||
**Opslaan & naar scherm**. Test vervolgens de fysieke bediening.
|
||||
|
||||
Je kunt later vanuit **Firmware & USB → Wifi / OTA** nieuwe firmware installeren.
|
||||
|
||||
+45
-39
@@ -33,42 +33,41 @@ Deze app bevat de geteste ESPHome 2026.6.2-CLI en draait binnen je HA-aanmelding
|
||||
Een tweede ESPHome-beheerpagina, MQTT, blueprint of long-lived token is niet nodig.
|
||||
Gebruik de GitHub-versie voor updates; een lokale test-add-on is een andere app.
|
||||
|
||||
## 2. Maak en bewaar je eigen apparaatprofiel
|
||||
|
||||
Klik **Nieuw scherm**. Kies CYD of Guition, een herkenbare naam en een unieke
|
||||
apparaatnaam, bijvoorbeeld `scherm-keuken`. Klik **Bewaar profiel in ESP Screens**.
|
||||
|
||||
De wizard controleert de bestaande ESPHome `secrets.yaml`. Met `wifi_ssid` en
|
||||
`wifi_password` aanwezig hoef je geen wifi in te vullen. Alleen als er nog geen
|
||||
secretsbestand is, vraagt de wizard je 2,4GHz-wifi eenmalig. Een bestaand bestand
|
||||
met ontbrekende of ongeldige wifi-sleutels moet je eerst herstellen; het wordt
|
||||
niet stilzwijgend overschreven.
|
||||
|
||||
Je apparaat-YAML bevat unieke API- en OTA-sleutels. Bewaar dit profiel en gebruik
|
||||
het opnieuw bij updates. Voor een bestaand scherm telkens een nieuw profiel
|
||||
maken genereert nieuwe sleutels en is niet de updateroute.
|
||||
|
||||
Wil je ESPHome Device Builder gebruiken, kies dan **Maak installatie-YAML** en
|
||||
kopieer/download die YAML naar een eigen apparaat daar. Beide routes gebruiken
|
||||
dezelfde firmwarepakketten. Wifi blijft in ESPHome `secrets.yaml`; API- en
|
||||
OTA-sleutels staan in het eigen apparaatprofiel.
|
||||
|
||||
## 3. Eerste flash via USB op de Raspberry
|
||||
## 2. Nieuw scherm: aansluiten en installeren
|
||||
|
||||
1. Sluit het scherm met een **USB-datakabel** aan op de machine waarop Home
|
||||
Assistant draait. Bij meerdere borden: identificeer eerst de juiste poort,
|
||||
of sluit ze voor de eerste installatie één voor één aan.
|
||||
2. Open **Firmware & USB** in ESP Screens en kies het zojuist bewaarde profiel.
|
||||
3. Kies bij **Installeren naar** de USB-poort van dit scherm.
|
||||
4. Klik **Bouwen & installeren** en wacht op een geslaagde build én upload.
|
||||
De eerste build kan op een Raspberry meerdere minuten duren.
|
||||
5. Het scherm herstart en verbindt met wifi. Latere firmware-updates kunnen via
|
||||
**Wifi / OTA** in dezelfde pagina, met het bestaande profiel.
|
||||
Assistant draait. Bij meerdere borden: sluit ze voor de eerste installatie
|
||||
één voor één aan, of controleer welke poort bij dit scherm hoort.
|
||||
2. Klik **Nieuw scherm**. Kies CYD of Guition en geef het scherm een naam,
|
||||
bijvoorbeeld `Keuken`. De apparaatnaam (`keuken`) volgt daaruit; met
|
||||
**aanpassen** kies je een andere.
|
||||
3. Wifi: staan `wifi_ssid` en `wifi_password` al in de ESPHome `secrets.yaml`,
|
||||
dan gebruikt het scherm die automatisch. Ontbreken ze, of bestaat het bestand
|
||||
nog niet, dan vraagt het venster ze eenmalig en zet ESP Screens alleen de
|
||||
ontbrekende regels in `secrets.yaml`; commentaar en andere secrets blijven
|
||||
staan. Een `secrets.yaml` die geen geldige YAML is, herstel je eerst zelf.
|
||||
4. Kies bij **Installeren via** de USB-poort van dit scherm en klik
|
||||
**Installeren**. ESP Screens bewaart het profiel (`keuken.yaml`, met unieke
|
||||
API- en OTA-sleutels) in de ESPHome-map, bouwt de firmware en schrijft die
|
||||
via USB. Het ESPHome-log en de fase (bouwen, schrijven) staan in hetzelfde
|
||||
venster; een eerste build duurt op een Raspberry enkele minuten. Je mag het
|
||||
venster sluiten: de installatie loopt door en staat bij heropenen weer klaar.
|
||||
5. Na **Klaar** toont het venster de API-sleutel met een kopieerknop en de
|
||||
koppelstappen uit hoofdstuk 3. Mislukt de build, dan staat het log open en
|
||||
kun je **Opnieuw proberen**.
|
||||
|
||||
Een USB-kabel aan je laptop is niet zichtbaar als USB-poort van de Raspberry.
|
||||
Gebruik voor die route de ESPHome-CLI op de laptop met je eigen profiel, of de
|
||||
browserinstallatie van ESPHome Device Builder. De stappen hierboven gebruiken
|
||||
uitsluitend ESP Screens op de HA-machine.
|
||||
Elk scherm krijgt zijn eigen profiel: vier schermen betekent vier keer **Nieuw
|
||||
scherm** met vier verschillende namen. Het gedeelde bordpakket is voor alle
|
||||
schermen gelijk; het profiel bevat alleen naam, sleutels en de wifi-verwijzing.
|
||||
Bewaar dat profiel en gebruik het opnieuw bij updates. Voor een bestaand scherm
|
||||
opnieuw **Nieuw scherm** doorlopen maakt nieuwe sleutels aan en is niet de
|
||||
updateroute.
|
||||
|
||||
Geen USB-poort in de lijst? Een kabel aan je laptop is niet zichtbaar voor de
|
||||
Raspberry. Kies dan **Later · alleen het profiel bewaren**: het profiel staat
|
||||
daarna in dezelfde map die ESPHome Device Builder gebruikt, dus je kunt het daar
|
||||
openen en vanuit je browser flashen (**Install → Plug into this computer**), of
|
||||
het later via **Firmware & USB** alsnog aan de HA-machine installeren.
|
||||
|
||||
**CYD:** bij de eerste start verschijnt de kalibratie. Tik het zichtbare kruisje
|
||||
drie keer rustig aan, houd elke tik kort vast en volg steeds het volgende kruisje.
|
||||
@@ -80,19 +79,26 @@ je later opnieuw meten. Voor diagnose en de oudere handmatige installatie:
|
||||
|
||||
**Guition:** de GT911-touchmapping zit in het bordprofiel; er is geen ADC-kalibratie.
|
||||
|
||||
## 4. Koppel het scherm aan Home Assistant
|
||||
## 3. Koppel het scherm aan Home Assistant
|
||||
|
||||
Dit gebeurt in Home Assistant zelf, buiten ESP Screens. Zolang een profiel nog
|
||||
niet aan Home Assistant is toegevoegd, staat het links onder **Mijn schermen**
|
||||
als kaart *nog niet in Home Assistant*, met de knop **Open Apparaten & diensten**
|
||||
en **Kopieer API-sleutel**; het klaar-scherm van **Nieuw scherm** heeft dezelfde
|
||||
knop. De kaart verdwijnt zodra het scherm in de lijst staat.
|
||||
|
||||
1. Open **Instellingen → Apparaten & diensten**. Voeg het ontdekte ESPHome-apparaat
|
||||
toe. Niet ontdekt? Voeg de integratie **ESPHome** handmatig toe met het IP-adres
|
||||
uit de ESPHome-logs, poort 6053.
|
||||
2. Vraagt HA om een encryptiesleutel? Kopieer de waarde van **api → encryption → key**
|
||||
uit je eigen YAML. Gebruik niet het OTA-wachtwoord.
|
||||
van het scherm, poort 6053.
|
||||
2. Vraagt HA om een encryptiesleutel? Plak de API-sleutel die het venster na de
|
||||
installatie toont (ook terug te vinden als **api → encryption → key** in het
|
||||
profiel). Gebruik niet het OTA-wachtwoord.
|
||||
3. Open bij de ESPHome-integratie **Configureren** en zet **Allow the device to
|
||||
perform Home Assistant actions** aan. Zonder deze toestemming verschijnen
|
||||
waarden wel, maar kan het scherm lampen en apparaten niet bedienen.
|
||||
4. Open ESP Screens. Het scherm verschijnt binnen ongeveer 30 seconden.
|
||||
|
||||
## 5. Kies en wijzig je tegels
|
||||
## 4. Kies en wijzig je tegels
|
||||
|
||||
Tip: zet in je ESPHome-YAML onder `wifi:` de regel `power_save_mode: none` (nieuwe
|
||||
wizard-YAML's hebben hem al); het scherm hangt aan het lichtnet en reageert dan zonder
|
||||
@@ -145,7 +151,7 @@ totdat de gegevens opnieuw ontvangen zijn.
|
||||
Je mag indelingen opslaan terwijl een scherm offline is. De app verstuurt ze
|
||||
zodra het scherm terugkomt. De app moet blijven draaien voor actuele tegeldata.
|
||||
|
||||
## 6. Updates zonder je instellingen kwijt te raken
|
||||
## 5. Updates zonder je instellingen kwijt te raken
|
||||
|
||||
| Wat verandert? | Wat doe je? | Wat blijft behouden? |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -177,6 +177,20 @@ staan iconen in de browser uit het midden); draai daarna `generate_packages.py`.
|
||||
De substituties `MDI_GLYPH_*` zijn vervallen: `TILEn_ICON` in handmatige
|
||||
profielen moet uit de set komen. Geen gewijzigde preferences of sleutels.
|
||||
|
||||
### Compatibiliteit 0.2.27 (firmware 0.2.22 blijft)
|
||||
|
||||
Alleen de app. `POST /api/install` (YAML-download) is verwijderd; `POST
|
||||
/api/firmware/profiles` schrijft het profiel, vult ontbrekende wifi-sleutels in
|
||||
`secrets.yaml` aan (alleen de ontbrekende regels, geverifieerd door het resultaat
|
||||
opnieuw te parsen; een ongeldig bestand blijft onaangeraakt) en start met `target`
|
||||
(een gemelde USB-poort) direct `install`. Poort en bouwslot worden vóór het
|
||||
schrijven gecontroleerd. Het antwoord bevat `api_key`; de pagina toont hem eenmalig
|
||||
voor de HA-koppeling. Het jobobject krijgt `stage` (`config`, `compile`, `upload`).
|
||||
`profile_meta` levert extra `screen` (profiel gebruikt het bordpakket van dit
|
||||
project) en `api_key`; `/api/inventory` krijgt de additieve lijst `pending` met
|
||||
zulke profielen zonder gekoppeld scherm. Opslag, protocol, preferences en sleutels
|
||||
ongewijzigd.
|
||||
|
||||
### Compatibiliteit 0.2.26 / firmware 0.2.22
|
||||
|
||||
Opslagversie en tegelprotocol blijven 1. Het layoutbericht krijgt het additieve veld
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Testresultaten app 0.2.27 (2026-09-14, firmware 0.2.22 blijft)
|
||||
|
||||
Alleen de app: "Nieuw scherm" is één venster van aansluiten tot flash, ontbrekende
|
||||
wifi gaat in `secrets.yaml`, en de koppeling in Home Assistant (buiten de add-on)
|
||||
is zichtbaar gemaakt met een knop en een kaart per nog niet gekoppeld profiel.
|
||||
|
||||
## Geautomatiseerd
|
||||
|
||||
- Python: 84 tests OK (`.venv-portal`, aiohttp 3.13.3, libyaml). Nieuw of aangepast:
|
||||
- `test_wifi_reuse`: ontbrekende `wifi_ssid`/`wifi_password` worden aangevuld met
|
||||
behoud van commentaar en andere secrets (ook een lege `wifi_ssid: ""` wordt
|
||||
vervangen, niet gedupliceerd; waarden met `: `, `\` en `"` overleven de
|
||||
round-trip); een ongeldig `secrets.yaml` blijft onaangeraakt en er komt dan geen
|
||||
profiel; `install()` weigert een onbekende poort en een bezet bouwslot vóór er
|
||||
iets geschreven is; `profile_names()` herkent eigen profielen (`screen`) en levert
|
||||
de API-sleutel, een handmatig ESPHome-profiel niet.
|
||||
- `test_portal`: `POST /api/firmware/profiles` met een onbekende `target` geeft 400
|
||||
zonder profiel; zonder `target` maakt het profiel en geeft `api_key` (32 bytes)
|
||||
zonder job; `/api/inventory?light=1` toont het profiel in `pending` met
|
||||
`installed: false`; `/api/firmware` lekt de wifi-waarden niet.
|
||||
- `test_updates`: metadata-cache met de extra velden.
|
||||
- `test_rich_cards.HourlySync` was datumgebonden (vaste 13 september) en faalde
|
||||
sinds 14 september op main; de uurvoorspelling in de managertest volgt nu de
|
||||
echte klok.
|
||||
- `py_compile` van de add-on; `node --check` van `app.js`.
|
||||
|
||||
## In de browser (in-app browser tegen de echte server, nep-CLI)
|
||||
|
||||
`server.py` in `SCREEN_DEV`-modus met een nep-`esphome` op het pad (schrijft
|
||||
compile-/uploadregels, faalt op een vlag) en een nep-USB-poort (`Firmware.ports`
|
||||
gepatcht). Doorlopen en per stap bekeken:
|
||||
|
||||
- Nieuw venster: bordkeuze, naam → apparaatnaam (`Keuken Guition` → `keuken-guition`),
|
||||
wifi-velden zichtbaar zolang `secrets.yaml` ontbreekt; na de eerste installatie
|
||||
"Wifi komt uit je ESPHome secrets.yaml" en geen velden meer. `secrets.yaml` bevat
|
||||
daarna precies `wifi_ssid`/`wifi_password` (0600), het profiel het bordpakket.
|
||||
- Installeren via USB: fase "Firmware bouwen…" → "Firmware naar … schrijven…" (uit
|
||||
`job.stage`), log inklapbaar, daarna "Klaar." met API-sleutel, kopieerknop, de
|
||||
vier koppelstappen en de knop **Open Apparaten & diensten**; CYD-tekst noemt de
|
||||
kalibratie, Guition niet.
|
||||
- Mislukte build: "Installeren mislukt", log open met de foutregel, **Opnieuw
|
||||
proberen** herstart alleen de job (geen tweede profiel) en slaagt na het weghalen
|
||||
van de vlag.
|
||||
- Zonder poort: "Later · alleen het profiel bewaren", knop "Profiel bewaren",
|
||||
klaar-scherm met de vervolgroutes.
|
||||
- Zijbalk: per niet-gekoppeld profiel een kaart ("geïnstalleerd, maar nog niet in
|
||||
Home Assistant" voor een in deze sessie geflasht profiel, anders "nog niet in Home
|
||||
Assistant … Firmware & USB → bestand"), met **Open Apparaten & diensten** (navigeert
|
||||
het bovenste venster naar `/config/integrations/dashboard`; op de dev-server een
|
||||
404, in HA de integratiepagina) en **Kopieer API-sleutel**.
|
||||
|
||||
## Nog niet geverifieerd
|
||||
|
||||
- De echte ESPHome-CLI en USB-detectie (`/dev/serial/by-id`) in de add-on-container
|
||||
op de Yellow, en de eerste echte flash via dit venster.
|
||||
- Of de knop **Open Apparaten & diensten** vanuit het HA-ingress-iframe naar de
|
||||
integratiepagina springt (zelfde origin, dus verwacht wel).
|
||||
- Of Home Assistant het scherm na de flash ontdekt en de kaart in de zijbalk
|
||||
verdwijnt zodra `Tegelinstellingen` in het register staat.
|
||||
@@ -1,3 +1,11 @@
|
||||
## 0.2.27 (firmware 0.2.22 blijft actueel)
|
||||
|
||||
- **Nieuw scherm in één venster**: bord, naam, USB-poort, **Installeren**. Het venster maakt het profiel met unieke API- en OTA-sleutels in de ESPHome-map, bouwt de firmware en schrijft die via USB, met het ESPHome-log en de fase (bouwen, schrijven) in hetzelfde venster. Na afloop staat de API-sleutel klaar met een kopieerknop plus de koppelstappen voor Home Assistant; mislukt de build, dan staat het log open en is er **Opnieuw proberen**. De apparaatnaam volgt uit de naam (`Keuken` → `keuken`) en is aan te passen. Zonder aangesloten scherm kun je alleen het profiel bewaren; het staat dan in dezelfde map als ESPHome Device Builder. Elk scherm krijgt zijn eigen profiel; het venster zegt dat nu ook.
|
||||
- **Waar is mijn scherm?** De koppeling gebeurt in Home Assistant zelf, buiten ESP Screens. Het klaar-scherm van het venster zegt dat nu expliciet, met een knop **Open Apparaten & diensten** die je daarheen brengt. Zolang een ESP Screens-profiel nog niet in Home Assistant staat, toont **Mijn schermen** er een kaart voor ("geïnstalleerd, maar nog niet in Home Assistant", of "nog niet geflasht"), met dezelfde knop, **Kopieer API-sleutel** en de herinnering om bij Configureren "Allow the device to perform Home Assistant actions" aan te zetten (anders ziet het scherm alles, maar bedient het niets). De kaart verdwijnt zodra het scherm in de lijst staat. `/api/inventory` levert daarvoor `pending` (profielen die het bordpakket van dit project gebruiken zonder gekoppeld scherm).
|
||||
- **Wifi zonder handwerk**: ontbreken `wifi_ssid` of `wifi_password` in de ESPHome `secrets.yaml`, of bestaat het bestand niet, dan vraagt het venster ze en zet ESP Screens alleen de ontbrekende regels in het bestand; commentaar en andere secrets blijven staan (de wizard weigerde eerder bij een bestaand bestand zonder wifi). Een ongeldig `secrets.yaml` wordt nog steeds niet aangeraakt.
|
||||
- Kopiëren en downloaden van de installatie-YAML is vervallen (`POST /api/install` bestaat niet meer): het profiel staat al in de ESPHome-map. `POST /api/firmware/profiles` accepteert `target` (USB-poort) en start dan direct de build en flash; poort en vrije bouwslot worden gecontroleerd vóór er iets wordt geschreven, en het antwoord bevat `api_key`. Het firmwarejob-object meldt de lopende fase in `stage`.
|
||||
- Alleen de app verandert; firmware 0.2.22 blijft actueel.
|
||||
|
||||
## 0.2.26 (firmware 0.2.22)
|
||||
|
||||
- **"HA niet verbonden" om de twee minuten opgelost**: sinds 0.2.20 herhaalt de app de indeling elke 120 s, maar de firmware verwachtte binnen 95 s een bericht (gemaakt voor de oude 25 s). Op een rustig scherm stonden alle tegels daardoor 25 tot 45 s per ronde op "Niet beschikbaar". De firmware gebruikt nu ESPHome's eigen API-verbindingsstatus voor "HA niet verbonden" (direct bij wegvallen en terugkeren van Home Assistant) en bewaakt de gegevensstroom apart met het interval dat de app zelf declareert: het layoutbericht bevat `keepalive` (seconden). Pas na twee gemiste rondes plus marge meldt het scherm "ESP Screens niet actief". Zonder het veld (oudere app) rekent de firmware met 120 s. Firmware 0.2.22 voor beide borden; firmware tot 0.2.21 negeert het veld en houdt zijn 95 s.
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
Zie [de volledige 0.2.0-handleiding](https://github.com/MaxGramser/homeassistant_espscreen/blob/main/docs/WHATS_NEW_020.md).
|
||||
|
||||
Open **Nieuw scherm** om een eigen profiel te bewaren. Open **Firmware & USB**
|
||||
om een profiel te controleren, bouwen of installeren. Bestaande ESPHome-profielen
|
||||
in de HA-configmap worden automatisch gevonden. Eerste flash: USB aan de Raspberry
|
||||
en expliciet de bijbehorende poort kiezen. Later: hetzelfde profiel plus het
|
||||
IP-adres van het scherm voor OTA. Bestaande secrets worden niet vervangen.
|
||||
Open **Nieuw scherm** om een scherm te installeren: sluit het via USB aan op de
|
||||
Home Assistant-machine, kies bord, naam en USB-poort en klik **Installeren**.
|
||||
Het profiel met unieke sleutels komt in de ESPHome-map van de HA-configuratie,
|
||||
ontbrekende wifi gaat in `secrets.yaml` (bestaande secrets blijven staan), en de
|
||||
build en de flash lopen in hetzelfde venster; daarna toont het de API-sleutel
|
||||
voor de koppeling. De koppeling zelf doe je in Home Assistant onder **Instellingen → Apparaten & diensten** (knop in het venster en op de kaart *nog niet in Home Assistant* onder Mijn schermen). Elk scherm heeft zijn eigen profiel. **Firmware & USB** is
|
||||
voor bestaande profielen: controleren, bouwen of opnieuw installeren via USB of
|
||||
het IP-adres (OTA). Bestaande ESPHome-profielen in de HA-configmap worden
|
||||
automatisch gevonden.
|
||||
|
||||
Na koppeling via de HA ESPHome-integratie kies je in ESP Screens de tegels.
|
||||
Onder **Tegelinstellingen** staan klikgedrag, grotere waarden en mini-schuiven.
|
||||
|
||||
@@ -10,7 +10,7 @@ import shutil
|
||||
import signal
|
||||
import time
|
||||
import yaml
|
||||
from core import installation_yaml
|
||||
from core import REPO, installation_yaml
|
||||
|
||||
# libyaml parses a 300 KB profile roughly ten times faster than the pure-Python loader.
|
||||
class LenientLoader(yaml.CSafeLoader if getattr(yaml, '__with_libyaml__', False) else yaml.SafeLoader):
|
||||
@@ -28,7 +28,14 @@ def profile_meta(text):
|
||||
if isinstance(value, str) and value.startswith('${') and value.endswith('}'):
|
||||
value = substitutions.get(value[2:-1])
|
||||
return value if isinstance(value, str) else None
|
||||
return {'node': resolve(block.get('name')), 'friendly': resolve(block.get('friendly_name'))}
|
||||
# 'screen': the profile pulls this project's board package, so it is one of ours (not any ESPHome device).
|
||||
packages = data.get('packages') if isinstance(data.get('packages'), dict) else {}
|
||||
ours = any(isinstance(entry, dict) and REPO in str(entry.get('url', '')) for entry in packages.values())
|
||||
api = data.get('api') if isinstance(data.get('api'), dict) else {}
|
||||
encryption = api.get('encryption') if isinstance(api.get('encryption'), dict) else {}
|
||||
key = encryption.get('key')
|
||||
return {'node': resolve(block.get('name')), 'friendly': resolve(block.get('friendly_name')),
|
||||
'screen': ours, 'api_key': key if isinstance(key, str) else None}
|
||||
|
||||
class Firmware:
|
||||
def __init__(self, root, data):
|
||||
@@ -37,6 +44,7 @@ class Firmware:
|
||||
self.task = None
|
||||
self.logs = deque(maxlen=300)
|
||||
self.process = None
|
||||
self.installed = set() # profiles this process flashed successfully; the page nudges pairing for them
|
||||
self._names = {} # file -> (stat signature, meta or None); parsed only when the file changes
|
||||
|
||||
def profiles(self):
|
||||
@@ -101,28 +109,74 @@ class Firmware:
|
||||
return {'available': bool(shutil.which('esphome')), 'profiles': self.profiles(),
|
||||
'ports': self.ports(), 'job': self.job, 'logs': list(self.logs), 'wifi': self.wifi_status()}
|
||||
|
||||
def store_wifi(self, data):
|
||||
"""Put wifi_ssid/wifi_password in secrets.yaml when they are missing. Existing values and
|
||||
every other secret stay as they are; the browser never sees stored values."""
|
||||
wifi = self.wifi_status()
|
||||
if wifi['state'] == 'ready':
|
||||
return
|
||||
if wifi['state'] == 'invalid':
|
||||
raise ValueError('secrets.yaml in de ESPHome-map is geen geldige YAML. Herstel het bestand eerst; het wordt niet overschreven.')
|
||||
values = {}
|
||||
for key in wifi['missing']:
|
||||
value = data.get(key)
|
||||
if not isinstance(value, str) or (key == 'wifi_ssid' and not value.strip()):
|
||||
raise ValueError('Vul de wifi-naam en het wachtwoord in; ze worden in ESPHome secrets.yaml bewaard.')
|
||||
values[key] = value
|
||||
path = self.root / 'secrets.yaml'
|
||||
if wifi['state'] == 'new':
|
||||
with path.open('x') as f:
|
||||
os.chmod(path, 0o600)
|
||||
yaml.safe_dump(values, f, width=4096)
|
||||
return
|
||||
# An existing file with missing keys: replace only that key's own line, or append it.
|
||||
text = path.read_text()
|
||||
for key, value in values.items():
|
||||
line = yaml.safe_dump({key: value}, width=4096).strip()
|
||||
pattern = re.compile(rf'^{key}\s*:.*$', re.M)
|
||||
if pattern.search(text):
|
||||
text = pattern.sub(lambda match: line, text, count=1)
|
||||
else:
|
||||
text = text + ('' if not text or text.endswith('\n') else '\n') + line + '\n'
|
||||
check = yaml.safe_load(text)
|
||||
if not isinstance(check, dict) or any(check.get(key) != value for key, value in values.items()):
|
||||
raise ValueError('Wifi kon niet in secrets.yaml worden gezet. Vul wifi_ssid en wifi_password daar zelf in.')
|
||||
path.write_text(text)
|
||||
|
||||
def create(self, data):
|
||||
"""Write the device profile (unique API/OTA keys) and any missing wifi secrets."""
|
||||
content = installation_yaml(data)
|
||||
self.root.mkdir(parents=True,exist_ok=True)
|
||||
# Existing wifi secrets remain untouched. New owners can supply them once.
|
||||
secret_path = self.root / 'secrets.yaml'
|
||||
wifi = self.wifi_status()
|
||||
if wifi['state'] in ('missing', 'invalid'):
|
||||
raise ValueError('Controleer wifi_ssid en wifi_password in ESPHome secrets.yaml. Bestaande secrets blijven behouden.')
|
||||
if wifi['state'] == 'new':
|
||||
ssid, password = data.get('wifi_ssid'), data.get('wifi_password')
|
||||
if not isinstance(ssid,str) or not ssid.strip() or not isinstance(password,str):
|
||||
raise ValueError('Vul wifi in voor deze eerste installatie.')
|
||||
with secret_path.open('x') as f:
|
||||
os.chmod(secret_path,0o600)
|
||||
yaml.safe_dump({'wifi_ssid':ssid,'wifi_password':password}, f)
|
||||
profile = self.root / (data['name']+'.yaml')
|
||||
if profile.exists() or profile.is_symlink():
|
||||
raise ValueError('Deze naam bestaat al. Gebruik het bestaande profiel voor updates.')
|
||||
self.store_wifi(data)
|
||||
try:
|
||||
with profile.open('x') as f:
|
||||
os.chmod(profile,0o600);f.write(content)
|
||||
except FileExistsError:
|
||||
raise ValueError('Deze naam bestaat al. Gebruik het bestaande profiel voor updates.')
|
||||
return {'file':profile.name, 'yaml':content}
|
||||
key = yaml.load(content, Loader=LenientLoader)['api']['encryption']['key']
|
||||
# The key is what Home Assistant asks for when pairing; the page shows it once.
|
||||
return {'file': profile.name, 'node': data['name'], 'api_key': key}
|
||||
|
||||
def install(self, data):
|
||||
"""Profile plus, when a USB port is chosen, the build and flash in one go.
|
||||
|
||||
The port and the job slot are checked before anything is written, so a refused
|
||||
install leaves no half-made profile behind."""
|
||||
target = data.get('target') or ''
|
||||
if target:
|
||||
if not isinstance(target, str) or target not in self.ports():
|
||||
raise ValueError('Kies de aangesloten USB-poort uit de lijst.')
|
||||
if self.task and not self.task.done():
|
||||
raise ValueError('Er loopt al een build of installatie. Wacht tot die klaar is.')
|
||||
if not shutil.which('esphome'):
|
||||
raise ValueError('ESPHome CLI ontbreekt in deze installatie.')
|
||||
result = self.create(data)
|
||||
if target:
|
||||
result['job'] = self.start({'file': result['file'], 'action': 'install', 'target': target})
|
||||
return result
|
||||
|
||||
def redact(self, text):
|
||||
text = re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]', '', text)
|
||||
@@ -171,6 +225,7 @@ class Firmware:
|
||||
for stage in stages:
|
||||
cmd=['esphome']+(['--quiet'] if stage=='config' else [])+[stage,str(profile)]
|
||||
if stage=='upload': cmd += ['--device',target]
|
||||
self.job['stage']=stage
|
||||
self.logs.append('ESPHome: '+stage)
|
||||
self.process=await asyncio.create_subprocess_exec(*cmd,cwd=self.root,env=env,stdout=asyncio.subprocess.PIPE,stderr=asyncio.subprocess.STDOUT,limit=1024*1024,start_new_session=True)
|
||||
async with asyncio.timeout(7200):
|
||||
@@ -179,6 +234,7 @@ class Firmware:
|
||||
code=await self.process.wait()
|
||||
if code: raise RuntimeError('ESPHome '+stage+' mislukt; bekijk het log.')
|
||||
self.job['state']='success'; self.logs.append('Geslaagd: '+action)
|
||||
if action=='install': self.installed.add(profile.name)
|
||||
except asyncio.CancelledError:
|
||||
self.job['state']='interrupted'
|
||||
raise
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -260,7 +260,62 @@ function renderUpdate(screen) {
|
||||
} else return null;
|
||||
return row;
|
||||
}
|
||||
function openIntegrations() {
|
||||
// Pairing happens in Home Assistant itself. This page lives in HA's ingress iframe,
|
||||
// so send the top window to Apparaten & diensten (same origin); elsewhere open a tab.
|
||||
const path = "/config/integrations/dashboard";
|
||||
try {
|
||||
window.top.location.assign(path);
|
||||
} catch {
|
||||
window.open(path, "_blank");
|
||||
}
|
||||
}
|
||||
async function copyText(text, element) {
|
||||
try {
|
||||
if (!navigator.clipboard || !window.isSecureContext) throw new Error();
|
||||
await navigator.clipboard.writeText(text);
|
||||
toast("API-sleutel gekopieerd.");
|
||||
} catch {
|
||||
if (element) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(element);
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
toast(document.execCommand("copy") ? "API-sleutel gekopieerd." : "De sleutel is geselecteerd. Kopieer met Ctrl+C of Command+C.");
|
||||
}
|
||||
}
|
||||
// Profiles that Home Assistant does not list yet: the freshly flashed screen is not lost, it
|
||||
// still has to be added under Apparaten & diensten, outside this page.
|
||||
function renderPending() {
|
||||
const box = $("#pending");
|
||||
box.replaceChildren();
|
||||
for (const p of inventory.pending || []) {
|
||||
const card = node("div", undefined, "pending");
|
||||
card.append(
|
||||
node("strong", p.friendly),
|
||||
node("small", p.installed
|
||||
? "Geïnstalleerd, maar nog niet in Home Assistant. Dat doe je buiten ESP Screens: voeg het ontdekte ESPHome-apparaat toe onder Instellingen → Apparaten & diensten, plak daar de API-sleutel en zet bij Configureren “Allow the device to perform Home Assistant actions” aan."
|
||||
: `Nog niet in Home Assistant. Al geflasht? Voeg het ESPHome-apparaat toe onder Instellingen → Apparaten & diensten en sta daarna bij Configureren de Home Assistant-acties toe. Nog niet geflasht? Firmware & USB → ${p.file}.`),
|
||||
);
|
||||
const actions = node("div", undefined, "pending-actions");
|
||||
const go = node("button", "Open Apparaten & diensten", "mini");
|
||||
go.type = "button";
|
||||
go.onclick = openIntegrations;
|
||||
actions.append(go);
|
||||
if (p.api_key) {
|
||||
const copy = node("button", "Kopieer API-sleutel", "mini quiet");
|
||||
copy.type = "button";
|
||||
copy.onclick = () => copyText(p.api_key);
|
||||
actions.append(copy);
|
||||
}
|
||||
card.append(actions);
|
||||
box.append(card);
|
||||
}
|
||||
}
|
||||
function renderScreens() {
|
||||
renderPending();
|
||||
// Keep an open address form alive across the periodic refresh.
|
||||
if ($("#screens .screen-host")) {
|
||||
renderUpdates();
|
||||
@@ -976,85 +1031,250 @@ for (const [value, label] of [
|
||||
};
|
||||
$("#filters").append(b);
|
||||
}
|
||||
async function checkWifiSecrets() {
|
||||
const fields = $("#wifi-fields"), status = $("#wifi-status");
|
||||
fields.hidden = true;
|
||||
fields.disabled = true;
|
||||
$("#create-profile").disabled = true;
|
||||
status.textContent = "ESPHome wifi-instellingen controleren…";
|
||||
// ----- Nieuw scherm: profiel, wifi en de eerste flash in één venster -----
|
||||
const installer = {
|
||||
poll: null, view: "setup", file: null, friendly: "", board: "cyd", target: "",
|
||||
apiKey: null, nodeEdited: false, ports: null, jobState: null,
|
||||
};
|
||||
// ESPHome's node-name rule: lowercase ASCII, digits and dashes, starting with a letter.
|
||||
function slug(text) {
|
||||
const clean = text.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "")
|
||||
.replace(/[^a-z0-9]+/g, "-").replace(/^[^a-z]+/, "").slice(0, 30).replace(/-+$/, "");
|
||||
return clean || "scherm";
|
||||
}
|
||||
function portLabel(port) {
|
||||
const id = port.replace(/^\/dev\/serial\/by-id\/usb-/, "").replace(/-if\d+(-port\d+)?$/, "").replace(/_/g, " ");
|
||||
return `USB · ${id === port ? port.replace(/^\/dev\//, "") : id}`;
|
||||
}
|
||||
function syncNode() {
|
||||
const form = $("#install-form");
|
||||
if (!installer.nodeEdited) form.elements.name.value = slug(form.elements.friendly_name.value);
|
||||
$("#node-preview").textContent = form.elements.name.value || "…";
|
||||
}
|
||||
function renderTargets(ports) {
|
||||
const select = $("#install-target");
|
||||
const key = ports.join("\n");
|
||||
if (installer.ports === key) return;
|
||||
installer.ports = key;
|
||||
const current = select.value;
|
||||
const later = node("option", "Later · alleen het profiel bewaren");
|
||||
later.value = "";
|
||||
select.replaceChildren(...ports.map((p) => { const o = node("option", portLabel(p)); o.value = p; return o; }), later);
|
||||
select.value = ports.includes(current) ? current : ports[0] || "";
|
||||
renderTargetHint();
|
||||
}
|
||||
function renderTargetHint() {
|
||||
const select = $("#install-target");
|
||||
const ports = select.options.length - 1;
|
||||
$("#target-hint").textContent = !ports
|
||||
? "Geen USB-poort gevonden. Sluit het scherm met een datakabel aan op de Home Assistant-machine; de lijst ververst vanzelf."
|
||||
: !select.value
|
||||
? "Het profiel komt in de ESPHome-map. Installeren kan later via Firmware & USB, of vanuit ESPHome Device Builder."
|
||||
: ports > 1
|
||||
? "Meer dan één bord aangesloten: kies de poort van dit scherm."
|
||||
: "Eén keer via USB; daarna gaat alles draadloos.";
|
||||
$("#install-go").textContent = select.value ? "Installeren" : "Profiel bewaren";
|
||||
}
|
||||
function renderWifi(wifi) {
|
||||
const fields = $("#wifi-fields");
|
||||
const ask = wifi?.state === "new" || wifi?.state === "missing";
|
||||
fields.hidden = !ask;
|
||||
fields.disabled = !ask;
|
||||
if (ask) {
|
||||
const missing = wifi.missing || [];
|
||||
$("#wifi-ssid-label").hidden = !missing.includes("wifi_ssid");
|
||||
$("#wifi-password-label").hidden = !missing.includes("wifi_password");
|
||||
$("#wifi-status").textContent = wifi.state === "new"
|
||||
? "Eén keer invullen: ESP Screens bewaart dit in ESPHome secrets.yaml, volgende schermen gebruiken het automatisch."
|
||||
: "Je ESPHome secrets.yaml mist nog wifi-gegevens. ESP Screens vult alleen de ontbrekende regels aan.";
|
||||
}
|
||||
return wifi?.state === "ready"
|
||||
? "Wifi komt uit je ESPHome secrets.yaml."
|
||||
: wifi?.state === "invalid"
|
||||
? "secrets.yaml in de ESPHome-map is geen geldige YAML. Herstel het bestand eerst; het wordt niet overschreven."
|
||||
: "";
|
||||
}
|
||||
async function installerRefresh() {
|
||||
let data;
|
||||
try {
|
||||
const {wifi} = await (await api("firmware")).json();
|
||||
if (wifi?.state === "ready") {
|
||||
status.textContent = "✓ Bestaande ESPHome-wifi gevonden. Dit scherm gebruikt automatisch wifi_ssid en wifi_password uit je secrets.yaml.";
|
||||
$("#create-profile").disabled = false;
|
||||
} else if (wifi?.state === "new") {
|
||||
status.textContent = "Nog geen ESPHome-secrets gevonden. Vul wifi één keer in; volgende schermen gebruiken deze gegevens automatisch.";
|
||||
fields.hidden = false;
|
||||
fields.disabled = false;
|
||||
$("#create-profile").disabled = false;
|
||||
} else {
|
||||
status.textContent = "Controleer wifi_ssid en wifi_password in ESPHome secrets.yaml. Je bestaande bestand blijft behouden. Sluit en open deze wizard opnieuw na aanpassen.";
|
||||
}
|
||||
} catch {
|
||||
status.textContent = "ESPHome-secrets konden niet worden gecontroleerd. Sluit en open de wizard opnieuw om het nogmaals te proberen.";
|
||||
data = await (await api("firmware")).json();
|
||||
} catch (e) {
|
||||
$("#install-note").textContent = e.message;
|
||||
return;
|
||||
}
|
||||
const job = data.job;
|
||||
const ours = job && installer.file && job.file === installer.file;
|
||||
if (ours) installer.jobState = job.state;
|
||||
if (installer.view === "setup") {
|
||||
if (ours && job.state === "running") return showProgress(job, data.logs);
|
||||
renderTargets(data.ports || []);
|
||||
const wifiNote = renderWifi(data.wifi);
|
||||
const busy = job?.state === "running";
|
||||
const flashing = !!$("#install-target").value;
|
||||
$("#install-note").textContent = busy
|
||||
? `Er loopt al een build of installatie (${job.file}). Wacht tot die klaar is.`
|
||||
: !data.available && flashing
|
||||
? "ESPHome CLI ontbreekt in deze installatie; alleen het profiel bewaren kan."
|
||||
: wifiNote;
|
||||
$("#install-go").disabled = data.wifi?.state === "invalid" || (flashing && (busy || !data.available));
|
||||
} else if (installer.view === "progress" && ours) {
|
||||
renderProgress(job, data.logs);
|
||||
}
|
||||
}
|
||||
$("#new-screen").onclick = $("#start").onclick = () => {
|
||||
function showProgress(job, logs) {
|
||||
installer.view = "progress";
|
||||
installer.jobState = job.state;
|
||||
$("#install-setup").hidden = true;
|
||||
$("#install-progress").hidden = false;
|
||||
$("#install-log-wrap").hidden = false;
|
||||
renderProgress(job, logs);
|
||||
}
|
||||
function outcome(ok) {
|
||||
$("#progress-spin").hidden = true;
|
||||
$("#progress-mark").hidden = false;
|
||||
$("#progress-mark").textContent = ok ? "✓" : "✕";
|
||||
$("#progress-mark").className = `outcome ${ok ? "ok" : "bad"}`;
|
||||
}
|
||||
function renderProgress(job, logs) {
|
||||
const running = job.state === "running";
|
||||
const ok = job.state === "success";
|
||||
if (running) {
|
||||
$("#progress-spin").hidden = false;
|
||||
$("#progress-mark").hidden = true;
|
||||
} else outcome(ok);
|
||||
$("#install-title").textContent = running ? "Even geduld…" : ok ? "Klaar." : "Dat lukte niet.";
|
||||
$("#progress-title").textContent = running
|
||||
? job.stage === "upload" ? `Firmware naar ${installer.friendly} schrijven…` : "Firmware bouwen…"
|
||||
: ok ? `Firmware staat op ${installer.friendly}` : "Installeren mislukt";
|
||||
$("#progress-detail").textContent = running
|
||||
? job.stage === "upload"
|
||||
? "Haal de USB-kabel nog niet los."
|
||||
: "Een eerste build duurt op een Raspberry enkele minuten. Je mag dit venster sluiten: de installatie loopt door en je vindt hem terug onder Nieuw scherm."
|
||||
: ok
|
||||
? `Het scherm start op en verbindt met je wifi.${installer.board === "cyd" ? " De CYD vraagt eerst om een touch-kalibratie: tik de kruisjes aan." : ""} Koppel het nu aan Home Assistant:`
|
||||
: logs.filter((l) => /error/i.test(l)).pop() || logs.filter((l) => /failed|mislukt|fout/i.test(l)).pop() || "Bekijk het log hieronder.";
|
||||
const pre = $("#install-log");
|
||||
const stick = pre.scrollTop + pre.clientHeight >= pre.scrollHeight - 8;
|
||||
pre.textContent = logs.join("\n");
|
||||
if (stick) pre.scrollTop = pre.scrollHeight;
|
||||
if (!running && !ok) $("#install-log-wrap").open = true;
|
||||
$("#install-result").hidden = !ok;
|
||||
if (ok) renderSteps();
|
||||
$("#install-retry").hidden = running || ok;
|
||||
$("#install-close").textContent = ok ? "Klaar" : "Sluiten";
|
||||
$("#install-close").classList.toggle("quiet", !ok);
|
||||
}
|
||||
function showSaved() {
|
||||
installer.view = "done";
|
||||
$("#install-setup").hidden = true;
|
||||
$("#install-progress").hidden = false;
|
||||
outcome(true);
|
||||
$("#install-title").textContent = "Profiel bewaard.";
|
||||
$("#progress-title").textContent = `${installer.file} staat in de ESPHome-map`;
|
||||
$("#progress-detail").textContent =
|
||||
"Installeren kan zodra het scherm aan de Home Assistant-machine hangt: Firmware & USB → dit profiel → USB-poort → Bouwen & installeren. Of open het profiel in ESPHome Device Builder (dezelfde map) en flash vanuit je browser. Bewaar de API-sleutel voor de koppeling:";
|
||||
$("#install-result").hidden = false;
|
||||
renderSteps();
|
||||
$("#install-log-wrap").hidden = true;
|
||||
$("#install-retry").hidden = true;
|
||||
$("#install-close").textContent = "Klaar";
|
||||
$("#install-close").classList.remove("quiet");
|
||||
}
|
||||
function renderSteps() {
|
||||
$("#api-key").textContent = installer.apiKey || "";
|
||||
const steps = [
|
||||
["Ga naar Home Assistant → Instellingen → Apparaten & diensten.", ` Dit gebeurt buiten ESP Screens. Home Assistant ontdekt ${installer.friendly} als ESPHome-apparaat; klik op Toevoegen. Niet ontdekt? Voeg ESPHome handmatig toe met het IP-adres van het scherm. `],
|
||||
["Plak de API-sleutel", " hierboven zodra Home Assistant om een encryptiesleutel vraagt."],
|
||||
["Sta HA-acties toe:", " ESPHome-integratie → Configureren → “Allow the device to perform Home Assistant actions”. Zonder dit ziet het scherm alles, maar bedient het niets."],
|
||||
["Kies je tegels.", " Terug in ESP Screens verschijnt het scherm binnen een halve minuut in de lijst links; tot die tijd staat het daar als “nog niet in Home Assistant”."],
|
||||
];
|
||||
$("#install-steps").replaceChildren(...steps.map(([b, t], i) => {
|
||||
const li = node("li");
|
||||
li.append(node("b", b), t);
|
||||
if (i === 0) {
|
||||
const go = node("button", "Open Apparaten & diensten", "mini");
|
||||
go.type = "button";
|
||||
go.onclick = openIntegrations;
|
||||
li.append(go);
|
||||
}
|
||||
return li;
|
||||
}));
|
||||
}
|
||||
function resetInstaller() {
|
||||
Object.assign(installer, { view: "setup", file: null, apiKey: null, nodeEdited: false, ports: null, jobState: null, target: "" });
|
||||
const form = $("#install-form");
|
||||
form.reset();
|
||||
$("#node-label").hidden = true;
|
||||
$("#install-setup").hidden = false;
|
||||
$("#install-progress").hidden = true;
|
||||
$("#install-result").hidden = true;
|
||||
$("#install-log-wrap").open = false;
|
||||
$("#install-log").textContent = "";
|
||||
$("#install-title").textContent = "Aansluiten en installeren.";
|
||||
$("#install-status").textContent = "";
|
||||
$("#install-note").textContent = "";
|
||||
$("#install-target").replaceChildren();
|
||||
$("#target-hint").textContent = "";
|
||||
$("#install-close").classList.add("quiet");
|
||||
syncNode();
|
||||
}
|
||||
function openInstaller() {
|
||||
if (installer.view !== "progress") resetInstaller();
|
||||
$("#installer").showModal();
|
||||
checkWifiSecrets();
|
||||
installerRefresh();
|
||||
clearInterval(installer.poll);
|
||||
installer.poll = setInterval(installerRefresh, 3000);
|
||||
}
|
||||
$("#new-screen").onclick = $("#start").onclick = openInstaller;
|
||||
$("#close-install").onclick = $("#install-close").onclick = () => $("#installer").close();
|
||||
$("#installer").addEventListener("close", () => {
|
||||
clearInterval(installer.poll);
|
||||
// A finished job is shown once; the next open starts a fresh form.
|
||||
if (installer.view === "progress" && installer.jobState !== "running") installer.view = "done";
|
||||
});
|
||||
$("#install-target").onchange = () => { renderTargetHint(); installerRefresh(); };
|
||||
$("#install-form").elements.friendly_name.oninput = syncNode;
|
||||
$("#install-form").elements.name.oninput = () => { installer.nodeEdited = true; syncNode(); };
|
||||
$("#edit-node").onclick = () => {
|
||||
installer.nodeEdited = true;
|
||||
$("#node-label").hidden = false;
|
||||
$("#install-form").elements.name.focus();
|
||||
};
|
||||
$("#close-install").onclick = () => $("#installer").close();
|
||||
let generatedName = "";
|
||||
$("#install-form").onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
$("#download").disabled = true;
|
||||
const form = e.target;
|
||||
syncNode();
|
||||
if (!form.reportValidity()) return;
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
$("#install-go").disabled = true;
|
||||
$("#install-status").textContent = "";
|
||||
try {
|
||||
const data = Object.fromEntries(new FormData(e.target));
|
||||
const response = await api("install", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
$("#yaml-output").value = await response.text();
|
||||
generatedName = data.name;
|
||||
$("#yaml-result").hidden = false;
|
||||
$("#install-status").textContent =
|
||||
"Je YAML is klaar. Kopieer hem naar ESPHome en bewaar het bestand met sleutels.";
|
||||
} catch (e) {
|
||||
$("#install-status").textContent = e.message;
|
||||
const result = await (await api("firmware/profiles", { method: "POST", body: JSON.stringify(data) })).json();
|
||||
Object.assign(installer, { file: result.file, apiKey: result.api_key, friendly: data.friendly_name.trim(), board: data.board, target: data.target });
|
||||
form.elements.wifi_password.value = "";
|
||||
if (result.job) showProgress(result.job, []);
|
||||
else showSaved();
|
||||
} catch (err) {
|
||||
$("#install-status").textContent = err.message;
|
||||
} finally {
|
||||
$("#download").disabled = false;
|
||||
$("#install-go").disabled = false;
|
||||
}
|
||||
};
|
||||
$("#copy-yaml").onclick = async () => {
|
||||
$("#install-retry").onclick = async () => {
|
||||
try {
|
||||
if (navigator.clipboard && window.isSecureContext)
|
||||
await navigator.clipboard.writeText($("#yaml-output").value);
|
||||
else {
|
||||
$("#yaml-output").select();
|
||||
if (!document.execCommand("copy")) throw new Error();
|
||||
}
|
||||
$("#install-status").textContent =
|
||||
"Gekopieerd. Plak dit bij Edit in ESPHome Device Builder.";
|
||||
} catch {
|
||||
$("#yaml-output").select();
|
||||
$("#install-status").textContent =
|
||||
"De tekst is geselecteerd. Kopieer met Ctrl+C of Command+C.";
|
||||
const { ports } = await (await api("firmware")).json();
|
||||
// The board may have been replugged; a single visible port is unambiguous.
|
||||
if (!ports.includes(installer.target) && ports.length === 1) installer.target = ports[0];
|
||||
const job = await (await api("firmware/jobs", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ file: installer.file, action: "install", target: installer.target }),
|
||||
})).json();
|
||||
showProgress(job, []);
|
||||
} catch (err) {
|
||||
toast(err.message);
|
||||
}
|
||||
};
|
||||
$("#download-yaml").onclick = () => {
|
||||
const url = URL.createObjectURL(
|
||||
new Blob([$("#yaml-output").value], { type: "text/plain" }),
|
||||
);
|
||||
const a = node("a");
|
||||
a.href = url;
|
||||
a.download = `${generatedName}.yaml`;
|
||||
document.body.append(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 30000);
|
||||
$("#install-status").textContent =
|
||||
"Download gestart. Blokkeert je browser deze? Gebruik Kopieer YAML.";
|
||||
};
|
||||
$("#copy-key").onclick = () => copyText(installer.apiKey || "", $("#api-key"));
|
||||
window.addEventListener("beforeunload", (e) => {
|
||||
if (dirty) {
|
||||
e.preventDefault();
|
||||
@@ -1184,26 +1404,6 @@ for (const action of ["validate", "build", "install"])
|
||||
toast(e.message);
|
||||
}
|
||||
};
|
||||
$("#create-profile").onclick = async () => {
|
||||
const form = $("#install-form");
|
||||
if (!form.reportValidity()) return;
|
||||
try {
|
||||
const data = await (
|
||||
await api("firmware/profiles", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(Object.fromEntries(new FormData(form))),
|
||||
})
|
||||
).json();
|
||||
generatedName = data.file.replace(/\.yaml$/, " ").trim();
|
||||
$("#yaml-output").value = data.yaml;
|
||||
$("#yaml-result").hidden = false;
|
||||
$("#install-status").textContent =
|
||||
`${data.file} bewaard. Open Firmware & USB om te installeren.`;
|
||||
form.elements.wifi_password.value = "";
|
||||
} catch (e) {
|
||||
toast(e.message);
|
||||
}
|
||||
};
|
||||
function openSection(id) {
|
||||
const section = $(id);
|
||||
if (section.tagName === "DETAILS") section.open = true;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<button id="new-screen" class="tool tool-primary">
|
||||
<span class="tool-icon" aria-hidden="true">+</span>
|
||||
<span class="tool-text"
|
||||
><b>Nieuw scherm</b><small>Koppel een CYD of Guition</small></span
|
||||
><b>Nieuw scherm</b><small>Aansluiten en installeren</small></span
|
||||
>
|
||||
</button>
|
||||
<button id="open-firmware" class="tool">
|
||||
@@ -47,9 +47,10 @@
|
||||
</div>
|
||||
<ol class="howto" aria-label="Zo werkt het">
|
||||
<li>
|
||||
<b>1 · Koppelen</b><span
|
||||
>Kies <b>Nieuw scherm</b> voor de installatie-YAML met eigen
|
||||
sleutels. Eén keer via USB flashen, daarna gaat alles draadloos.</span
|
||||
<b>1 · Installeren</b><span
|
||||
>Sluit het scherm via USB aan op de Home Assistant-machine en kies
|
||||
<b>Nieuw scherm</b>: naam, wifi en klaar. Eén keer via USB, daarna
|
||||
gaat alles draadloos.</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
@@ -61,7 +62,8 @@
|
||||
<li>
|
||||
<b>3 · Opslaan</b><span
|
||||
>Met <b>Opslaan & naar scherm</b> staat het direct op je scherm.
|
||||
Nieuwe schermfuncties installeer je via <b>Firmware & USB</b>.</span
|
||||
Nieuwe schermfuncties komen met <b>Bijwerken</b> draadloos op je
|
||||
scherm.</span
|
||||
>
|
||||
</li>
|
||||
</ol>
|
||||
@@ -69,9 +71,10 @@
|
||||
<aside>
|
||||
<h2>Mijn schermen</h2>
|
||||
<div id="screens"></div>
|
||||
<div id="pending"></div>
|
||||
<p class="hint">
|
||||
Nieuw scherm gekoppeld? Het verschijnt hier automatisch binnen 30
|
||||
seconden.
|
||||
Nieuw scherm gekoppeld in Home Assistant? Het verschijnt hier
|
||||
automatisch binnen 30 seconden.
|
||||
</p>
|
||||
<button class="quiet" id="refresh">↻ Vernieuwen</button>
|
||||
<div class="updates" id="updates" hidden>
|
||||
@@ -93,8 +96,8 @@
|
||||
<span class="empty-icon">▦</span>
|
||||
<h2>Begin met een scherm</h2>
|
||||
<p>
|
||||
Sluit een CYD of Guition aan. We maken de installatie-YAML voor je
|
||||
en helpen je met de eerste flash.
|
||||
Sluit een CYD of Guition via USB aan op je Home Assistant-machine.
|
||||
Profiel, firmware en de eerste flash gebeuren in één venster.
|
||||
</p>
|
||||
<button id="start">Nieuw scherm installeren</button>
|
||||
</section>
|
||||
@@ -184,8 +187,8 @@
|
||||
</section>
|
||||
</div>
|
||||
<p class="firmware-link">
|
||||
Firmware bouwen en via USB of wifi installeren kan via
|
||||
<b>Firmware & USB</b> bovenaan. Je bestaande YAML en secrets blijven
|
||||
Een bestaand profiel opnieuw bouwen of via USB of wifi installeren kan
|
||||
via <b>Firmware & USB</b> bovenaan. Je eigen YAML en secrets blijven
|
||||
bewaard.
|
||||
</p>
|
||||
<details class="help" id="help">
|
||||
@@ -206,11 +209,13 @@
|
||||
tegel op <b>Dubbelbreed</b> voor meer ruimte.
|
||||
</p>
|
||||
<p>
|
||||
Nieuwe firmware bouw en installeer je via <b>Firmware & USB</b> met
|
||||
de ingebouwde ESPHome CLI: kies je eigen profiel, dan een USB-poort of
|
||||
wifi, en klik op <b>Bouwen & installeren</b>. Je tegels blijven in
|
||||
deze app bewaard; een offline scherm krijgt de wijzigingen zodra het
|
||||
terug is.
|
||||
Een nieuw scherm installeer je via <b>Nieuw scherm</b>: het profiel
|
||||
met eigen sleutels, ontbrekende wifi in ESPHome secrets.yaml, de build
|
||||
en de USB-flash gebeuren in één venster, met de API-sleutel voor de
|
||||
koppeling aan het eind. Nieuwe firmware voor bestaande schermen komt
|
||||
via <b>Bijwerken</b> in de lijst, of handmatig via
|
||||
<b>Firmware & USB</b>. Je tegels blijven in deze app bewaard; een
|
||||
offline scherm krijgt de wijzigingen zodra het terug is.
|
||||
</p>
|
||||
</details>
|
||||
</main>
|
||||
@@ -219,7 +224,7 @@
|
||||
<div class="dialog-head">
|
||||
<div>
|
||||
<p class="eyebrow">NIEUW SCHERM</p>
|
||||
<h2>Van USB naar aan de muur.</h2>
|
||||
<h2 id="install-title">Aansluiten en installeren.</h2>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -230,104 +235,102 @@
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<p>
|
||||
Kies je scherm. Elk bestand krijgt eigen sleutels voor een veilige
|
||||
koppeling en draadloze updates.
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend>Welk scherm heb je?</legend>
|
||||
<label class="board"
|
||||
><input type="radio" name="board" value="cyd" checked /><span
|
||||
><b>CYD · 2,8 inch</b
|
||||
><small>ESP32-2432S028 · 320 × 240</small></span
|
||||
></label
|
||||
><label class="board"
|
||||
><input type="radio" name="board" value="guition" /><span
|
||||
><b>Guition · 4 inch</b
|
||||
><small>ESP32-S3-4848S040 · 480 × 480 · GT911</small></span
|
||||
<div id="install-setup">
|
||||
<p>
|
||||
Sluit het scherm met een USB-datakabel aan op de machine waarop Home
|
||||
Assistant draait. ESP Screens maakt een eigen profiel met unieke
|
||||
sleutels, bouwt de firmware en zet die direct op het scherm.
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend>Welk scherm heb je?</legend>
|
||||
<label class="board"
|
||||
><input type="radio" name="board" value="cyd" checked /><span
|
||||
><b>CYD · 2,8 inch</b
|
||||
><small>ESP32-2432S028 · 320 × 240</small></span
|
||||
></label
|
||||
><label class="board"
|
||||
><input type="radio" name="board" value="guition" /><span
|
||||
><b>Guition · 4 inch</b
|
||||
><small>ESP32-S3-4848S040 · 480 × 480 · GT911</small></span
|
||||
></label
|
||||
>
|
||||
</fieldset>
|
||||
<label
|
||||
>Naam<input
|
||||
name="friendly_name"
|
||||
required
|
||||
maxlength="60"
|
||||
placeholder="Scherm woonkamer"
|
||||
autocomplete="off" /><small class="node-line"
|
||||
>Apparaatnaam <code id="node-preview">scherm</code
|
||||
><button type="button" class="link" id="edit-node">
|
||||
aanpassen
|
||||
</button></small
|
||||
><small
|
||||
>Elk scherm krijgt een eigen profiel met eigen sleutels. Vier
|
||||
schermen? Doorloop dit vier keer met een andere naam.</small
|
||||
></label
|
||||
>
|
||||
</fieldset>
|
||||
<label
|
||||
>Herkenbare naam<input
|
||||
name="friendly_name"
|
||||
required
|
||||
maxlength="60"
|
||||
placeholder="Scherm woonkamer" /></label
|
||||
><label
|
||||
>Unieke apparaatnaam<input
|
||||
name="name"
|
||||
required
|
||||
pattern="[a-z][a-z0-9-]{0,29}"
|
||||
maxlength="30"
|
||||
placeholder="scherm-woonkamer"
|
||||
/><small
|
||||
>Kleine letters, cijfers en streepjes. Elk scherm een andere
|
||||
naam.</small
|
||||
></label>
|
||||
<p id="wifi-status" role="status"></p>
|
||||
<fieldset id="wifi-fields" hidden disabled>
|
||||
<legend>Wifi voor je eerste ESPHome-apparaat</legend>
|
||||
<label
|
||||
>Wifi-naam<input
|
||||
name="wifi_ssid"
|
||||
autocomplete="off"
|
||||
/></label>
|
||||
<label
|
||||
>Wifiwachtwoord<input
|
||||
name="wifi_password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
/></label>
|
||||
</fieldset>
|
||||
<button type="button" id="create-profile">
|
||||
Bewaar profiel in ESP Screens
|
||||
</button>
|
||||
<button type="submit" id="download">Maak installatie-YAML</button>
|
||||
<p id="install-status" role="status"></p>
|
||||
<div id="yaml-result" hidden>
|
||||
<label id="node-label" hidden
|
||||
>Apparaatnaam<input
|
||||
name="name"
|
||||
pattern="[a-z][a-z0-9-]{0,29}"
|
||||
maxlength="30"
|
||||
autocomplete="off" /><small
|
||||
>Kleine letters, cijfers en streepjes. Elk scherm een andere
|
||||
naam.</small
|
||||
></label
|
||||
>
|
||||
<fieldset id="wifi-fields" class="wifi" hidden disabled>
|
||||
<legend>Wifi</legend>
|
||||
<p class="hint" id="wifi-status"></p>
|
||||
<label id="wifi-ssid-label"
|
||||
>Wifi-naam (2,4 GHz)<input name="wifi_ssid" autocomplete="off"
|
||||
/></label>
|
||||
<label id="wifi-password-label"
|
||||
>Wifiwachtwoord<input
|
||||
name="wifi_password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
/></label>
|
||||
</fieldset>
|
||||
<label
|
||||
>Je eigen ESPHome-YAML<textarea
|
||||
id="yaml-output"
|
||||
readonly
|
||||
rows="9"
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
</label>
|
||||
<div class="yaml-actions">
|
||||
<button type="button" id="copy-yaml">Kopieer YAML</button
|
||||
><button type="button" class="quiet" id="download-yaml">
|
||||
Download bestand
|
||||
>Installeren via<select name="target" id="install-target"></select
|
||||
><small id="target-hint"></small
|
||||
></label>
|
||||
<p class="hint" id="install-note"></p>
|
||||
<div class="install-actions">
|
||||
<button type="submit" id="install-go">Installeren</button>
|
||||
<span id="install-status" role="status"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="install-progress" hidden>
|
||||
<div class="progress-head">
|
||||
<span class="spin" id="progress-spin"></span>
|
||||
<span class="outcome" id="progress-mark" hidden></span>
|
||||
<strong id="progress-title"></strong>
|
||||
</div>
|
||||
<p id="progress-detail"></p>
|
||||
<div id="install-result" hidden>
|
||||
<div class="key-box">
|
||||
<span>API-sleutel</span><code id="api-key"></code>
|
||||
<button type="button" class="mini" id="copy-key">Kopieer</button>
|
||||
</div>
|
||||
<ol class="steps" id="install-steps"></ol>
|
||||
</div>
|
||||
<details id="install-log-wrap">
|
||||
<summary>Log van ESPHome</summary>
|
||||
<pre id="install-log"></pre>
|
||||
</details>
|
||||
<div class="install-actions">
|
||||
<button type="button" id="install-retry" hidden>
|
||||
Opnieuw proberen
|
||||
</button>
|
||||
<button type="button" class="quiet" id="install-close">
|
||||
Sluiten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ol class="steps">
|
||||
<li>
|
||||
<b>Bewaar je profiel.</b> Bestaande wifi-secrets worden behouden;
|
||||
bij een eerste installatie vul je wifi hierboven in.
|
||||
</li>
|
||||
<li>
|
||||
<b>Open Firmware & USB.</b> Kies dit profiel, sluit het scherm op je
|
||||
Raspberry aan en selecteer de juiste USB-poort. Kies Bouwen &
|
||||
installeren.
|
||||
</li>
|
||||
<li>
|
||||
<b>Koppel in HA → Apparaten & diensten → ESPHome.</b> Gebruik de
|
||||
API-sleutel uit je eigen YAML en sta HA-acties toe.
|
||||
</li>
|
||||
<li>
|
||||
<b>Kies hier je tegels.</b> CYD vraagt bij de eerste start om
|
||||
kalibratie; Guition niet.
|
||||
</li>
|
||||
<li>
|
||||
<b>Later updaten?</b> Kies hetzelfde profiel en het IP-adres van je
|
||||
scherm in Firmware & USB.
|
||||
</li>
|
||||
</ol>
|
||||
<p class="hint">
|
||||
Bewaar je YAML met sleutels. Gebruik later Firmware & USB met het
|
||||
IP-adres voor draadloze updates. Tegels wijzigen doe je hier.
|
||||
</p>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="firmware-dialog">
|
||||
|
||||
@@ -989,3 +989,145 @@ pre {
|
||||
.icon-choice.icon-auto .mdi { font-size: 22px; }
|
||||
.icon-picker .hint { margin: 0; }
|
||||
.icon-hint { font-weight: 400; }
|
||||
|
||||
/* Nieuw scherm: profiel, wifi en de eerste flash in één venster. */
|
||||
.link {
|
||||
background: none;
|
||||
color: var(--accent);
|
||||
padding: 0 0 0 8px;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
}
|
||||
.node-line code {
|
||||
background: var(--soft);
|
||||
padding: 1px 6px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
#installer .wifi {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0;
|
||||
margin: 0 0 16px;
|
||||
padding: 12px 16px 2px;
|
||||
border: 1px solid #f4e3c2;
|
||||
background: #fff9ee;
|
||||
border-radius: 12px;
|
||||
}
|
||||
#installer .wifi legend {
|
||||
padding: 0 4px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
#installer .wifi .hint {
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
#installer select {
|
||||
margin-top: 7px;
|
||||
padding: 11px 12px;
|
||||
border-radius: 10px;
|
||||
border-color: #cdd5df;
|
||||
}
|
||||
.install-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
#install-status {
|
||||
color: #b23b3b;
|
||||
font-size: 13px;
|
||||
}
|
||||
#install-note:empty {
|
||||
display: none;
|
||||
}
|
||||
.progress-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 17px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.progress-head .spin {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
border-width: 3px;
|
||||
}
|
||||
.outcome {
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
font-weight: 700;
|
||||
}
|
||||
.outcome.ok {
|
||||
color: #21704b;
|
||||
}
|
||||
.outcome.bad {
|
||||
color: #b23b3b;
|
||||
}
|
||||
.key-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: #f7f9fc;
|
||||
margin: 4px 0 16px;
|
||||
}
|
||||
.key-box span {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
}
|
||||
.key-box code {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 12px;
|
||||
}
|
||||
#install-log-wrap {
|
||||
margin-top: 12px;
|
||||
}
|
||||
#install-log-wrap summary {
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
}
|
||||
#install-log {
|
||||
max-height: 240px;
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
|
||||
/* Profiles that Home Assistant does not list yet: pairing happens outside this page. */
|
||||
#pending:empty {
|
||||
display: none;
|
||||
}
|
||||
.pending {
|
||||
border: 1px dashed #b9cbec;
|
||||
border-radius: 14px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 10px;
|
||||
background: #f8fbff;
|
||||
}
|
||||
.pending strong {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
}
|
||||
.pending small {
|
||||
margin: 3px 0 10px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.pending-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.pending-actions .quiet {
|
||||
padding: 5px 9px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.steps .mini {
|
||||
display: inline-block;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: ESP Screen Manager
|
||||
version: "0.2.26"
|
||||
version: "0.2.27"
|
||||
slug: esp_screen_manager
|
||||
description: Kies tegels voor je CYD en Guition touchscreen, zonder opnieuw flashen.
|
||||
url: https://github.com/MaxGramser/homeassistant_espscreen
|
||||
|
||||
+17
-2
@@ -154,9 +154,24 @@ class ManagerTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(response.status,400)
|
||||
response=await client.put('/api/screens/text.screen',headers=headers,json={'title':'Test','tiles':[]})
|
||||
self.assertEqual(response.status,200)
|
||||
response=await client.post('/api/install',headers=headers,json={'board':'guition','name':'screen-new','friendly_name':'Nieuw'})
|
||||
from firmware import Firmware
|
||||
m.firmware=Firmware(Path(temp)/'esphome',Path(temp))
|
||||
profile={'board':'guition','name':'screen-new','friendly_name':'Nieuw','wifi_ssid':'net','wifi_password':'pw'}
|
||||
# A refused USB target leaves no profile behind.
|
||||
response=await client.post('/api/firmware/profiles',headers=headers,json={**profile,'target':'/dev/ttyUSB9'})
|
||||
self.assertEqual(response.status,400)
|
||||
self.assertFalse((Path(temp)/'esphome'/'screen-new.yaml').exists())
|
||||
response=await client.post('/api/firmware/profiles',headers=headers,json=profile)
|
||||
self.assertEqual(response.status,200)
|
||||
self.assertEqual(response.headers['Cache-Control'],'no-store')
|
||||
self.assertIn('packages/guition.yaml',await response.text())
|
||||
result=await response.json()
|
||||
self.assertEqual(result['file'],'screen-new.yaml')
|
||||
self.assertEqual(len(base64.b64decode(result['api_key'])),32)
|
||||
self.assertNotIn('job',result)
|
||||
self.assertIn('packages/guition.yaml',(Path(temp)/'esphome'/'screen-new.yaml').read_text())
|
||||
self.assertNotIn('pw',json.dumps((await (await client.get('/api/firmware')).json())['wifi']))
|
||||
# Until Home Assistant lists the screen, the page shows the profile as "not yet in HA" with its key.
|
||||
pending=(await (await client.get('/api/inventory?light=1')).json())['pending']
|
||||
self.assertEqual([(p['file'],p['friendly'],p['installed'],p['api_key']==result['api_key']) for p in pending],[('screen-new.yaml','Nieuw',False,True)])
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Weather card data (hours, rain) and last-run times for scenes, scripts and buttons."""
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -16,8 +16,10 @@ TZ = ZoneInfo('Europe/Amsterdam')
|
||||
NOW = datetime(2026, 9, 13, 11, 40, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def hourly():
|
||||
return [{'datetime': f'2026-09-13T{h:02d}:00:00+00:00', 'condition': 'rainy' if h < 12 else 'partlycloudy', 'temperature': 18 + h / 10,
|
||||
def hourly(day=None):
|
||||
# Whole hours from 08:00 on the given day (default: the fixed NOW day) into the next morning.
|
||||
start = (day or NOW).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
return [{'datetime': (start + timedelta(hours=h)).isoformat(), 'condition': 'rainy' if h < 12 else 'partlycloudy', 'temperature': 18 + h / 10,
|
||||
'precipitation': 0.2 if h < 12 else 0.0, 'precipitation_probability': 60 if h < 12 else 5} for h in range(8, 30)]
|
||||
|
||||
|
||||
@@ -70,7 +72,8 @@ class HourlySync(unittest.IsolatedAsyncioTestCase):
|
||||
asked = []
|
||||
async def forecast(entity, kind='daily'):
|
||||
asked.append(kind)
|
||||
return daily() if kind == 'daily' else hourly()
|
||||
# The manager reads the real clock, so the hourly strip is anchored to today.
|
||||
return daily() if kind == 'daily' else hourly(datetime.now(timezone.utc))
|
||||
m.ha.forecast = forecast
|
||||
m.ha.registry.append({'entity_id': 'sensor.fw', 'platform': 'esphome', 'original_name': 'Schermfirmware'})
|
||||
m.ha.states['sensor.fw'] = {'state': '0.2.19'}
|
||||
|
||||
@@ -61,8 +61,10 @@ class ProfileNameTests(unittest.TestCase):
|
||||
'packages:\n a: !include other.yaml\napi:\n encryption:\n key: !secret api\n')
|
||||
(Path(tmp) / 'broken.yaml').write_text('esphome: [\n')
|
||||
names = f.profile_names()
|
||||
self.assertEqual(names['woonkamer.yaml'], {'node': 'woonkamer', 'friendly': 'Woonkamer'})
|
||||
self.assertEqual(names['manual.yaml'], {'node': 'keuken', 'friendly': 'Keuken'})
|
||||
self.assertEqual(names['woonkamer.yaml'], {'node': 'woonkamer', 'friendly': 'Woonkamer', 'screen': True, 'api_key': names['woonkamer.yaml']['api_key']})
|
||||
self.assertEqual(len(names['woonkamer.yaml']['api_key']), 44)
|
||||
# A manual profile (no board package from this repo, key behind !secret) is not an ESP Screens profile.
|
||||
self.assertEqual(names['manual.yaml'], {'node': 'keuken', 'friendly': 'Keuken', 'screen': False, 'api_key': None})
|
||||
self.assertNotIn('broken.yaml', names)
|
||||
self.assertNotIn('secrets.yaml', names)
|
||||
|
||||
@@ -84,7 +86,7 @@ class ProfileNameTests(unittest.TestCase):
|
||||
os.utime(b, ns=(b.stat().st_atime_ns, b.stat().st_mtime_ns + 1_000_000))
|
||||
names = f.profile_names()
|
||||
self.assertEqual(len(parsed), 3, 'only the changed file is parsed')
|
||||
self.assertEqual(names['b.yaml'], {'node': 'b2', 'friendly': 'B2'})
|
||||
self.assertEqual(names['b.yaml'], {'node': 'b2', 'friendly': 'B2', 'screen': False, 'api_key': None})
|
||||
a.unlink()
|
||||
self.assertEqual(set(f.profile_names()), {'b.yaml'})
|
||||
self.assertEqual(len(parsed), 3)
|
||||
|
||||
@@ -3,6 +3,7 @@ import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'screen_manager/app'))
|
||||
from firmware import Firmware
|
||||
|
||||
@@ -22,18 +23,56 @@ class WifiReuse(unittest.TestCase):
|
||||
self.assertIn('password: !secret wifi_password', profile)
|
||||
self.assertNotIn('example-private', profile)
|
||||
|
||||
def test_missing_keys_do_not_overwrite_existing_secrets(self):
|
||||
def test_missing_keys_are_added_without_touching_other_secrets(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
f = Firmware(tmp, tmp)
|
||||
path = Path(tmp) / 'secrets.yaml'
|
||||
path.write_text('other: keep\n')
|
||||
self.assertEqual(f.wifi_status()['state'], 'missing')
|
||||
original = '# keep comments\nother: keep\nwifi_ssid: ""\n'
|
||||
path.write_text(original)
|
||||
self.assertEqual(f.wifi_status(), {'state': 'missing', 'missing': ['wifi_password', 'wifi_ssid']})
|
||||
# Without values nothing is written: no profile, secrets untouched.
|
||||
with self.assertRaises(ValueError):
|
||||
f.create({'board': 'cyd', 'name': 'screen', 'friendly_name': 'Screen'})
|
||||
self.assertEqual(path.read_text(), original)
|
||||
self.assertFalse((Path(tmp) / 'screen.yaml').exists())
|
||||
f.create({'board': 'cyd', 'name': 'screen', 'friendly_name': 'Screen',
|
||||
'wifi_ssid': 'home: net', 'wifi_password': 'p\\ss "word"'})
|
||||
text = path.read_text()
|
||||
self.assertTrue(text.startswith('# keep comments\nother: keep\n'))
|
||||
self.assertEqual(text.count('wifi_ssid'), 1)
|
||||
self.assertEqual(yaml.safe_load(text), {'other': 'keep', 'wifi_ssid': 'home: net', 'wifi_password': 'p\\ss "word"'})
|
||||
self.assertEqual(f.wifi_status()['state'], 'ready')
|
||||
self.assertTrue((Path(tmp) / 'screen.yaml').exists())
|
||||
|
||||
def test_invalid_secrets_are_never_rewritten(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
f = Firmware(tmp, tmp)
|
||||
path = Path(tmp) / 'secrets.yaml'
|
||||
path.write_text('wifi_ssid: [invalid')
|
||||
with self.assertRaises(ValueError):
|
||||
f.create({'board': 'cyd', 'name': 'screen', 'friendly_name': 'Screen',
|
||||
'wifi_ssid': 'replacement', 'wifi_password': 'replacement'})
|
||||
self.assertEqual(path.read_text(), 'other: keep\n')
|
||||
'wifi_ssid': 'net', 'wifi_password': 'pw'})
|
||||
self.assertEqual(path.read_text(), 'wifi_ssid: [invalid')
|
||||
self.assertFalse((Path(tmp) / 'screen.yaml').exists())
|
||||
|
||||
def test_install_checks_port_and_busy_slot_before_writing(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
f = Firmware(tmp, tmp)
|
||||
data = {'board': 'cyd', 'name': 'screen', 'friendly_name': 'Screen', 'wifi_ssid': 'net', 'wifi_password': 'pw'}
|
||||
with self.assertRaises(ValueError):
|
||||
f.install({**data, 'target': '/dev/ttyUSB0'})
|
||||
self.assertFalse((Path(tmp) / 'screen.yaml').exists())
|
||||
self.assertFalse((Path(tmp) / 'secrets.yaml').exists())
|
||||
result = f.install(data)
|
||||
self.assertEqual(result['file'], 'screen.yaml')
|
||||
self.assertEqual(len(result['api_key']), 44)
|
||||
self.assertNotIn('job', result)
|
||||
self.assertIn(result['api_key'], f.profile('screen.yaml').read_text())
|
||||
meta = f.profile_names()['screen.yaml']
|
||||
self.assertEqual((meta['screen'], meta['api_key']), (True, result['api_key']))
|
||||
(Path(tmp) / 'other.yaml').write_text('esphome:\n name: other\napi:\n encryption:\n key: !secret k\n')
|
||||
self.assertEqual((f.profile_names()['other.yaml']['screen'], f.profile_names()['other.yaml']['api_key']), (False, None))
|
||||
|
||||
def test_first_install_and_invalid_file(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
f = Firmware(tmp, tmp)
|
||||
|
||||
Reference in New Issue
Block a user