From 2f183190ed63bfa4d884c0ea3b02cd9e8ade169b Mon Sep 17 00:00:00 2001 From: Christoph Emsenhuber Date: Sun, 23 Aug 2026 21:12:46 +0200 Subject: [PATCH] TP17.1 verworfen + s9 zurueckgebaut, eco_mode flottenweit aus Christophs strukturelle Frage hat TP17.1 gekippt: "zigbee sollte die antwort auf die vielen radios auf -iot geben. nutzt aber wenig wenn man das wifi nicht abdrehen kann bzw. es nicht mehr aktivieren kann ohne reset." Der Pilot hat den Weg technisch vollstaendig bewiesen -- und gezeigt, dass das Ziel so nicht erreichbar ist. Das Geraet bleibt WLAN-Client; der gemessene Funkgewinn ist klein (12 von 26 IoT-Clients, je 150-200 KB rx / 0,6-3 MB tx in sechs Stunden); und WLAN abzuschalten waere unumkehrbar, weil die Zigbee-Firmware keinen AP hat, Shelly kein Zigbee-OTA macht und InstallAlt eine IP braucht. Der Weg ist nicht gescheitert, das Ziel war mit ihm nicht erreichbar. Das Runbook bleibt als Wissensspeicher. Rueckbau: ZHA-Knoten entfernt -- Leave VOR dem Flash, damit kein Geisterrouter im 52-Geraete-Mesh bleibt. s9 zurueck auf PowerStrip, Config aus dem Backup. Alle vier roten EZSP-Zaehler blieben 0, Retry-Quote fiel im Delta auf 8,9 %. Neue Merkregel: der Rueckflash zog einen OFFENEN AP hoch (ap.enable=true, is_open=true). Abgeschaltet und mit dem IoT-PSK abgesichert. Nach jedem Shelly-Firmwarewechsel den AP pruefen. eco_mode flottenweit aus (User: "der rotz macht nur probleme"): 13 Geraete umgestellt, null Fehler, 9 standen schon so. Jetzt eco=false auf allen 22 erreichbaren Shellys ausser s24, das es nicht unterstuetzt. Dafuer migrate.py um ein eco-Kommando ergaenzt -- mit Retry je Geraet, weil ein Timeout an einem schwachen Link den Sweep sonst abreisst. Die Gegenmessung widerlegt meine eigene Warnung: eco aus hebt die Sendeleistung NICHT messbar an. 13 geaenderte Geraete im Mittel -0,9 dB, Kontrollgruppe von 13 unveraenderten +0,0 dB bei gleicher Streuung. Die TP17-Aussage, eco senke Takt UND Sendeleistung, stimmt nur zur Haelfte. Vorbedingung der Passwort-Rotation geprueft: alle vier Skripte laufen laengst auf sshpass -e, die gegenteilige Notiz war veraltet. Uebrig war CLAUDE.md, das sshpass -p weiterhin aktiv als Reboot-Weg empfahl -- genau das Muster, das den Leak verursacht hat. Auf sshpass -e korrigiert. TP17.2 aus "zurueckgestellt" aktiviert und an die PSK-Rotation gekoppelt. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 5 +- docs/teilprojekte.md | 94 +++++++++++++++---- ...p17.1-shelly-gen4-funkmigration-runbook.md | 22 +++++ mcp/shelly/migrate.py | 59 ++++++++++++ 4 files changed, 163 insertions(+), 17 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index adad8a4..7244375 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,7 +55,10 @@ GW je VLAN = `.1` (crs326-core L3) · Mgmt VLAN99: core `.99.1`, branch `.99.2`, ## Tooling - **MikroTik (core/branch/ap1/ap2):** MikroMCP-Server live → `mcp__mikromcp__*`-Tools (routerId `crs326-core`/`crs326-branch`/`ap1`/`ap2`). - Reads immer safe; Writes mit `dryRun:true` previewen, dann `apply_plan`. **Passwort setzen via `manage_user set-password`** (`/user set*` ist per Deny-Policy blockiert). ⚠️ **Reboot geht auf ROS 7.24 gar nicht ueber MikroMCP:** das `reboot`-Tool sendet `delay`, die REST-API kennt den Parameter nicht (`unknown parameter delay`), und `run_command` ist fuer `/system reboot*` gesperrt — Weg ist `sshpass -p "$PW" ssh admin@ "/system reboot"` (PW aus `~/.secrets`, im Output maskieren). `bridge/vlan/set` nur auf `[find … and !dynamic]`. + Reads immer safe; Writes mit `dryRun:true` previewen, dann `apply_plan`. **Passwort setzen via `manage_user set-password`** (`/user set*` ist per Deny-Policy blockiert). ⚠️ **Reboot geht auf ROS 7.24 gar nicht ueber MikroMCP:** das `reboot`-Tool sendet `delay`, die REST-API kennt den Parameter nicht (`unknown parameter delay`), und `run_command` ist fuer `/system reboot*` gesperrt — Weg ist `SSHPASS="$PW" sshpass -e ssh admin@ "/system reboot"` (PW aus `~/.secrets`, im Output + maskieren). 🔴 **Niemals `sshpass -p`** — das schreibt das Passwort in die Kommandozeile, wo es in + `ps` und jedem argv-Trace steht; **genau so ist am 2026-08-22 das Core-Passwort in ein + Sitzungstranskript geraten** und musste rotiert werden. `-e` hält es in der Umgebung des Kindprozesses. `bridge/vlan/set` nur auf `[find … and !dynamic]`. - **~~ALLNET-Switch (sw1)~~:** ❌ **Gerät ausgebaut 2026-08-08** → der Telnet-Runner unter `switch/` ist **Archiv, nicht mehr ausführbar** (kein Endpunkt). Siehe `switch/README.md`. Nicht mehr als Werkzeug anbieten. - **~~Omada-Switch (sw2)~~:** ❌ abgebaut 2026-07-23 (TP15, durch `crs326-branch` ersetzt). Controller + omada-mcp komplett gelöscht — **Omada spielt keine Rolle mehr, nie wieder aufgreifen** (User-Entscheid 2026-07-22). Aufräum-Reste ✅ geprüft 2026-08-16 (s. Tabelle oben). - **Nautobot Golden Config (= der einzige legitime Config-Weg für Netzgeräte):** `nautobot/gc-configplan.py` diff --git a/docs/teilprojekte.md b/docs/teilprojekte.md index 9db53fc..f237dd0 100644 --- a/docs/teilprojekte.md +++ b/docs/teilprojekte.md @@ -87,9 +87,10 @@ ab jetzt bei jeder Änderung mitgezogen, nicht nur die TP-Abschnitte.)* > 📋 **Offen und ohne Blockadewirkung:** **TP25** (Fund F = 5:05-Takt bei IoT, plus die Entscheidung > „5 GHz auf 20 MHz") · **TP16** (CF-Tunnel-HA degradiert, vier tote Alt-Tunnel) · **TP16.2/16.3** · > **TP23 Stufe 2** · **TP24** (DC-USV) · **TP13/TP14.4** (beide mit fertigem Runbook) · -> **TP17.1** (Shelly Gen4 raus aus dem WLAN, entscheidungsreif) · ✅ **TP17.3 ERLEDIGT 2026-08-23** -> (Shelly-Regression: Ursache **`eco_mode`** bewiesen, `s23` + `s26` repariert; der Flotten-Entscheid -> „eco flottenweit aus" hängt jetzt an TP17.1) · +> ❌ **TP17.1 VERWORFEN 2026-08-23** (Zigbee-Weg bewiesen, Ziel damit nicht erreichbar — das Gerät +> bleibt WLAN-Client; `s9` zurückgebaut) · ✅ **TP17.3 ERLEDIGT** (Ursache **`eco_mode`** bewiesen; +> `s23` + `s26` repariert; **`eco_mode` flottenweit aus**, 22/22 außer `s24`) · +> 🔜 **TP17.2 aktiviert** (Shelly-Gerätepasswort, an die PSK-Rotation gekoppelt) · > 🔑 **Rotation des MikroTik-Admin-Passworts** — seit 08-23 bewusst **hinter** TP17.1 eingereiht. > Zugeschnittene Sitzungsvorschläge: [`handoff-20260822-naechste-schritte.md`](handoff-20260822-naechste-schritte.md). > ✅ **`crs326-core` ist transaktionsfrei** (seit 2026-08-22, geprüft): ein gemeldeter Drift ist ab @@ -372,7 +373,36 @@ nachmessen und dann entscheiden**. Blocker gibt es keine mehr. TP5 ist in der Na liefert das Entscheidungskriterium). ⚠️ **Berührt TP5:** der Mesh-Verkehr braucht bei deny-by-default eine explizite `forward`-Zeile — s. TP5 Etappe 2. -### TP17.1 — Shelly Gen4 raus aus dem WLAN (Thread oder Zigbee) ▸ 🔜 **ENTSCHEIDUNGSREIF (2026-08-23)** — 📋 `docs/tp17.1-shelly-gen4-funkmigration-runbook.md` +### TP17.1 — Shelly Gen4 raus aus dem WLAN ▸ ❌ **VERWORFEN (2026-08-23) — Weg bewiesen, Ziel damit nicht erreichbar** — 📋 `docs/tp17.1-shelly-gen4-funkmigration-runbook.md` +> ❌ **User-Entscheid 2026-08-23 nach dem Pilot: stoppen und zurückbauen.** Auslöser war Christophs +> strukturelle Frage: *„zigbee sollte die antwort auf die vielen radios auf -iot geben. nutzt aber +> wenig wenn man das wifi nicht abdrehen kann bzw. es nicht mehr aktivieren kann ohne reset."* +> +> **Der Pilot hat den Weg technisch vollständig bewiesen** — `s9` lief auf `PowerStripZB`, trat ZHA +> bei, ein Zigbee-Befehl schaltete das physische Relais, kein Broadcast-Sturm. **Und er hat gezeigt, +> dass das Ziel so nicht erreichbar ist:** +> - Das Gerät **bleibt WLAN-Client** — Zigbee kommt *zusätzlich*, nicht *statt*. +> - Der Funkgewinn ist **gemessen klein**: die 12 Gen4 sind 12 von 26 IoT-Clients, bewegen aber je nur +> ~150–200 KB rx / 0,6–3 MB tx in sechs Stunden. Leerlaufende Stationen. +> - Der Preis wäre **dauerhaft**: die Zigbee-Firmware hat **keinen AP** (`WiFi.GetConfig` kennt nur +> `sta`/`sta1`/`roam`), Shelly macht **kein Zigbee-OTA**, und `Shelly.InstallAlt` braucht IP. +> WLAN aus hieße: nie wieder Firmware-Update, kein Rückweg — bei 10 Unterputz-Modulen. +> +> ✅ **Rückbau durchgeführt 2026-08-23:** ZHA-Knoten sauber entfernt (Leave **vor** dem Flash, damit +> kein Geisterrouter im 52-Geräte-Mesh bleibt), `s9` zurück auf `PowerStrip`, Config aus dem Backup +> wiederhergestellt. Compliance des Zigbee-Netzes unauffällig: alle vier roten EZSP-Zähler blieben +> **0**, Retry-Quote fiel im Delta auf 8,9 %. +> ⚠️ **Beim Rückflash zog die Firmware einen OFFENEN AP hoch** (`ap.enable=true, is_open=true`) — +> abgeschaltet und mit dem IoT-PSK abgesichert. **Nach jedem Shelly-Firmwarewechsel den AP prüfen.** +> +> 📌 **Das Runbook bleibt** — es enthält den kartierten Mechanismus (drei undokumentierte RPCs, +> Zwei-Slot-Modell), das gemessene 2,4-GHz-Bild und die ZHA-Sturmzähler mit Baseline. +> *Der Weg ist nicht gescheitert, das Ziel war mit ihm nicht erreichbar.* +> 🔭 **Einziger Restposten:** der **Frühindikator** — `Shelly.CheckForUpdate` auf einem Gen4 auf einen +> `…MT`/Thread-`alt`-Eintrag beobachten. Erscheint er, ändert sich die Rechnung. + +
Ursprünglicher Planungstext (historisch) + > **User-Formulierung (2026-08-23):** „wir lassen das pwd vorerst wie es ist. wir werden alle shelly > gen4 auf thread umstellen — neue firmware. das machen wir im ha-projekt. das sehe ich als subpunkt > zu pwd ändern. der vorteil: wir müssen dann weit weniger geräte umstellen, steigern @@ -493,10 +523,30 @@ nachmessen und dann entscheiden**. Blocker gibt es keine mehr. TP5 ist in der Na (IPv6/Matter — profitiert direkt), **TP25** (jedes Gerät weniger im 2,4-GHz-Band ist Funkruhe), **TP17** im [Archiv](teilprojekte-archiv.md#tp17) (Migrationshistorie + `mcp/shelly/`-Werkzeug). -### TP17.2 — einheitliches Geräte-Passwort (Shelly `SetAuth`) ▸ ⏸️ ZURÜCKGESTELLT (Kosmetik, User 2026-07-13) +
+ +### TP17.2 — einheitliches Geräte-Passwort (Shelly `SetAuth`) ▸ 🔜 **AKTIVIERT (2026-08-23)** — an die PSK-Rotation gekoppelt +> **User 2026-08-23:** „außerdem haben wir einen punkt, um die shellys mit einem pwd zu schützen." +> Damit ist der Zurückgestellt-Status von 2026-07-13 aufgehoben. **Begründung der Kopplung:** die +> PSK-Rotation fasst ohnehin jeden Shelly an und rebootet ihn — `SetAuth` gehört in denselben +> Durchgang, ein Reboot statt zwei. +> +> ✅ **Werkzeugseitig ist nichts zu bauen:** `mcp/shelly/migrate.py` hat `cmd_auth` (setzt +> `Shelly.SetAuth` mit SHA-256-`ha1` gegen die Geräte-`realm`, Dry-Run als Default), und `_rpc` hat +> einen **Digest-Auth-Fallback** auf 401 mit `SHELLY_DEVICE_PW`. +> +> 🔴 **Der teure Teil ist nicht `SetAuth`, sondern Home Assistant.** Mit aktiver Auth verlangt die +> Shelly-Integration je Gerät einen **Re-Auth-Flow** — bei 23 Geräten ist *das* der Aufwand, und genau +> der Grund für die frühere Einstufung als „Kosmetik". ⇒ **Erst an EINEM Gerät machen und den +> HA-Aufwand messen** (Vorschlag `s15 technik_pve`, RSSI −36, unkritisch), bevor der Rest folgt. +> Sonst bündelt man ein ungemessenes Risiko in eine 23-Geräte-Aktion. + +
Ursprünglicher Text (historisch) - **Was:** je Gerät `Shelly.SetAuth` (User `admin`, realm-individueller `ha1` = sha256("admin::"), einheitliches Klartext-PW) via `mcp/shelly/migrate.py auth --go`. `SHELLY_DEVICE_PW` liegt in `mcp/shelly/.env` (gesetzt, len 13). Runner + Digest-Auth-Fallback in `_rpc` fertig. - **Warum zurückgestellt:** rein kosmetisch/Härtung; **bricht die HA-Shelly-Integration**, bis pro Config-Entry (22×) das Credential neu eingetragen ist → disruptiv, kein Mehrwert im abgeschotteten IOT-VLAN. **Erst nach** dem Netz-Teardown (TP19) neu bewerten. +
+ ### TP17.3 — Shelly-Regression nach der VLAN30-Migration ▸ ✅ **ERLEDIGT (2026-08-23, Folge-Session): beide Geräte repariert, Ursache `eco_mode` bewiesen** — offen nur noch der Flotten-Entscheid (gehört TP17.1) — aus TP17 herausgelöst, User-Meldung > **User-Meldung 2026-08-23:** „vor einiger zeit hast du an den shellys änderungen vorgenommen. das > hatte bei einigen einen bösen seiteneffekt: **lüfter-shelly musste ich doppelt drücken**. der übelste @@ -568,13 +618,23 @@ des Dimmers (= Lichtausfall in Diele UG/Wohnzimmer) wurde daraufhin **nicht** au darunter s20 (−85) und s21 (−85) — die RSSI-Regel ist an keiner Kante mehr erkennbar. Die Werte haben sich seit Juli durch TP25/TP28 ohnehin verschoben. Die in TP17 vorgemerkte „2. Evaluation nach TP3.3" hat nie stattgefunden. - ⇒ **Zu entscheiden ist nicht mehr eine Tabelle, sondern ein Grundsatz: `eco_mode` flottenweit AUS.** - Argumente dafür: nachgewiesener Schaden; die Regel wurde nie befolgt; `eco_mode` ist ohne Reboot - schaltbar und sofort reversibel; 5 Geräte laufen längst ohne und sind unauffällig. - ⚠️ **Was dagegen abzuwägen ist:** eco aus hebt die **WLAN-Sendeleistung** auf ~20 Geräten an — das ist - eine Airtime-/Funkbild-Änderung und berührt **TP25** (2,4 GHz ist die knappe Ressource, 26 von 27 - Clients hängen auf `dirtyvader-iot`). **Nicht nebenbei ausrollen, sondern gebündelt mit TP17.1**, wenn - ohnehin alle Shellys angefasst werden — dann ist auch messbar, was es am Funkbild ändert. + ⇒ ✅ **ENTSCHIEDEN UND AUSGEFÜHRT 2026-08-23. User: „eco-mode für alle deaktivieren. der rotz macht + nur probleme."** 13 Geräte umgestellt (s5, s6, s8, s10, s11, s13, s14, s15, s16, s20, s21, s22, s25), + **null Fehler**; 9 standen bereits auf `false`. **Stand jetzt: `eco_mode=false` auf allen 22 + erreichbaren Shellys**, einzige Ausnahme `s24` (SNDC-0D4P10WW meldet `eco_mode: None` — nicht + unterstützt). Offen nur s1/s2/s3 (`weihnachten_*`, saisonal stromlos). + 🔧 **Werkzeug:** `mcp/shelly/migrate.py eco [--go|--on]` — neu, über denselben `_rpc`-Pfad, + mit Retry je Gerät (ein Timeout an einem schwachen Link darf den Sweep nicht abreißen) und einer + Fehlerliste am Ende. + + 🔬 **Und die Gegenmessung widerlegt meine eigene Warnung.** Ich hatte hier notiert, eco aus hebe die + **Sendeleistung** an und sei deshalb eine Airtime-Änderung, die TP25 berühre. **Gemessen stimmt das + nicht:** Signal der 13 geänderten Geräte in der Registration-Table des Managers, vorher/nachher — + **Mittel −0,9 dB, Median −1 dB, Spanne −3..+3**, bei einer **Kontrollgruppe von 13 unveränderten + Geräten mit +0,0 dB und derselben Streuung**. Das ist Rauschen, kein Effekt. + ⇒ **Die Aussage aus TP17, `eco_mode` senke „den CPU-Takt UND die WLAN-Sendeleistung", stimmt nur zur + Hälfte.** Der Takt-Teil ist durch das Dimmer-Flackern bewiesen; der Sendeleistungs-Teil ist an diesen + Geräten nicht messbar. **Kein Funkbild-Argument mehr für oder gegen eco.** - ✅ **AP-Anker-Sweep erledigt (der offene read-only-Punkt aus der Übergabe):** `WiFi.GetConfig → ap.enable` über alle 22 Geräte. **Nur `s23` hatte einen AP an** (= Christophs Workaround, inzwischen abgeschaltet), die übrigen 21 stehen auf `false`. **Kein Aufräumbedarf, kein weiterer Anker im Bestand.** @@ -994,10 +1054,12 @@ den Weg, über den man den Fehler zurücknehmen wollte. auf User-Entscheid 2026-08-23 vorerst unverändert**, weil Redigieren ihn nicht aus der Historie holt und Rotieren heute ~22 Shellys kostet. Nach TP17.1 sind es deutlich weniger. Anlass: das Core-Passwort ist bei einer Debug-Ausgabe im Klartext in ein Sitzungstranskript geraten. Ursache war `sshpass -p`, das es in die **Kommandozeile** - schreibt — sichtbar in `ps` und in jedem argv-Trace. `docs/tp6-marker/server.py` ist auf - `sshpass -e` umgestellt (Passwort nur in der Umgebung des Kindprozesses); **`docs/tp6-monitor-core.sh` - und `docs/tp25-pin-s22.sh` haben dieselbe Schwäche noch** und gehören vor der Rotation - nachgezogen — sonst ist die Rotation nach dem nächsten Messlauf wieder wertlos. + schreibt — sichtbar in `ps` und in jedem argv-Trace. ✅ **Vorbedingung erfüllt, geprüft 2026-08-23:** alle vier + Skripte laufen auf `sshpass -e` (`tp6-marker/server.py`, `tp6-monitor-core.sh`, + `tp25-walk-messung.sh`, `tp25-pin-s22.sh`) — die frühere Notiz, zwei davon hätten die Schwäche + noch, war veraltet. Übrig blieb nur **`CLAUDE.md`, das `sshpass -p` weiterhin aktiv als Reboot-Weg + empfahl** — am 2026-08-23 auf `sshpass -e` korrigiert. **Damit ist die Rotation ohne Rückfallgefahr + fahrbar.** Weg: `manage_user set-password` (⚠️ `/user set*` ist per Deny-Policy blockiert), danach `~/.secrets/credentials.env` (`ROUTER_*_PASS`/`CRS326_*_PASS`) und `~/.claude.json` nachziehen. Es ist **ein** Passwort für alle vier Geräte — die „Überlebensversicherung" aus CLAUDE.md. diff --git a/docs/tp17.1-shelly-gen4-funkmigration-runbook.md b/docs/tp17.1-shelly-gen4-funkmigration-runbook.md index 97da5d1..05259ab 100644 --- a/docs/tp17.1-shelly-gen4-funkmigration-runbook.md +++ b/docs/tp17.1-shelly-gen4-funkmigration-runbook.md @@ -1,5 +1,27 @@ # TP17.1 — Shelly Gen4 raus aus dem WLAN (Thread oder Zigbee) · Runbook +> ❌ **TP17.1 IST VERWORFEN (User-Entscheid 2026-08-23).** Dieses Runbook bleibt als **Wissensspeicher** +> stehen — es ist kein Arbeitsauftrag mehr. +> +> **Warum:** der Pilot hat den Zigbee-Weg technisch **vollständig bewiesen** (s. Schritt 1 unten), +> aber gezeigt, dass das **Ziel** so nicht erreichbar ist — das Gerät bleibt WLAN-Client, der +> gemessene Funkgewinn ist klein, und WLAN abzuschalten wäre unumkehrbar (kein AP auf der +> Zigbee-Firmware, kein Zigbee-OTA, `InstallAlt` braucht IP). +> **Der Weg ist nicht gescheitert, das Ziel war mit ihm nicht erreichbar.** +> +> ✅ **Rückbau am 2026-08-23 durchgeführt:** ZHA-Knoten entfernt (Leave **vor** dem Flash), `s9` zurück +> auf `PowerStrip`, Config aus dem Backup. ⚠️ **Der Rückflash zog einen OFFENEN AP hoch** +> (`ap.enable=true, is_open=true`) — abgeschaltet und mit dem IoT-PSK abgesichert. +> **Merkregel: nach jedem Shelly-Firmwarewechsel den AP prüfen.** +> +> 🔭 **Weiter gültig ist der Frühindikator:** `Shelly.CheckForUpdate` auf einem Gen4 auf einen +> `…MT`/Thread-`alt`-Eintrag beobachten. Erscheint er, ist die Rechnung neu zu machen. +> +> 📌 **Was hier bleibt und Wert hat:** der kartierte Firmware-Mechanismus (drei undokumentierte RPCs, +> Zwei-Slot-Modell), das gemessene 2,4-GHz-Bild aller vier Funknetze, die ZHA-Sturmzähler mit +> Baseline und Abbruchkriterium — und die Erkenntnis, dass ein ZHA-Join dieses Netz **nicht** +> destabilisiert (alle vier roten EZSP-Zähler blieben 0). + > **Status 2026-08-23:** 🔜 **entscheidungsreif, nicht startbereit.** Schritt 0 ist eine Abwägung, > kein Flash. Steuerung/Scope: [`teilprojekte.md`](teilprojekte.md) → **TP17.1**. > **Kein Netz-Eingriff bisher**, alle Zahlen hier sind read-only erhoben. diff --git a/mcp/shelly/migrate.py b/mcp/shelly/migrate.py index 1ed8ead..f5c5d4e 100644 --- a/mcp/shelly/migrate.py +++ b/mcp/shelly/migrate.py @@ -24,6 +24,7 @@ Commands: python3 migrate.py migrate [--go] anchor+sta+reboot+verify (dry-run w/o --go) python3 migrate.py deanchor [--go] turn the AP anchor back off (.30.x IPs) python3 migrate.py auth [--go] set uniform admin password (last step) + python3 migrate.py eco [--go] turn device.eco_mode OFF (--on to re-enable) python3 migrate.py table final tabular overview (old/new/model/fw) accepts single IPs, ranges (10.40.41.205-233) or 'all' (the default sweep). @@ -214,6 +215,62 @@ def cmd_auth(specs, go): print(f"{ip}: auth set (realm {realm})") +def cmd_eco(specs, go, want=False): + """Set device.eco_mode across the fleet (TP17.3, 2026-08-23). + + eco_mode throttles the CPU clock and TX power. It was applied in TP17 by an + RSSI rule and turned out to cause real misbehaviour: the s23 phase-cut dimmer + flickered, and s26's `momentary` input swallowed the first edge. Both were + fixed by switching it off, so the fleet decision is: OFF everywhere. + + No reboot needed — the device answers `restart_required: false` to this write. + Devices that don't support it report eco_mode=None and are skipped (s24). + """ + # Weak links (s20 sits at -85 dBm) time out intermittently. A single timeout + # must not abort the sweep, so every call is retried and failures are + # collected and reported at the end instead of raising. + def _try(ip, method, params=None, tries=3): + for n in range(tries): + try: + return _rpc(ip, method, params, timeout=10.0) + except Exception as e: + last = e + raise last + + failed = [] + for ip in _resolve(specs): + ident = discover.probe(ip) + if not ident: + continue + name = _name_for(ident["mac"]) or ident.get("name_dev") or "?" + try: + cur = _try(ip, "Sys.GetConfig").get("result", {}).get("device", {}).get("eco_mode") + if cur is None: + print(f" {ip:15} {name:26} eco not supported — skipped") + continue + if cur == want: + print(f" {ip:15} {name:26} already {want}") + continue + if not go: + print(f" dry: {ip:11} {name:26} eco {cur} -> {want}") + continue + res = _try(ip, "Sys.SetConfig", {"config": {"device": {"eco_mode": want}}}) + new = _try(ip, "Sys.GetConfig").get("result", {}).get("device", {}).get("eco_mode") + rr = (res.get("result") or {}).get("restart_required") + ok = "OK " if new == want else "MISMATCH" + print(f" {ip:15} {name:26} eco {cur} -> {new} {ok}" + + (" (restart_required was already set before this write)" if rr else "")) + if new != want: + failed.append((ip, name, f"read back {new}")) + except Exception as e: + print(f" {ip:15} {name:26} FAILED: {type(e).__name__}") + failed.append((ip, name, type(e).__name__)) + if failed: + print(f"\n{len(failed)} device(s) need a retry:") + for ip, name, why in failed: + print(f" {ip:15} {name:26} {why}") + + def cmd_table(): inv = discover.load_inventory() ips = discover.expand("10.40.30.180-250") @@ -252,6 +309,8 @@ def main(): cmd_deanchor(specs or ["all"], go) elif cmd == "auth": cmd_auth(specs, go) + elif cmd == "eco": + cmd_eco(specs, go, want=("--on" in rest)) elif cmd == "table": cmd_table() else: