Discovery: alle Geraete finden und stabil ablegen

Ein Lauf mit allen Modulen brachte 54 Geraete und 441 Messwerte - und fuenf
Fehler ans Licht, die vorher niemand sehen konnte, weil clear_tables die
Tabellen bei jedem Lauf geleert hat.

Vier davon sind Schluessel- und Upsert-Fehler derselben Familie:

 * command_parameters war ueber (command_id, url) eindeutig. WLED-Parameter
   haben keine URL - ihr Name ist der Platzhalter in der Kommando-Vorlage -,
   und eine NULL kollidiert in MySQL nie. ON DUPLICATE KEY UPDATE griff
   also nicht, und jeder Lauf legte dieselben Parameter erneut an: aus 18
   wurden nach drei Laeufen 54. Schluessel jetzt (command_id,
   parameter_name).

 * actor_states war ueber (actor_id, url) eindeutig. Umgekehrtes Problem:
   Messwerte, die sich ein Topic teilen, ueberschrieben einander. Der
   go-eCharger schickt sechzehn Werte als JSON-Feld auf einem Topic - von
   denen kam genau einer in der Datenbank an. Schluessel jetzt (actor_id,
   state_name), das bringt 50 verlorene Messwerte zurueck.

 * Kommandos, Messwerte und Parameter aktualisierten ihre URL beim
   Wiederholungslauf nicht - sie stand nicht im UPDATE-Teil. Eine Korrektur
   in einem Modul kam damit nie in einer bestehenden Datenbank an.

 * Der Typ eines Kommando-Parameters wurde als Text in eine int-Spalte
   geschrieben. MariaDB macht daraus stillschweigend 0, und 0 ist "bool" -
   deshalb bot der Editor fuer die WLED-Helligkeit (0..255) ein Ja/Nein an.

Der fuenfte: die Shelly-Gen1-Relais hatten weder eine Kommando-URL noch eine
URL am Zustand. Ohne die weiss niemand, was zu schicken und wo nachzusehen
ist. Gen1 schaltet ueber ?turn=on|off|toggle und meldet sich in "ison".

Damit entfaellt auch die Uebersetzungstabelle im HTTPTransport: die
Zuordnung gehoert ins Geraetemodell, nicht in den Runner.

Zwei Luecken auf der Runner-Seite, die derselbe Lauf gezeigt hat:

 * WLED liess sich gar nicht ansteuern - es gab keinen Transport dafuer.
   Der neue nutzt die JSON-Vorlage aus command_url, fuellt die Platzhalter
   und schickt sie am Stueck an /json/state.

 * Tahoma wurde am Schema "io://" erkannt. Das Schema beschreibt aber die
   Funkart: dieselbe Box liefert rts:// fuer die Dachfenster und
   internal:// fuer die Alarmanlage. Drei von 22 Geraeten fielen durch.
   Erkannt wird jetzt an der Box-Kennung in der URL.

Nachgemessen: zwei Laeufe hintereinander aendern keine einzige Zeile mehr,
die Geraete-IDs bleiben ueber Laeufe stabil (Voraussetzung fuer die
Fremdschluessel der Automatiken), alle 54 Geraete finden genau einen
Transport, und 440 der 441 Messwerte sind tatsaechlich lesbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-31 14:57:29 +02:00
co-authored by Claude Opus 5
parent 0cb1fc635e
commit 631baa7333
6 changed files with 180 additions and 46 deletions
+10 -2
View File
@@ -261,7 +261,11 @@ ALTER TABLE `actor_commands`
-- --
ALTER TABLE `actor_states` ALTER TABLE `actor_states`
ADD PRIMARY KEY (`id`), ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `actor_id_2` (`actor_id`,`url`), -- Auf den Namen und nicht auf die URL, aus demselben Grund wie bei
-- command_parameters: korrigiert ein Modul die URL eines Messwerts, soll
-- die vorhandene Zeile geaendert werden. Am URL-Schluessel entstand
-- stattdessen eine zweite Zeile, und die alte blieb ohne URL zurueck.
ADD UNIQUE KEY `actor_id_2` (`actor_id`,`state_name`),
ADD KEY `actor_id` (`actor_id`), ADD KEY `actor_id` (`actor_id`),
ADD KEY `idx_actor_states_name` (`state_name`), ADD KEY `idx_actor_states_name` (`state_name`),
ADD KEY `state_type` (`state_type`); ADD KEY `state_type` (`state_type`);
@@ -271,7 +275,11 @@ ALTER TABLE `actor_states`
-- --
ALTER TABLE `command_parameters` ALTER TABLE `command_parameters`
ADD PRIMARY KEY (`id`), ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `command_id_2` (`command_id`,`url`), -- Auf den Namen und nicht auf die URL: Parameter ohne URL (WLED traegt
-- alles in der Kommando-Vorlage) haetten dort NULL, und eine NULL
-- kollidiert in MySQL nie - ON DUPLICATE KEY UPDATE griff nicht und jeder
-- Discovery-Lauf legte die Parameter erneut an.
ADD UNIQUE KEY `command_id_2` (`command_id`,`parameter_name`),
ADD KEY `command_id` (`command_id`); ADD KEY `command_id` (`command_id`);
-- --
+17 -9
View File
@@ -114,19 +114,27 @@ Fenster gar nichts gelaufen ist — dann ist auch keine Sperre aktiv.
Welcher Weg zum Gerät führt, entscheidet die URL des Aktors in `actors`: Welcher Weg zum Gerät führt, entscheidet die URL des Aktors in `actors`:
| URL | Messwert (`actor_states.url`) | Kommando | | URL | Messwert (`actor_states.url`) | Kommando (`actor_commands.command_url`) |
|-------------|-------------------------------|-------------------------------------| |---|---|---|
| `mqtt://…` | vollständiges Topic, abonniert | publiziert auf das Parameter-Topic | | `mqtt://…` | vollständiges Topic, abonniert | Nutzlast auf das Parameter-Topic |
| `http://…` | Feldname in der JSON-Antwort, gepollt | Parameter als Abfrageargumente | | `http://…` | Feldname in der JSON-Antwort, gepollt | Abfrageargumente an die Geräte-URL (`turn=on`) |
| `io://…` | Tahoma-Statusname, gepollt | `exec/apply` an die Tahoma-Box | | `wled://…` | Pfad in `/json/state` (`seg[0].col[0]`), gepollt | JSON-Vorlage mit Platzhaltern, als Ganzes gesendet |
| Tahoma | Statusname (`core:ClosureState`), gepollt | `exec/apply` an die Box |
| `Logic` | gerechnet: Uhrzeit, Datum, Sonne | | | `Logic` | gerechnet: Uhrzeit, Datum, Sonne | |
Alle vier stehen in `transports.py`. Eine fünfte Geräteart kommt als weitere Alle fünf stehen in `transports.py`. Eine sechste Geräteart kommt als weitere
Klasse dazu; sie braucht `passt()`, `zustaende_lesen()` und `senden()`. Klasse dazu; sie braucht `passt()`, `zustaende_lesen()` und `senden()`.
Bei HTTP heißen die Schaltbefehle nicht so wie in der Datenbank — dafür gibt es Tahoma ist der einzige, der nicht am URL-Schema erkannt wird, sondern an der
in `HTTPTransport` eine kleine Übersetzungstabelle (`turn_on``on=true`). **Box-Kennung** in der URL. Das Schema beschreibt dort die Funkart, und
Das ist die Stelle, die beim Anschluss neuer HTTP-Geräte wächst. dieselbe Box liefert `io://` für die Jalousien, `rts://` für die Dachfenster
und `internal://` für die Alarmanlage. Ohne `pin` in der `config.ini` ist
niemand zuständig — dann meldet der Runner beim Auslösen „kein Transport",
statt still nichts zu tun.
Bei WLED trägt die Kommando-Vorlage alles: `{"seg":[{"col":[[%red%,%green%,%blue%]]}]}`
wird mit den Parameterwerten gefüllt und am Stück geschickt. Deshalb haben die
Parameter dort keine eigene URL — ihr Name *ist* der Platzhalter.
## Voraussetzungen ## Voraussetzungen
+2 -1
View File
@@ -55,7 +55,7 @@ import requests
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
from transports import (HTTPTransport, LogicTransport, MQTTTransport, from transports import (HTTPTransport, LogicTransport, MQTTTransport,
TahomaTransport) TahomaTransport, WLEDTransport)
logger = logging.getLogger("autoaction") logger = logging.getLogger("autoaction")
@@ -377,6 +377,7 @@ class Runner:
self.mqtt = self._mqtt_verbinden() self.mqtt = self._mqtt_verbinden()
self.transporte = [ self.transporte = [
MQTTTransport(self.mqtt, self.dry_run), MQTTTransport(self.mqtt, self.dry_run),
WLEDTransport(requests, dry_run=self.dry_run),
HTTPTransport(requests, dry_run=self.dry_run), HTTPTransport(requests, dry_run=self.dry_run),
TahomaTransport(requests, config.text("tahoma", "pin"), TahomaTransport(requests, config.text("tahoma", "pin"),
config.text("tahoma", "token"), config.text("tahoma", "token"),
+133 -18
View File
@@ -8,11 +8,14 @@ URL des Aktors in der Tabelle `actors`:
mqtt://... MQTT-Geraete (Home-Assistant-Discovery) mqtt://... MQTT-Geraete (Home-Assistant-Discovery)
http://... Shelly und andere HTTP-Geraete http://... Shelly und andere HTTP-Geraete
io://... Tahoma wled://... WLED-Lampen
io://, rts://, internal://, ogp:// Tahoma - das Schema haengt an der
Funkart des Geraets, deshalb wird dort nicht danach
entschieden, sondern an der Box-Kennung in der URL
Logic das gerechnete Geraet "Zeitpunkt" (Uhrzeit, Datum, Sonne) Logic das gerechnete Geraet "Zeitpunkt" (Uhrzeit, Datum, Sonne)
Alle liegen in einer Datei statt in einem Paket wie bei deviceDiscovery: es Alle liegen in einer Datei statt in einem Paket wie bei deviceDiscovery: es
sind vier kurze Klassen, und wer eine fuenfte Geraeteart anschliesst, sieht sind fuenf kurze Klassen, und wer eine sechste Geraeteart anschliesst, sieht
hier auf einen Blick, was dafuer zu tun ist. hier auf einen Blick, was dafuer zu tun ist.
Jeder Transport hat zwei Haelften: Jeder Transport hat zwei Haelften:
@@ -28,11 +31,19 @@ Jeder Transport hat zwei Haelften:
import json import json
import logging import logging
import re
import urllib3
from datetime import datetime from datetime import datetime
from urllib.parse import quote from urllib.parse import quote
logger = logging.getLogger("autoaction.transport") logger = logging.getLogger("autoaction.transport")
# Die Tahoma-Box hat ein selbst ausgestelltes Zertifikat auf einen Namen, den
# nur das Heimnetz kennt. Die Pruefung ist dort bewusst aus (wie in
# ajax/tahoma.php); ohne diese Zeile warnt urllib3 bei jeder einzelnen
# Abfrage und uebertoent das eigentliche Protokoll.
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
class Transport: class Transport:
"""Gemeinsame Form. Wer nichts zu lesen hat, erbt die leeren Methoden.""" """Gemeinsame Form. Wer nichts zu lesen hat, erbt die leeren Methoden."""
@@ -137,20 +148,15 @@ class HTTPTransport(Transport):
JSON-Antwort ("tC", "a_voltage"). Solche Geraete melden sich nicht von JSON-Antwort ("tC", "a_voltage"). Solche Geraete melden sich nicht von
selbst, sie werden im Poll-Takt gefragt. selbst, sie werden im Poll-Takt gefragt.
Beim Senden haengen die Parameter als Abfrageargumente an die actor_url. Beim Senden wird die command_url als Abfrageargument an die actor_url
Die Schaltbefehle von Shelly heissen aber nicht so, wie sie in der gehaengt ("turn=on") und die Parameter mit ihrem eigenen Namen dazu.
Datenbank stehen - dafuer steht unten eine kleine Uebersetzungstabelle. Frueher stand hier eine Uebersetzungstabelle, weil die Shelly-Kommandos
Wer ein anderes HTTP-Geraet anschliesst, erweitert genau die. ohne URL in der Datenbank landeten - das ist im Discovery behoben, die
Zuordnung gehoert dorthin und nicht in den Runner.
""" """
schema = "http" schema = "http"
SHELLY_BEFEHLE = {
"turn_on": {"on": "true"},
"turn_off": {"on": "false"},
"toggle": {"toggle": "true"},
}
def __init__(self, requests_modul, timeout=5, dry_run=False): def __init__(self, requests_modul, timeout=5, dry_run=False):
self.requests = requests_modul self.requests = requests_modul
self.timeout = timeout self.timeout = timeout
@@ -181,14 +187,17 @@ class HTTPTransport(Transport):
return werte return werte
def senden(self, aktion): def senden(self, aktion):
argumente = dict(self.SHELLY_BEFEHLE.get(aktion["command_url"], {})) argumente = {}
for teil in (aktion["command_url"] or "").split("&"):
if "=" in teil:
schluessel, wert = teil.split("=", 1)
argumente[schluessel] = wert
for p in aktion["params"]: for p in aktion["params"]:
if p["url"]: if p["url"]:
argumente[p["url"]] = p["wert"] argumente[p["url"]] = p["wert"]
if not argumente and aktion["command_url"]: if not argumente:
# Kein bekannter Schaltbefehl und keine Parameter: das Kommando raise RuntimeError("Kommando ohne URL und ohne Parameter - im "
# als Argument anhaengen, mehr laesst sich hier nicht ableiten. "Geraetemodell fehlt die Angabe, was zu schicken ist")
argumente[aktion["command_url"]] = "true"
if self.dry_run: if self.dry_run:
logger.info("[dry-run] HTTP %s %s", aktion["actor_url"], argumente) logger.info("[dry-run] HTTP %s %s", aktion["actor_url"], argumente)
return return
@@ -197,16 +206,119 @@ class HTTPTransport(Transport):
raise RuntimeError("HTTP %d von %s" % (antwort.status_code, aktion["actor_url"])) raise RuntimeError("HTTP %d von %s" % (antwort.status_code, aktion["actor_url"]))
# ---------------------------------------------------------------------------
# WLED
# ---------------------------------------------------------------------------
class WLEDTransport(Transport):
"""
WLED-Lampen sprechen ueber eine einzige JSON-Schnittstelle:
GET http://IP/json/state liefert den Zustand, POST dorthin setzt ihn.
Das Geraetemodell nutzt das elegant aus - die command_url ist eine
JSON-Vorlage mit Platzhaltern:
{"bri":%brightness%}
{"seg":[{"col":[[%red%,%green%,%blue%]]}]}
Gesendet wird also nicht Argument fuer Argument, sondern die ausgefuellte
Vorlage am Stueck. Deshalb haben die Parameter hier auch keine eigene URL:
ihr Name ist der Platzhalter.
Die state_url ist ein Pfad in die Antwort ("on", "bri",
"seg[0].col[0]") - dieselbe Schreibweise, die auch in current_value steht.
"""
schema = "wled://"
def __init__(self, requests_modul, timeout=5, dry_run=False):
self.requests = requests_modul
self.timeout = timeout
self.dry_run = dry_run
self.states = []
@staticmethod
def _adresse(actor_url):
return "http://" + actor_url[len("wled://"):].rstrip("/")
def zustaende_anmelden(self, states):
self.states = [s for s in states if s["state_url"]]
logger.info("WLED: %d Messwerte an %d Lampen",
len(self.states), len({s["actor_url"] for s in self.states}))
@staticmethod
def _pfad(daten, pfad):
""""seg[0].col[0]" in der Antwort nachschlagen."""
for teil in pfad.split("."):
treffer = re.match(r"^([^\[]*)((?:\[\d+\])*)$", teil)
if not treffer:
return None
if treffer.group(1):
daten = daten[treffer.group(1)]
for index in re.findall(r"\[(\d+)\]", treffer.group(2)):
daten = daten[int(index)]
return daten
def zustaende_lesen(self):
werte = {}
nach_lampe = {}
for s in self.states:
nach_lampe.setdefault(s["actor_url"], []).append(s)
for actor_url, states in nach_lampe.items():
try:
antwort = self.requests.get(self._adresse(actor_url) + "/json/state",
timeout=self.timeout)
daten = antwort.json()
except Exception as fehler:
logger.debug("WLED %s nicht erreichbar: %s", actor_url, fehler)
continue
for s in states:
try:
werte[s["id"]] = str(self._pfad(daten, s["state_url"]))
except (KeyError, IndexError, TypeError):
logger.debug("WLED %s: Pfad %s nicht gefunden", actor_url, s["state_url"])
return werte
def senden(self, aktion):
vorlage = aktion["command_url"]
if not vorlage:
raise RuntimeError("WLED-Kommando ohne Vorlage")
for p in aktion["params"]:
vorlage = vorlage.replace("%" + p["name"] + "%", str(p["wert"]))
try:
rumpf = json.loads(vorlage)
except ValueError:
# Ein nicht ersetzter Platzhalter oder ein Textwert an einer
# Stelle, wo eine Zahl stehen muss. Lieber hier abbrechen als der
# Lampe etwas Unverstaendliches schicken.
raise RuntimeError("WLED-Vorlage ergibt kein gueltiges JSON: " + vorlage[:120])
if self.dry_run:
logger.info("[dry-run] WLED %s <- %s", aktion["actor_url"],
json.dumps(rumpf, ensure_ascii=False))
return
antwort = self.requests.post(self._adresse(aktion["actor_url"]) + "/json/state",
json=rumpf, timeout=self.timeout)
if antwort.status_code >= 400:
raise RuntimeError("WLED antwortete mit %d" % antwort.status_code)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Tahoma # Tahoma
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TahomaTransport(Transport): class TahomaTransport(Transport):
""" """
Die actor_url ist die deviceURL (io://...), die state_url ein Statusname Die actor_url ist die deviceURL, die state_url ein Statusname
("core:ClosureState"), die command_url ein Kommandoname ("setClosure"). ("core:ClosureState"), die command_url ein Kommandoname ("setClosure").
Geschickt wird ueber exec/apply - genauso wie in ajax/tahoma.php, nur ohne Geschickt wird ueber exec/apply - genauso wie in ajax/tahoma.php, nur ohne
die dortige Sonderbehandlung fuer "faehrt gerade". die dortige Sonderbehandlung fuer "faehrt gerade".
Zustaendig ist dieser Transport fuer alles, was die Kennung der eigenen
Box in der URL traegt. Am Schema laesst sich das nicht festmachen: es
beschreibt die Funkart, und dieselbe Box liefert io:// fuer die
Jalousien, rts:// fuer die Dachfenster und internal:// fuer die Alarm-
anlage. Ohne PIN in der config.ini ist niemand zustaendig - dann meldet
der Runner beim Ausloesen "kein Transport", statt still nichts zu tun.
""" """
schema = "io://" schema = "io://"
@@ -219,6 +331,9 @@ class TahomaTransport(Transport):
self.dry_run = dry_run self.dry_run = dry_run
self.states = [] self.states = []
def passt(self, actor_url):
return bool(self.pin) and ("://" + self.pin + "/") in actor_url
@property @property
def basis(self): def basis(self):
return "https://gateway-%s:8443/enduser-mobile-web/1/enduserAPI" % self.pin return "https://gateway-%s:8443/enduser-mobile-web/1/enduserAPI" % self.pin
+10 -10
View File
@@ -365,13 +365,13 @@ class DatabaseManager:
INSERT INTO actor_commands (actor_id, command_name, command_url) INSERT INTO actor_commands (actor_id, command_name, command_url)
VALUES (%s, %s, %s) VALUES (%s, %s, %s)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
command_name = VALUES(command_name) command_url = VALUES(command_url)
""" """
cursor.execute(cmd_query, (actor_id, command_name, command_url)) cursor.execute(cmd_query, (actor_id, command_name, command_url))
query = """ query = """
SELECT ID FROM actor_commands where actor_id = %s and command_url = %s SELECT ID FROM actor_commands where actor_id = %s and command_name = %s
""" """
cursor.execute(query, (actor_id, command_url)) cursor.execute(query, (actor_id, command_name))
command_id = cursor.fetchone()[0] command_id = cursor.fetchone()[0]
# Parameter einfügen # Parameter einfügen
@@ -381,14 +381,14 @@ class DatabaseManager:
(command_id, parameter_name, parameter_type, min_value, max_value, possible_values, url) (command_id, parameter_name, parameter_type, min_value, max_value, possible_values, url)
VALUES (%s, %s, %s, %s, %s, %s, %s) VALUES (%s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
parameter_name = VALUES(parameter_name),
parameter_type = VALUES(parameter_type), parameter_type = VALUES(parameter_type),
min_value = VALUES(min_value), min_value = VALUES(min_value),
max_value = VALUES(max_value), max_value = VALUES(max_value),
possible_values = VALUES(possible_values) possible_values = VALUES(possible_values),
url = VALUES(url)
""" """
param_name = param.get('name', '') param_name = param.get('name', '')
param_type = param.get('type', '') param_type = self.PARAMETER_TYPES_MAPPING.get(param.get('type', ''), 4)
min_val = param.get('min') min_val = param.get('min')
max_val = param.get('max') max_val = param.get('max')
possible_vals = json.dumps(param.get('values')) if 'values' in param else "" possible_vals = json.dumps(param.get('values')) if 'values' in param else ""
@@ -404,11 +404,11 @@ class DatabaseManager:
(actor_id, state_name, state_type, current_value, unit, url, possible_values) (actor_id, state_name, state_type, current_value, unit, url, possible_values)
VALUES (%s, %s, %s, %s, %s, %s, %s) VALUES (%s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
state_name = VALUES(state_name),
state_type = VALUES(state_type), state_type = VALUES(state_type),
current_value = VALUES(current_value), current_value = VALUES(current_value),
possible_values = VALUES(possible_values), possible_values = VALUES(possible_values),
unit = VALUES(unit) unit = VALUES(unit),
url = VALUES(url)
""" """
state_name = state.get('name', '') state_name = state.get('name', '')
state_type = self.PARAMETER_TYPES_MAPPING.get(state.get('type', 0), 4) state_type = self.PARAMETER_TYPES_MAPPING.get(state.get('type', 0), 4)
@@ -474,10 +474,10 @@ class DatabaseManager:
(actor_id, state_name, state_type, current_value, unit, url) (actor_id, state_name, state_type, current_value, unit, url)
VALUES (%s, %s, %s, %s, %s, %s) VALUES (%s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
state_name = VALUES(state_name),
state_type = VALUES(state_type), state_type = VALUES(state_type),
current_value = VALUES(current_value), current_value = VALUES(current_value),
unit = VALUES(unit) unit = VALUES(unit),
url = VALUES(url)
""" """
state_name = state.get('name', '') state_name = state.get('name', '')
state_type = self.PARAMETER_TYPES_MAPPING.get(state.get('type', 0), 4) state_type = self.PARAMETER_TYPES_MAPPING.get(state.get('type', 0), 4)
@@ -267,14 +267,15 @@ class ShellyModule(BaseModule):
'name': f"{device_name}_Switch_{i}", 'name': f"{device_name}_Switch_{i}",
'url': f"http://{ip}/rpc/Switch.Set?id={i}", 'url': f"http://{ip}/rpc/Switch.Set?id={i}",
'commands': [ 'commands': [
{'command': 'turn_on', 'parameters': []}, {'command': 'turn_on', 'url': 'on=true', 'parameters': []},
{'command': 'turn_off', 'parameters': []}, {'command': 'turn_off', 'url': 'on=false', 'parameters': []},
{'command': 'toggle', 'parameters': []} {'command': 'toggle', 'parameters': []}
], ],
'states': [ 'states': [
{ {
'name': 'output', 'name': 'output',
'type': 'boolean', 'type': 'boolean',
'url': 'output',
'current_value': switch_data.get('output', False) 'current_value': switch_data.get('output', False)
} }
] ]
@@ -444,14 +445,15 @@ class ShellyModule(BaseModule):
'name': f"{device_name}_Relay_{i}", 'name': f"{device_name}_Relay_{i}",
'url': f"http://{ip}/relay/{i}", 'url': f"http://{ip}/relay/{i}",
'commands': [ 'commands': [
{'command': 'turn_on', 'parameters': []}, {'command': 'turn_on', 'url': 'turn=on', 'parameters': []},
{'command': 'turn_off', 'parameters': []}, {'command': 'turn_off', 'url': 'turn=off', 'parameters': []},
{'command': 'toggle', 'parameters': []} {'command': 'toggle', 'url': 'turn=toggle', 'parameters': []}
], ],
'states': [ 'states': [
{ {
'name': 'ison', 'name': 'ison',
'type': 'boolean', 'type': 'boolean',
'url': 'ison',
'current_value': relay.get('ison', False) 'current_value': relay.get('ison', False)
} }
] ]