From 7e7f59760004439b18b75a8f4f12e5c956fe0415 Mon Sep 17 00:00:00 2001 From: Max Gramser Date: Sat, 12 Sep 2026 17:17:20 +0200 Subject: [PATCH] Detect and reuse ESPHome wifi secrets in new screen wizard --- docs/TEST_RESULTS_026.md | 16 ++++++++++ screen_manager/CHANGELOG.md | 4 +++ screen_manager/app/firmware.py | 24 +++++++++++++-- screen_manager/app/static/app.js | 27 ++++++++++++++++- screen_manager/app/static/index.html | 10 +++++-- screen_manager/config.yaml | 2 +- tests/test_wifi_reuse.py | 45 ++++++++++++++++++++++++++++ 7 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 docs/TEST_RESULTS_026.md create mode 100644 tests/test_wifi_reuse.py diff --git a/docs/TEST_RESULTS_026.md b/docs/TEST_RESULTS_026.md new file mode 100644 index 0000000..93b62f1 --- /dev/null +++ b/docs/TEST_RESULTS_026.md @@ -0,0 +1,16 @@ +# Wifi-secrets hergebruiken — 0.2.6 + +De wizard controleert de ESPHome-map van de add-on. Met geldige wifi_ssid en +wifi_password in secrets.yaml worden wifi-velden verborgen en uitgeschakeld. +Alleen als dat bestand nog niet bestaat vraagt de wizard om wifi voor de eerste +installatie. Een bestaand onvolledig/ongeldig bestand wordt niet overschreven. +De browser ontvangt uitsluitend beschikbaarheid, geen secretwaarden. + +API-encryptie- en OTA-sleutels blijven uniek gegenereerd door ESP Screens en staan +in de eigen apparaat-YAML. Updates behouden die YAML. Geen firmwarewijziging. + +Controles: 45 Python-tests geslaagd, inclusief hergebruik zonder invoer, ongewijzigd +behoud van bestaande secrets, geen secretwaarden in status/profiel, eerste +installatie en ongeldige YAML. JavaScript-syntax en Git-diff gecontroleerd. +Browserpreview: bij bestaande wifi alleen bevestiging zichtbaar, geen wifi-invoer; +profiel aanmaken beschikbaar. Test gebruikt geen echte wifiwaarden. diff --git a/screen_manager/CHANGELOG.md b/screen_manager/CHANGELOG.md index 8ab6128..427367c 100644 --- a/screen_manager/CHANGELOG.md +++ b/screen_manager/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.6 + +- Nieuw scherm hergebruikt bestaande ESPHome-wifi-secrets automatisch. De wizard vraagt alleen wifi als secrets.yaml nog niet bestaat. Ontbrekende wifi-sleutels in een bestaand bestand worden gemeld zonder het bestand te overschrijven. + ## 0.2.5 - Firmware: een vacuumkaart openen toont de echte status, geen opdrachtmelding. Het statuslabel in de robotkaart blijft ook na een actie actueel. diff --git a/screen_manager/app/firmware.py b/screen_manager/app/firmware.py index 73a7c0a..dc48439 100644 --- a/screen_manager/app/firmware.py +++ b/screen_manager/app/firmware.py @@ -36,16 +36,36 @@ class Firmware: raise ValueError('Profiel bestaat niet in de ESPHome-map.') return p + def wifi_status(self): + # Return availability only; never send secret values to the browser. + path = self.root / 'secrets.yaml' + if not path.exists(): + return {'state': 'new', 'missing': ['wifi_ssid', 'wifi_password']} + try: + values = yaml.safe_load(path.read_text()) + if not isinstance(values, dict): + return {'state': 'invalid'} + missing = [key for key in ('wifi_ssid', 'wifi_password') + if not isinstance(values.get(key), str)] + if isinstance(values.get('wifi_ssid'), str) and not values['wifi_ssid'].strip(): + missing.append('wifi_ssid') + return {'state': 'missing' if missing else 'ready', 'missing': missing} + except (OSError, yaml.YAMLError, UnicodeError): + return {'state': 'invalid'} + def status(self): return {'available': bool(shutil.which('esphome')), 'profiles': self.profiles(), - 'ports': self.ports(), 'job': self.job, 'logs': list(self.logs)} + 'ports': self.ports(), 'job': self.job, 'logs': list(self.logs), 'wifi': self.wifi_status()} def create(self, data): 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' - if not secret_path.exists(): + 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.') diff --git a/screen_manager/app/static/app.js b/screen_manager/app/static/app.js index cda5159..0591777 100644 --- a/screen_manager/app/static/app.js +++ b/screen_manager/app/static/app.js @@ -569,8 +569,33 @@ for (const [value, label] of [ }; $("#filters").append(b); } -$("#new-screen").onclick = $("#start").onclick = () => +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…"; + 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."; + } +} +$("#new-screen").onclick = $("#start").onclick = () => { $("#installer").showModal(); + checkWifiSecrets(); +}; $("#close-install").onclick = () => $("#installer").close(); let generatedName = ""; $("#install-form").onsubmit = async (e) => { diff --git a/screen_manager/app/static/index.html b/screen_manager/app/static/index.html index 10fc945..0037fa7 100644 --- a/screen_manager/app/static/index.html +++ b/screen_manager/app/static/index.html @@ -202,9 +202,12 @@ />Kleine letters, cijfers en streepjes. Elk scherm een andere naam. +

