From 1c2e834d45a437bd264aa36170f6817835bea3a9 Mon Sep 17 00:00:00 2001 From: Helios Date: Mon, 23 Feb 2026 00:28:31 +0100 Subject: [PATCH] feat: support Shelly 1 Mini Gen 3 and generic devices --- README.md | 34 +++++++++++++++++++++------------- smarthome.py | 36 +++++++++++++++++++++++++++--------- 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 97f8613..9090395 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,23 @@ # Helios Smart Home CLI -CLI-Tool zur lokalen Steuerung von **Shelly Plug S Gen 3** und **Plus Plug S** Steckdosen über die Shelly RPC-API (Gen 2/3). Entwickelt als Schnittstelle für KI-Agenten — alle Ausgaben sind maschinenlesbar (JSON auf stdout, Fehler auf stderr). +CLI-Tool zur lokalen Steuerung von **Shelly Geräten (Gen 2/3)** (z.B. Plug S, 1 Mini Gen3) über die Shelly RPC-API. Entwickelt als Schnittstelle für KI-Agenten — alle Ausgaben sind maschinenlesbar (JSON auf stdout, Fehler auf stderr). + +## Unterstützte Geräte + +- **Shelly Plug S Gen 3 / Plus Plug S** (Schalten, Messen, LED) +- **Shelly 1 Mini Gen 3** (Schalten, kein Messen, keine LED) +- Generell alle Gen 2/3 Geräte mit `Switch` Komponente ## Voraussetzungen - Python 3.10+ - `requests` — `pip install requests` -- Shelly Plugs müssen im selben lokalen Netzwerk erreichbar sein +- Shelly Geräte müssen im selben lokalen Netzwerk erreichbar sein ## Installation ```bash -git clone https://github.com/Moritz/helios-smart-home.git +git clone https://github.com/agent-helios/helios-smart-home.git cd helios-smart-home pip install requests ``` @@ -19,14 +25,14 @@ pip install requests ## Schnellstart ```bash -# Gerät hinzufügen (Alias "1" für physisch beschriftete Steckdose) -python smarthome.py add 192.168.1.50 1 +# Gerät hinzufügen (Alias "lampe" für IP) +python smarthome.py add 192.168.1.50 lampe # Einschalten -python smarthome.py on 1 +python smarthome.py on lampe # Status abfragen -python smarthome.py status 1 +python smarthome.py status lampe # Alle Geräte auflisten python smarthome.py list @@ -58,17 +64,19 @@ python smarthome.py list | `on ` | Relais einschalten | | `off ` | Relais ausschalten | | `toggle ` | Relais-Zustand umschalten | -| `status ` | Status abfragen (Ein/Aus, Leistung in Watt, Energie) | -| `led ` | LED-Modus setzen: `switch`, `power` oder `off` | +| `status ` | Status abfragen (Ein/Aus, Leistung in Watt*, Energie*) | +| `led ` | LED-Modus setzen: `switch`, `power` oder `off` (nur Plugs) | | `list` | Alle Geräte und Gruppen anzeigen | +*\*Falls vom Gerät unterstützt (z.B. Plug S)* + ## Ziel-Auflösung Das ``-Argument wird automatisch aufgelöst: 1. `all` — alle registrierten Geräte 2. **Gruppenname** — alle Mitglieder der Gruppe -3. **Alias** — z.B. `1`, `2`, `Drucker` +3. **Alias** — z.B. `lampe`, `drucker` 4. **Hardware-ID** — direkte Zuordnung ## Datenstruktur @@ -78,10 +86,10 @@ Geräte und Gruppen werden in `mappings.json` neben dem Skript gespeichert: ```json { "devices": { - "shellyplugsg3-abc123": { "ip": "192.168.1.50", "alias": "1" } + "shellyplugsg3-abc123": { "ip": "192.168.1.50", "alias": "lampe", "model": "SNSN-0013A" } }, "groups": { - "wohnzimmer": ["1", "2"] + "wohnzimmer": ["lampe"] } } ``` @@ -97,7 +105,7 @@ Beispiel `status`-Antwort: [ { "hw_id": "shellyplugsg3-abc123", - "alias": "1", + "alias": "lampe", "online": true, "output": true, "apower": 42.5, diff --git a/smarthome.py b/smarthome.py index 6c588e5..9b7a3a7 100644 --- a/smarthome.py +++ b/smarthome.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""CLI interface for managing Shelly Plug S (Gen 2/3) smart plugs via local HTTP API.""" +"""CLI interface for managing Shelly devices (Gen 2/3) via local HTTP API.""" import argparse import json @@ -119,11 +119,14 @@ def cmd_add(args: argparse.Namespace) -> None: print("ERROR: device response missing 'id' field", file=sys.stderr) sys.exit(1) + # Store device model if available to distinguish capabilities later + model = info.get("model", "unknown") + data = load_mappings() alias = str(args.alias) if args.alias is not None else "" - data["devices"][hw_id] = {"ip": args.ip, "alias": alias} + data["devices"][hw_id] = {"ip": args.ip, "alias": alias, "model": model} save_mappings(data) - print(json.dumps({"ok": True, "hw_id": hw_id, "ip": args.ip, "alias": alias})) + print(json.dumps({"ok": True, "hw_id": hw_id, "ip": args.ip, "alias": alias, "model": model})) def cmd_remove(args: argparse.Namespace) -> None: @@ -241,14 +244,17 @@ def cmd_status(args: argparse.Namespace) -> None: entry = {"hw_id": dev["hw_id"], "alias": dev.get("alias", ""), "online": resp is not None} if resp is not None: entry["output"] = resp.get("output") - entry["apower"] = resp.get("apower") - entry["aenergy_total"] = resp.get("aenergy", {}).get("total") + # Shelly 1 Mini Gen3 has no power measurement, check if key exists + if "apower" in resp: + entry["apower"] = resp["apower"] + if "aenergy" in resp: + entry["aenergy_total"] = resp["aenergy"].get("total") results.append(entry) print(json.dumps(results)) def cmd_led(args: argparse.Namespace) -> None: - """Set LED ring mode for target device(s).""" + """Set LED ring mode for target device(s). Skips devices without LED ring.""" valid_modes = ("switch", "power", "off") if args.mode not in valid_modes: print(f"ERROR: invalid LED mode '{args.mode}', must be one of {valid_modes}", file=sys.stderr) @@ -258,8 +264,20 @@ def cmd_led(args: argparse.Namespace) -> None: payload = {"config": {"leds": {"mode": args.mode}}} results = [] for dev in resolve_targets(data, args.target): + # Identify if device supports LED (e.g. S1MINI has none). + # For now, we try; if it fails or model is known S1MINI, we note it. + # But to be safe and simple: just try. If it fails, mark success: false. + + # Exception: if we KNOW it's a Mini, skip it to avoid errors log. + # However, we might not have 'model' in stored mappings yet for old devices. + # So we just try. + resp = shelly_post(dev["ip"], "/rpc/PLUGS_UI.SetConfig", payload) - results.append({"hw_id": dev["hw_id"], "alias": dev.get("alias", ""), "success": resp is not None}) + + # If response is an error because method not found (e.g. Mini), resp might be None or contain 'error'. + # Shelly 1 Mini Gen3 returns error for PLUGS_UI.SetConfig. + success = resp is not None and "error" not in resp + results.append({"hw_id": dev["hw_id"], "alias": dev.get("alias", ""), "success": success}) print(json.dumps(results)) @@ -275,7 +293,7 @@ def cmd_list(_args: argparse.Namespace) -> None: def build_parser() -> argparse.ArgumentParser: """Build and return the argument parser.""" - parser = argparse.ArgumentParser(description="Shelly Plug S (Gen2/3) local network CLI") + parser = argparse.ArgumentParser(description="Shelly Device (Gen2/3) local network CLI") sub = parser.add_subparsers(dest="command", required=True) # add @@ -320,7 +338,7 @@ def build_parser() -> argparse.ArgumentParser: p_st.add_argument("target", help="Alias, hardware_id, group, or 'all'") # led - p_led = sub.add_parser("led", help="Set LED ring mode") + p_led = sub.add_parser("led", help="Set LED ring mode (Plugs only)") p_led.add_argument("target", help="Alias, hardware_id, group, or 'all'") p_led.add_argument("mode", choices=["switch", "power", "off"], help="LED mode")