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 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 21:12:46 +02:00
parent 0a7f2e2c08
commit 2f183190ed
4 changed files with 163 additions and 17 deletions

View File

@@ -24,6 +24,7 @@ Commands:
python3 migrate.py migrate <ip...> [--go] anchor+sta+reboot+verify (dry-run w/o --go)
python3 migrate.py deanchor <ip...> [--go] turn the AP anchor back off (.30.x IPs)
python3 migrate.py auth <ip...> [--go] set uniform admin password (last step)
python3 migrate.py eco <ip...> [--go] turn device.eco_mode OFF (--on to re-enable)
python3 migrate.py table final tabular overview (old/new/model/fw)
<ip...> 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: