diff --git a/docs/teilprojekte.md b/docs/teilprojekte.md index ac8fb7d..97e0950 100644 --- a/docs/teilprojekte.md +++ b/docs/teilprojekte.md @@ -360,6 +360,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung". - ✅ **Live-Abgleich 2026-07-11 (go-ready):** (a) `shelly-mcp` (`server.py`) fertig — discover/get/set_sta/verify/reservation_plan, PSK in `.env` gesetzt (noch nicht als MCP registriert, optional). (b) **Alle VLAN30-DHCP-Reservierungen liegen** (`.30.2xx`, Oktett erhalten, außerhalb Pool `.100–.200`); **s6-Reservierung `.30.213` MAC gefixt** (falsch `0C:72…9E` → real `D0:CF:13:D8:18:FC`). (c) **IOT-SSID live verifiziert:** ap1 `w2-iot` `running:true`, 8 Clients −26…−68 dBm (ap1-`wifi2`-„running:false" ist kosmetisch). (d) **Migration bereits ~5/26 durch:** s14/16/18/22/23 seit ~2,5 d stabil auf reservierten `.30.221/.223/.225/.202/.203` (+ `.30.204` unbekannt, evtl. s24) → **Pilot faktisch bestanden**. - ✅ **HA-Cross-Check 2026-07-11 (alle IPv4-Geräte in HA):** Shellys deckungsgleich; **`.30.204` = Shelly „Bad/Spiegelschrank" (Plus RGBW PM, `14:33:5c:d8:2d:7c`) = vmtl. s24** → 6 statt 5 schon migriert. **Netzweite Aufstellung + VLAN1-Leerstand-Analyse** im Runbook: nach TP17 + pve-Host→VLAN20 + Christophs Endgeräte→VLAN10 ist VLAN1 **endgeräte-frei**; es bleiben nur Switch-/AP-Mgmt (sw1/sw2/ap2 → VLAN99-Cutover, eigener Schritt) + **1 unidentifiziertes Gerät `.41.246`** (morgen klären). **Nicht-Shelly-VLAN30-Mover (User-bestätigt):** SwitchBot floor-lamp `.41.184` (Res. `.30.227` steht) + Twinkly-Girlande (`.30.228`, saisonal) + Creality Ender (fehlt noch, off). **Withings schon übersiedelt; Tasmota existiert nicht mehr** (kein aktives Gerät → stale HA-Eintrag). EOS-Smartwatches/eBook-Reader später + von Christoph selbst (eBook eher VLAN50). - **Scope-Erweiterung (User 2026-07-11):** je Gerät **3 Änderungen** — WLAN→`dirtyvader-iot` + IP (`.41.2xx`→`.30.2xx`) **+ einheitliches Geräte-Passwort** (`Shelly.SetAuth`, User `admin`, realm-individueller ha1 → Tool `set_auth` in `server.py` nachrüsten). **PW entschieden: eigenes Shelly-Admin-PW** (getrennt vom WLAN-PSK) → `SHELLY_DEVICE_PW` in `.env` (Christoph liefert Wert). **HA-Entities unverändert** (keyt per MAC). **HA-Entities bleiben unverändert** (HA keyt per MAC). **Projektion TP17.1 (experimentell, nachrangig):** Gen4 (~11 St.) auf **Thread/Matter** flashen (Firmware-Verfügbarkeit + Thread-Border-Router offen) — Konflikt mit „Entities unverändert" (Matter = neue Entities) → erst WiFi-Migration stabilisieren. +- ✅ **Reservierungs-/Pool-Konvention (User 2026-07-11):** alle Shellys bekommen **statische IP** (`10.40.30.`, gw/dns `.30.1`) **+ in-pool DHCP-Reservierung** als Backup. `pool-v30` auf `.100-.250` erweitert (Reservierungen jetzt in-pool); `set_sta` auf `static=true` umgestellt; s24-Inventar + `.30.204`-Reservierung angelegt. **Vollständiger HA-MAC-Scan:** 2 verpasste Media-Geräte **Apple TV 4K + Philips TV** (aus, → VLAN40) — **vor cudy-Abschuss** VLAN verifizieren; **Apple TV = vmtl. Thread-Border-Router** (TP17.1). **`.41.246/.247 = cudy1/cudy2`** (Teardown-Ziele). Tasmota bestätigt tot. - **Offene Schritte (morgen, „nur noch go", Runbook `docs/tp17-shelly-go-runbook.md`):** die **16 verbliebenen VLAN1-Shellys** (s4–s11, s13, s15, s17, s19–s21, s25, s26 auf `10.40.41.2xx`) per `WiFi.SetConfig`→`dirtyvader-iot` umklemmen (Batch, je Gerät `.30.x` verifizieren) → Geräte-PW setzen → HA/ioBroker-Re-Point → SwitchBot/Tasmota/3. Nicht-Shelly → saisonale s1/s2/s3/s12 beim nächsten Einstecken → cudy-Alt-WLAN abbauen → DNS/Inventar/`ip-plan` nachziehen. - Abh.: SSID/VLAN30 (TP3/TP4 ✅), DHCP-v30 ✅. Verwandt: `project_vlan1_dhcp_incident` (cudy-Rest-Leak), TP5 (IOT-Firewall danach). Memory `project_shelly_migration_tp17`. diff --git a/docs/tp17-shelly-go-runbook.md b/docs/tp17-shelly-go-runbook.md index 8647ab5..8e458ad 100644 --- a/docs/tp17-shelly-go-runbook.md +++ b/docs/tp17-shelly-go-runbook.md @@ -68,14 +68,20 @@ Ziel-IP = letztes Oktett erhalten; alle Reservierungen existieren & MAC-verifizi Der User-Auftrag ist **WLAN + IP + Geräte-Passwort** vereinheitlichen — nicht nur WLAN/IP. Reihenfolge je Gerät, damit Recovery immer möglich bleibt: -**(1) WLAN→VLAN30** (Gen2+ RPC, identisch für alle Gen3/4, aktuell `auth=off`): +**(1) WLAN→VLAN30 mit STATISCHER IP** (Gen2+ RPC, identisch für alle Gen3/4, `auth=off`). +**Entscheidung (User 2026-07-11): alle Shellys bekommen ausnahmslos eine statische IP** (letztes Oktett erhalten → `10.40.30.`, gw/dns `.30.1`), **plus** die passende **in-pool** DHCP-Reservierung als Backup: ```bash curl -s -X POST http:///rpc \ -H 'Content-Type: application/json' \ - -d '{"id":1,"method":"WiFi.SetConfig","params":{"config":{"sta":{"ssid":"dirtyvader-iot","pass":"","enable":true,"ipv4mode":"dhcp"}}}}' + -d '{"id":1,"method":"WiFi.SetConfig","params":{"config":{"sta":{ + "ssid":"dirtyvader-iot","pass":"","enable":true, + "ipv4mode":"static","ip":"10.40.30.","netmask":"255.255.255.0", + "gw":"10.40.30.1","nameserver":"10.40.30.1"}}}}' curl -s -X POST http:///rpc -d '{"id":1,"method":"Shelly.Reboot"}' ``` -→ Gerät kommt auf `.30.x` (letztes Oktett = Reservierung). **Verify** (`discover.py `). +Oder via `shelly-mcp` **`set_sta {ip, confirm:true}`** (Default `static=true`, rechnet die Ziel-IP aus dem Oktett). +→ Gerät kommt exakt auf `10.40.30.`. **Verify** (`discover.py `). +**Pool-Konvention:** `pool-v30` wurde 2026-07-11 auf `10.40.30.100-.250` erweitert, damit die Reservierungen (`.202–.244`) **in-pool** liegen (eine DHCP-Neuconfig liefert dann sofort brauchbare IPs). **(2) Einheitliches Geräte-Passwort** (erst NACH erfolgreichem VLAN30-Verify): ```bash @@ -109,7 +115,12 @@ Sweep über `configuration_url` aller HA-Devices (Container/Portainer/pve-„Ger - **Shellys:** vollständig deckungsgleich mit dem Netz-Sweep. **`.30.204` aufgelöst:** = Shelly **„Bad/Spiegelschrank" (Shelly Plus RGBW PM, `14:33:5c:d8:2d:7c`)** → sehr wahrscheinlich das inventarlose **s24**; damit **6 Shellys** bereits auf VLAN30 (s14/16/18/22/23/24). ✅ Inventar `bf.csv` ergänzen (s24-MAC). - **Naming-Drift (nur kosmetisch, KEINE Entity-Änderung ohne Freigabe):** s10 HA-Label „schlafzi/teststecker" vs. Inventar `bad_sonos`; s18-Label „küche/steckdose 3-2" bei `.30.202` (=s22-Reservierung). Vor cudy-Abbau Labels/Inventar konsolidieren. - **Sonos ×7** → VLAN40 `.40.19x` (korrekt, MEDIA). **Brother MFC-J5720DW** `.10.199` (VLAN10), **Brother PT-P750W** `.30.5` (VLAN30) — beide korrekt platziert. **CCU/RaspberryMatic** `.20.232` (VLAN20/INFRA, korrekt). -- ⚠️ **`tasmota_plug9` (Nous A1, Tasmota)** in HA mit `.41.206` — aber `.41.206` gehört per ARP s20 (Shelly), also **stale**. **User-Ruling 2026-07-11:** „wenn kein aktives Tasmota-Gerät auffindbar, gibt es keine mehr" → **kein VLAN30-Mover**; **stale HA-Device/Entity entfernen** (User macht Aufräumen selbst). +- ⚠️ **`tasmota_plug9` (Nous A1, Tasmota)** in HA mit `.41.206` — aber `.41.206` gehört per ARP s20 (Shelly), also **stale**. **Vollständiger Scan 2026-07-11:** Tasmota-MAC `08:f9:e0:72:12:b6` ist **nicht in ap1-ARP** → **kein aktives Gerät** (bestätigt User-Ruling) → **kein VLAN30-Mover; stale HA-Device entfernen** (User). + +**Nachtrag — vollständiger HA-Device-Scan (alle Geräte mit MAC-Connection, nicht nur `configuration_url`):** fördert **2 Media-Geräte ohne IP in der URL** zutage, die der erste Sweep verpasst hat: +- **Apple TV 4K** (`2a:77:fe:14:ad:5f`, „Wohnzimmer (2)") und **Philips TV 50PUS7304** (`44:d8:78:30:b8:d5`) — beide **aktuell aus/Standby** (nicht in ap1-ARP). Zielnetz = **VLAN40/MEDIA**. ⚠️ **Vor cudy-Abschuss verifizieren**, ob sie schon auf `dirtyvader-media` hängen oder **noch am cudy/VLAN1** — sonst fällt der Wohnzimmer-TV/AppleTV mit dem cudy weg. (Einschalten → ap1-ARP prüfen.) +- **Apple TV 4K = sehr wahrscheinlich der aktive Thread-Border-Router** für die bestehende IKEA-Matter-over-Thread-Flotte (22 Matter-Geräte in HA) → relevant für TP17.1 (Thread-Onboarding liefe über diesen BR bzw. einen HA-eigenen OTBR). +- Alle übrigen MAC-Geräte sind zugeordnet: Shellys, Sonos ×7 (VLAN40), Brother MFC (VLAN10). Radio-only-Geräte (Zigbee/Matter-over-Thread/BLE) haben keine IP → kein VLAN-Thema. ## Netzweite Aufstellung → „ist VLAN1 danach leer?" (Netz-Wahrheit via ap1 ARP+DHCP, 2026-07-11) **Live in VLAN1 (bridge), klassifiziert nach Ziel-VLAN** (Phantom-/`failed`-ARP-Leichen weggelassen): @@ -130,7 +141,9 @@ Sweep über `configuration_url` aller HA-Devices (Container/Portainer/pve-„Ger **Nicht-Shelly-Mover (User-bestätigt 2026-07-11):** SwitchBot floor-lamp + Twinkly-Girlande + Creality Ender (fehlt noch, off). **Withings = schon übersiedelt** (nicht mehr offen). **Tasmota = existiert nicht mehr** (kein aktives Gerät). **EOS-Smartwatches / eBook-Reader** kommen später und macht **Christoph selbst** (eBook-Reader gehört lt. VLAN-Modell eher nach **VLAN50/UNTRUSTED**, nicht VLAN30) — nicht Teil dieses Go. -**Verdikt:** Nach TP17 (16 Shellys + SwitchBot/Twinkly/Ender → VLAN30) + pve-Host → VLAN20 + Christophs Endgeräte → VLAN10 (cudy-Abbau) ist VLAN1 **frei von Endgeräten**. **Nicht** automatisch leer: die **Switch-/AP-Mgmt-IPs (sw1/sw2/ap2)** sind *by design* interim in VLAN1 bis zum separaten **MGMT-VLAN99-Cutover** (kein TP17-Scope). Plus **`.41.246` muss identifiziert werden**, bevor „leer" behauptet werden kann. `defconf`/pool-v1-DHCP erst abschalten, wenn all das erledigt ist. +**`.41.246`/`.247` identifiziert (User):** = **cudy1/cudy2** (die alten WLAN-APs, zum Abbau freigegeben, nur für VLAN1). Kein Fremdkörper → das ist der cudy-Teardown selbst. + +**Verdikt:** Nach TP17 (16 Shellys + SwitchBot/Twinkly/Ender → VLAN30) + pve-Host → VLAN20 + Christophs Endgeräte → VLAN10 ist VLAN1 **frei von Endgeräten**. **Prüf-Prereq vor cudy-Abschuss:** **Apple TV + Philips TV** (aktuell aus) einschalten und verifizieren, dass sie auf `dirtyvader-media`/VLAN40 hängen und **nicht** noch am cudy/VLAN1 — sonst mit umziehen. **Nicht** automatisch leer bleiben: die **Switch-/AP-Mgmt-IPs (sw1/sw2/ap2)** sind *by design* interim in VLAN1 bis zum separaten **MGMT-VLAN99-Cutover** (kein TP17-Scope). `defconf`/pool-v1-DHCP erst abschalten, wenn all das erledigt ist. ## Projektion: Gen4-Shellys auf Thread/Matter (experimentell, separate Spur) User-Wunsch: prüfen, ob die **Gen4-Geräte** (S4PL Power-Strip-4, S4SW 2PM Gen4 — ~11 Stück: s9,s13,s16,s17,s18,s19,s20,s21,s24?,s25,s26) auf **Thread-Firmware geflasht** und dann via **Matter-over-Thread** in HA eingebunden werden können. @@ -140,10 +153,15 @@ User-Wunsch: prüfen, ob die **Gen4-Geräte** (S4PL Power-Strip-4, S4SW 2PM Gen4 - ⚠️ **KONFLIKT mit „Entities bleiben unverändert":** Matter-Onboarding erzeugt in HA **neue** Entities (andere Integration) → die alten Shelly-WiFi-Entity-IDs würden brechen. Vor jedem Thread-Flash klären, ob Entity-Stabilität oder Thread-Migration Vorrang hat (ggf. Entity-`unique_id` manuell übernehmen / bewusst neu benennen). **Solange „Entities unverändert" gilt, ist die Thread-Spur nachrangig zur WiFi-Migration.** - **Reihenfolge-Empfehlung:** morgen erst die **WiFi+IP+PW-Migration** komplett durchziehen (stabiler Zielzustand, HA unverändert). Thread als **eigene Folge-Spur (TP17.1)** danach evaluieren — nichts davon blockt den Go. -## Offene Datenlücken / To-verify (nicht go-blockierend) -- **`.41.246` (80:af:ca:7d:e5:b1)** identifizieren — einziges unklares Live-Gerät in VLAN1, nicht in HA. -- **Creality Ender** (3D-Drucker) beim nächsten Einschalten verorten (MAC/IST-IP) + `.30.x`-Reservierung anlegen (existiert noch nicht). -- **s24** MAC (`14:33:5c:d8:2d:7c`) ins Inventar `bf.csv` nachtragen; **`.30.204`-Reservierung fehlt noch** (Gerät hat aktuell vmtl. statische IP). +## Erledigt heute (2026-07-11, nicht-disruptiv) +- ✅ **s24** MAC (`14:33:5c:d8:2d:7c`) ins `inventory/bf.csv` + **`.30.204`-Reservierung** angelegt. +- ✅ **pool-v30 → `.100-.250`** erweitert (Reservierungen in-pool). +- ✅ **`set_sta` auf statische IP** umgestellt (Default), **`set_auth`-Tool** vorhanden (Digest-Auth-Fallback). +- ✅ **Tasmota** als nicht-existent bestätigt (MAC nicht im Netz). + +## Offen / To-verify (nicht go-blockierend) +- **Apple TV + Philips TV** (aus): beim Einschalten VLAN prüfen (Ziel VLAN40) — **vor cudy-Abschuss** (s.o.). +- **Creality Ender** (3D-Drucker) beim nächsten Einschalten verorten (MAC/IST-IP) + `.30.x`-Reservierung anlegen. - **Stale HA-Device `tasmota_plug9`** entfernen (kein reales Gerät mehr) — macht Christoph. -- `SHELLY_DEVICE_PW` (eigenes Admin-PW) von Christoph in `mcp/shelly/.env`; `set_auth`-Tool in `server.py` nachrüsten (ha1-Berechnung je realm). +- **`SHELLY_DEVICE_PW`** (eigenes Admin-PW, Wert liegt vor) von Christoph in `mcp/shelly/.env` ablegen (chmod600). - Die **6 bereits Migrierten**: Reservierung `waiting/never` trotz Erreichbarkeit auf reservierter IP → wahrscheinlich **statische IP** am Gerät. Entscheiden: auf DHCP-Reservierung vereinheitlichen (Konsistenz) oder statisch belassen (funktional egal). diff --git a/inventory/bf.csv b/inventory/bf.csv index b0ffbc9..0416288 100644 --- a/inventory/bf.csv +++ b/inventory/bf.csv @@ -201,7 +201,7 @@ Sonos,10.40.41.200,SonosWohnzimmer2,48:a6:b8:14:fe:f2,,,,"dhcp-host=48:a6:b8:14: Sonos,10.40.41.201,SonosWohnzimmer3,78:28:ca:d2:0d:ac,,,,"dhcp-host=78:28:ca:d2:0d:ac,10.40.41.201,SonosWohnzimmer3" Sonos,10.40.41.202,shelly: s22,a0:85:e3:c8:65:34,,,,kueche_steckdose3-2 Sonos,10.40.41.203,shelly: s23,ac:15:18:7b:62:20,,,,dieleug-wohnzimmer_licht -Sonos,10.40.41.204,shelly: s24,,,,,bad_spielgelschrank +Sonos,10.40.41.204,shelly: s24,14:33:5c:d8:2d:7c,,,,bad_spiegelschrank (Shelly Plus RGBW PM, bereits VLAN30 .30.204) Sonos,10.40.41.205,shelly: s25,98:a3:16:79:38:50,,,,bad_lichtschalter IOT,10.40.41.206,shelly: s20,98:a3:16:7c:5f:ec,,,,kueche_steckdosen2 IOT,10.40.41.207,shelly: s21,20:6e:f1:01:f9:f8,,,,kueche_steckdosen4 diff --git a/mcp/shelly/server.py b/mcp/shelly/server.py index 78c30d6..627c277 100644 --- a/mcp/shelly/server.py +++ b/mcp/shelly/server.py @@ -121,37 +121,58 @@ def get_device(ip: str) -> dict: return out +V30_NET = "10.40.30" +V30_MASK = "255.255.255.0" +V30_GW = "10.40.30.1" + + +def _sta_cfg(ssid: str, static: bool, target_ip: str) -> dict: + """Build the WiFi STA config. Project decision (2026-07-11): all Shellys get a + STATIC IP on-device (keeping the last octet) plus a matching in-pool DHCP + reservation as backup — not DHCP-only.""" + sta = {"ssid": ssid, "pass": IOT_PSK, "enable": True} + if static: + sta.update({"ipv4mode": "static", "ip": target_ip, "netmask": V30_MASK, + "gw": V30_GW, "nameserver": V30_GW}) + else: + sta["ipv4mode"] = "dhcp" + return sta + + @mcp.tool() -def set_sta(ip: str, confirm: bool = False, ssid: str | None = None) -> dict: +def set_sta(ip: str, confirm: bool = False, ssid: str | None = None, + static: bool = True) -> dict: """MIGRATION ACTION — point a Shelly's Wi-Fi STA at the IOT SSID, then reboot. - Reconfigures a LIVE device: it drops off VLAN1 and rejoins on VLAN30 via DHCP. + Reconfigures a LIVE device: it drops off VLAN1 and rejoins on VLAN30. Per the + project decision it sets a STATIC IP (10.40.30., gw/dns .30.1); + a matching in-pool DHCP reservation exists as backup. Pass static=False to use DHCP. A wrong PSK leaves it in AP-mode (physical reset needed). Therefore: - confirm MUST be true to actually write, - SHELLY_IOT_PSK must be set in the environment. With confirm=false this is a dry-run (returns the exact config that would be sent). """ target_ssid = ssid or IOT_SSID + target_ip = f"{V30_NET}.{ip.rsplit('.', 1)[-1]}" ident = discover.probe(ip) - plan = {"ip": ip, "target_ssid": target_ssid, + plan = {"ip": ip, "target_ssid": target_ssid, "target_ip": target_ip, + "mode": "static" if static else "dhcp", "device": ident, "psk_configured": bool(IOT_PSK)} if not ident: return {**plan, "ok": False, "error": "device not reachable"} if not IOT_PSK: return {**plan, "ok": False, "error": "SHELLY_IOT_PSK not set in environment"} + sta = _sta_cfg(target_ssid, static, target_ip) if not confirm: + redacted = {**sta, "pass": "***"} return {**plan, "ok": False, "dry_run": True, "would_send": {"method": "WiFi.SetConfig", - "params": {"config": {"sta": {"ssid": target_ssid, - "pass": "***", "enable": True, - "ipv4mode": "dhcp"}}}}} + "params": {"config": {"sta": redacted}}}} try: - cfg = {"config": {"sta": {"ssid": target_ssid, "pass": IOT_PSK, - "enable": True, "ipv4mode": "dhcp"}}} - res = _rpc(ip, "WiFi.SetConfig", cfg) + res = _rpc(ip, "WiFi.SetConfig", {"config": {"sta": sta}}) reboot = _rpc(ip, "Shelly.Reboot") return {**plan, "ok": True, "set_result": res, "reboot": reboot, - "note": "device rebooting; expect it on VLAN30 (10.40.30.x) shortly"} + "note": f"device rebooting; expect it at {target_ip} shortly"} except Exception as e: return {**plan, "ok": False, "error": f"{type(e).__name__}: {e}"}