+ diff --git a/screen_manager/config.yaml b/screen_manager/config.yaml index 7d31239..c85dafc 100644 --- a/screen_manager/config.yaml +++ b/screen_manager/config.yaml @@ -1,5 +1,5 @@ name: ESP Screen Manager -version: "0.2.5" +version: "0.2.6" slug: esp_screen_manager description: Kies tegels voor je CYD en Guition touchscreen, zonder opnieuw flashen. url: https://github.com/MaxGramser/homeassistant_espscreen diff --git a/tests/test_wifi_reuse.py b/tests/test_wifi_reuse.py new file mode 100644 index 0000000..0b278d1 --- /dev/null +++ b/tests/test_wifi_reuse.py @@ -0,0 +1,45 @@ +import json +import sys +import tempfile +import unittest +from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'screen_manager/app')) +from firmware import Firmware + +class WifiReuse(unittest.TestCase): + def test_existing_wifi_reused_without_browser_values(self): + with tempfile.TemporaryDirectory() as tmp: + f = Firmware(tmp, tmp) + path = Path(tmp) / 'secrets.yaml' + original = '# keep comments\nwifi_ssid: example-private-network\nwifi_password: example-private-password\nother_secret: leave-me\n' + path.write_text(original) + self.assertEqual(f.wifi_status()['state'], 'ready') + self.assertNotIn('example-private', json.dumps(f.status())) + f.create({'board': 'guition', 'name': 'second-screen', 'friendly_name': 'Second'}) + self.assertEqual(path.read_text(), original) + profile = f.profile('second-screen.yaml').read_text() + self.assertIn('ssid: !secret wifi_ssid', profile) + self.assertIn('password: !secret wifi_password', profile) + self.assertNotIn('example-private', profile) + + def test_missing_keys_do_not_overwrite_existing_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') + 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') + self.assertFalse((Path(tmp) / 'screen.yaml').exists()) + + def test_first_install_and_invalid_file(self): + with tempfile.TemporaryDirectory() as tmp: + f = Firmware(tmp, tmp) + self.assertEqual(f.wifi_status()['state'], 'new') + f.create({'board': 'cyd', 'name': 'screen', 'friendly_name': 'Screen', + 'wifi_ssid': 'network', 'wifi_password': ''}) + self.assertEqual(f.wifi_status()['state'], 'ready') + (Path(tmp) / 'secrets.yaml').write_text('wifi_ssid: [invalid') + self.assertEqual(f.wifi_status(), {'state': 'invalid'})