feat: support Shelly 1 Mini Gen 3 and generic devices
This commit is contained in:
parent
00b17c0bb1
commit
1c2e834d45
2 changed files with 48 additions and 22 deletions
34
README.md
34
README.md
|
|
@ -1,17 +1,23 @@
|
||||||
# Helios Smart Home CLI
|
# 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
|
## Voraussetzungen
|
||||||
|
|
||||||
- Python 3.10+
|
- Python 3.10+
|
||||||
- `requests` — `pip install requests`
|
- `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
|
## Installation
|
||||||
|
|
||||||
```bash
|
```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
|
cd helios-smart-home
|
||||||
pip install requests
|
pip install requests
|
||||||
```
|
```
|
||||||
|
|
@ -19,14 +25,14 @@ pip install requests
|
||||||
## Schnellstart
|
## Schnellstart
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Gerät hinzufügen (Alias "1" für physisch beschriftete Steckdose)
|
# Gerät hinzufügen (Alias "lampe" für IP)
|
||||||
python smarthome.py add 192.168.1.50 1
|
python smarthome.py add 192.168.1.50 lampe
|
||||||
|
|
||||||
# Einschalten
|
# Einschalten
|
||||||
python smarthome.py on 1
|
python smarthome.py on lampe
|
||||||
|
|
||||||
# Status abfragen
|
# Status abfragen
|
||||||
python smarthome.py status 1
|
python smarthome.py status lampe
|
||||||
|
|
||||||
# Alle Geräte auflisten
|
# Alle Geräte auflisten
|
||||||
python smarthome.py list
|
python smarthome.py list
|
||||||
|
|
@ -58,17 +64,19 @@ python smarthome.py list
|
||||||
| `on <ziel>` | Relais einschalten |
|
| `on <ziel>` | Relais einschalten |
|
||||||
| `off <ziel>` | Relais ausschalten |
|
| `off <ziel>` | Relais ausschalten |
|
||||||
| `toggle <ziel>` | Relais-Zustand umschalten |
|
| `toggle <ziel>` | Relais-Zustand umschalten |
|
||||||
| `status <ziel>` | Status abfragen (Ein/Aus, Leistung in Watt, Energie) |
|
| `status <ziel>` | Status abfragen (Ein/Aus, Leistung in Watt*, Energie*) |
|
||||||
| `led <ziel> <mode>` | LED-Modus setzen: `switch`, `power` oder `off` |
|
| `led <ziel> <mode>` | LED-Modus setzen: `switch`, `power` oder `off` (nur Plugs) |
|
||||||
| `list` | Alle Geräte und Gruppen anzeigen |
|
| `list` | Alle Geräte und Gruppen anzeigen |
|
||||||
|
|
||||||
|
*\*Falls vom Gerät unterstützt (z.B. Plug S)*
|
||||||
|
|
||||||
## Ziel-Auflösung
|
## Ziel-Auflösung
|
||||||
|
|
||||||
Das `<ziel>`-Argument wird automatisch aufgelöst:
|
Das `<ziel>`-Argument wird automatisch aufgelöst:
|
||||||
|
|
||||||
1. `all` — alle registrierten Geräte
|
1. `all` — alle registrierten Geräte
|
||||||
2. **Gruppenname** — alle Mitglieder der Gruppe
|
2. **Gruppenname** — alle Mitglieder der Gruppe
|
||||||
3. **Alias** — z.B. `1`, `2`, `Drucker`
|
3. **Alias** — z.B. `lampe`, `drucker`
|
||||||
4. **Hardware-ID** — direkte Zuordnung
|
4. **Hardware-ID** — direkte Zuordnung
|
||||||
|
|
||||||
## Datenstruktur
|
## Datenstruktur
|
||||||
|
|
@ -78,10 +86,10 @@ Geräte und Gruppen werden in `mappings.json` neben dem Skript gespeichert:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"devices": {
|
"devices": {
|
||||||
"shellyplugsg3-abc123": { "ip": "192.168.1.50", "alias": "1" }
|
"shellyplugsg3-abc123": { "ip": "192.168.1.50", "alias": "lampe", "model": "SNSN-0013A" }
|
||||||
},
|
},
|
||||||
"groups": {
|
"groups": {
|
||||||
"wohnzimmer": ["1", "2"]
|
"wohnzimmer": ["lampe"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -97,7 +105,7 @@ Beispiel `status`-Antwort:
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"hw_id": "shellyplugsg3-abc123",
|
"hw_id": "shellyplugsg3-abc123",
|
||||||
"alias": "1",
|
"alias": "lampe",
|
||||||
"online": true,
|
"online": true,
|
||||||
"output": true,
|
"output": true,
|
||||||
"apower": 42.5,
|
"apower": 42.5,
|
||||||
|
|
|
||||||
36
smarthome.py
36
smarthome.py
|
|
@ -1,5 +1,5 @@
|
||||||
#!/usr/bin/env python3
|
#!/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 argparse
|
||||||
import json
|
import json
|
||||||
|
|
@ -119,11 +119,14 @@ def cmd_add(args: argparse.Namespace) -> None:
|
||||||
print("ERROR: device response missing 'id' field", file=sys.stderr)
|
print("ERROR: device response missing 'id' field", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Store device model if available to distinguish capabilities later
|
||||||
|
model = info.get("model", "unknown")
|
||||||
|
|
||||||
data = load_mappings()
|
data = load_mappings()
|
||||||
alias = str(args.alias) if args.alias is not None else ""
|
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)
|
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:
|
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}
|
entry = {"hw_id": dev["hw_id"], "alias": dev.get("alias", ""), "online": resp is not None}
|
||||||
if resp is not None:
|
if resp is not None:
|
||||||
entry["output"] = resp.get("output")
|
entry["output"] = resp.get("output")
|
||||||
entry["apower"] = resp.get("apower")
|
# Shelly 1 Mini Gen3 has no power measurement, check if key exists
|
||||||
entry["aenergy_total"] = resp.get("aenergy", {}).get("total")
|
if "apower" in resp:
|
||||||
|
entry["apower"] = resp["apower"]
|
||||||
|
if "aenergy" in resp:
|
||||||
|
entry["aenergy_total"] = resp["aenergy"].get("total")
|
||||||
results.append(entry)
|
results.append(entry)
|
||||||
print(json.dumps(results))
|
print(json.dumps(results))
|
||||||
|
|
||||||
|
|
||||||
def cmd_led(args: argparse.Namespace) -> None:
|
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")
|
valid_modes = ("switch", "power", "off")
|
||||||
if args.mode not in valid_modes:
|
if args.mode not in valid_modes:
|
||||||
print(f"ERROR: invalid LED mode '{args.mode}', must be one of {valid_modes}", file=sys.stderr)
|
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}}}
|
payload = {"config": {"leds": {"mode": args.mode}}}
|
||||||
results = []
|
results = []
|
||||||
for dev in resolve_targets(data, args.target):
|
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)
|
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))
|
print(json.dumps(results))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -275,7 +293,7 @@ def cmd_list(_args: argparse.Namespace) -> None:
|
||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
"""Build and return the argument parser."""
|
"""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)
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
# add
|
# add
|
||||||
|
|
@ -320,7 +338,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
p_st.add_argument("target", help="Alias, hardware_id, group, or 'all'")
|
p_st.add_argument("target", help="Alias, hardware_id, group, or 'all'")
|
||||||
|
|
||||||
# led
|
# 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("target", help="Alias, hardware_id, group, or 'all'")
|
||||||
p_led.add_argument("mode", choices=["switch", "power", "off"], help="LED mode")
|
p_led.add_argument("mode", choices=["switch", "power", "off"], help="LED mode")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue