Compare commits
3
Commits
0cb1fc635e
...
17678da824
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17678da824 | ||
|
|
56b2ab1648 | ||
|
|
631baa7333 |
+11
-2
@@ -68,6 +68,7 @@ CREATE TABLE `actor_states` (
|
||||
`current_value` varchar(255) DEFAULT NULL COMMENT 'Aktueller Wert des State',
|
||||
`unit` varchar(20) DEFAULT NULL COMMENT 'Einheit falls vorhanden',
|
||||
`url` varchar(255) DEFAULT NULL COMMENT 'MQTT Topic oder URL zum State',
|
||||
`value_path` varchar(120) DEFAULT NULL COMMENT 'Pfad in die Nutzlast, wenn sich mehrere Messwerte ein Topic teilen: der go-eCharger schickt sechzehn Zahlen als JSON-Feld auf einem Topic. Schreibweise [3], ssid, foo[0].bar. NULL = ganze Nutzlast',
|
||||
`last_updated` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
`possible_values` text NOT NULL COMMENT 'JSON array with possible enum values.'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -261,7 +262,11 @@ ALTER TABLE `actor_commands`
|
||||
--
|
||||
ALTER TABLE `actor_states`
|
||||
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 `idx_actor_states_name` (`state_name`),
|
||||
ADD KEY `state_type` (`state_type`);
|
||||
@@ -271,7 +276,11 @@ ALTER TABLE `actor_states`
|
||||
--
|
||||
ALTER TABLE `command_parameters`
|
||||
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`);
|
||||
|
||||
--
|
||||
|
||||
@@ -84,9 +84,11 @@ function wertFeld(spec, wert) {
|
||||
optionListe(feld, [{ value: "true", text: "JA" }, { value: "false", text: "NEIN" }],
|
||||
wert === "" ? "true" : wert);
|
||||
} else if (spec.input === "select") {
|
||||
// Die Auswahl kommt fertig aus dem Katalog: bei Messwerten steht der
|
||||
// Klartext auch als Wert, bei Geraeteparametern die Zahl dahinter.
|
||||
feld = document.createElement("select");
|
||||
feld.className = "form-select";
|
||||
optionListe(feld, spec.options.map(o => ({ value: o, text: o })), wert);
|
||||
optionListe(feld, spec.options.map(o => ({ value: o.value, text: o.label })), wert);
|
||||
} else {
|
||||
feld = document.createElement("input");
|
||||
feld.className = "form-control";
|
||||
@@ -104,7 +106,7 @@ function wertFeld(spec, wert) {
|
||||
/** Standardwert, wenn eine Zeile auf einen anderen Messwert umgestellt wird. */
|
||||
function standardWert(spec) {
|
||||
if (spec.input === "bool") return "true";
|
||||
if (spec.input === "select") return spec.options.length ? spec.options[0] : "";
|
||||
if (spec.input === "select") return spec.options.length ? spec.options[0].value : "";
|
||||
if (spec.input === "time") return "00:00";
|
||||
if (spec.input === "date") return new Date().toISOString().split("T")[0];
|
||||
if (spec.input === "number") return "0";
|
||||
|
||||
@@ -114,19 +114,64 @@ 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`:
|
||||
|
||||
| URL | Messwert (`actor_states.url`) | Kommando |
|
||||
|-------------|-------------------------------|-------------------------------------|
|
||||
| `mqtt://…` | vollständiges Topic, abonniert | publiziert auf das Parameter-Topic |
|
||||
| `http://…` | Feldname in der JSON-Antwort, gepollt | Parameter als Abfrageargumente |
|
||||
| `io://…` | Tahoma-Statusname, gepollt | `exec/apply` an die Tahoma-Box |
|
||||
| `Logic` | gerechnet: Uhrzeit, Datum, Sonne | – |
|
||||
| URL | Messwert (`actor_states.url`) | Kommando (`actor_commands.command_url`) |
|
||||
|---|---|---|
|
||||
| `mqtt://…` | vollständiges Topic, abonniert; bei mehreren Messwerten je Topic zusätzlich `value_path` | Nutzlast auf das Parameter-Topic |
|
||||
| `http://…` | Feldname in der JSON-Antwort, gepollt | Abfrageargumente an die Geräte-URL (`turn=on`) |
|
||||
| `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 | – |
|
||||
|
||||
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()`.
|
||||
|
||||
Bei HTTP heißen die Schaltbefehle nicht so wie in der Datenbank — dafür gibt es
|
||||
in `HTTPTransport` eine kleine Übersetzungstabelle (`turn_on` → `on=true`).
|
||||
Das ist die Stelle, die beim Anschluss neuer HTTP-Geräte wächst.
|
||||
Tahoma ist der einzige, der nicht am URL-Schema erkannt wird, sondern an der
|
||||
**Box-Kennung** in der URL. Das Schema beschreibt dort die Funkart, und
|
||||
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.
|
||||
|
||||
Mehrere Messwerte teilen sich oft **ein Topic**: der go-eCharger schickt
|
||||
sechzehn Zahlen als JSON-Feld auf `…/nrg`, und erst das `value_template` der
|
||||
Home-Assistant-Discovery sagt, dass „Strom L1" das fünfte Element ist. Diese
|
||||
Angabe steht in `actor_states.value_path` — in derselben Schreibweise, die
|
||||
auch WLED benutzt: `[4]`, `ssid`, `seg[0].col[0]`. Ohne Pfad gilt die ganze
|
||||
Nutzlast.
|
||||
|
||||
Gelesen wird nur der einfache Fall aus dem Template: ein Zugriff auf
|
||||
`value_json` und was danach an Punkten und Klammern folgt.
|
||||
|
||||
### Werttabellen
|
||||
|
||||
Manche Geräte schicken eine Zahl und meinen einen Zustand:
|
||||
|
||||
```
|
||||
{{ ['Unknown','Idle','Charging','WaitCar','Complete','Error'][value_json|int] }}
|
||||
{{ ['Default','Eco','NextTrip'][value_json|int-3] }}
|
||||
```
|
||||
|
||||
Das ist kein Pfad, sondern eine Übersetzung von Zahl nach Text. Sie landet in
|
||||
`possible_values` — in der Schreibweise, die WLED für seine Effektliste schon
|
||||
benutzt: eine Liste aus `{Wert: Bezeichnung}`. Ein Versatz im Ausdruck wandert
|
||||
dabei in die Schlüssel, aus `[value_json|int-3]` wird also `{"3":"Default"}`.
|
||||
|
||||
Der Runner übersetzt beim Lesen: aus der gesendeten `2` wird `Charging`. Eine
|
||||
Bedingung vergleicht damit genau den Klartext, den der Editor zur Auswahl
|
||||
stellt. Steht die Zahl nicht in der Tabelle, bleibt sie stehen — ein
|
||||
erfundener Name wäre schlimmer als ein roher Wert.
|
||||
|
||||
Auf beiden Seiten des Editors steckt dieselbe Tabelle, aber der gespeicherte
|
||||
Wert ist ein anderer:
|
||||
|
||||
| | angezeigt | gespeichert |
|
||||
|---|---|---|
|
||||
| **Messwert** (Bedingung) | `Charging` | `Charging` — der Runner hat schon übersetzt |
|
||||
| **Parameter** (Aktion) | `Blink` | `1` — das Gerät will die Zahl |
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ Tabellen siehe homeMesh_automations.sql, Konfiguration siehe config.ini.example.
|
||||
|
||||
import argparse
|
||||
import configparser
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
@@ -55,7 +56,7 @@ import requests
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
from transports import (HTTPTransport, LogicTransport, MQTTTransport,
|
||||
TahomaTransport)
|
||||
TahomaTransport, WLEDTransport)
|
||||
|
||||
logger = logging.getLogger("autoaction")
|
||||
|
||||
@@ -145,13 +146,25 @@ class Regelwerk:
|
||||
|
||||
states = {}
|
||||
with db.cursor() as c:
|
||||
c.execute("""SELECT s.id, s.state_name, s.url AS state_url, s.current_value,
|
||||
c.execute("""SELECT s.id, s.state_name, s.url AS state_url, s.value_path,
|
||||
s.current_value, s.possible_values,
|
||||
a.url AS actor_url, a.name AS actor_name, t.type
|
||||
FROM actor_states s
|
||||
JOIN actors a ON a.id = s.actor_id
|
||||
LEFT JOIN state_types t ON s.state_type = t.id""")
|
||||
for row in c.fetchall():
|
||||
row["type"] = row["type"] or "string"
|
||||
# Werttabelle als flaches {gesendeter Wert: Bezeichnung}. In
|
||||
# der Datenbank steht sie als Liste aus Ein-Schluessel-
|
||||
# Objekten, weil der Editor sie so schon versteht.
|
||||
row["wertetabelle"] = {}
|
||||
try:
|
||||
for eintrag in json.loads(row["possible_values"] or "[]"):
|
||||
if isinstance(eintrag, dict):
|
||||
for wert, name in eintrag.items():
|
||||
row["wertetabelle"][str(wert)] = name
|
||||
except ValueError:
|
||||
pass
|
||||
states[row["id"]] = row
|
||||
|
||||
kommandos = {}
|
||||
@@ -377,6 +390,7 @@ class Runner:
|
||||
self.mqtt = self._mqtt_verbinden()
|
||||
self.transporte = [
|
||||
MQTTTransport(self.mqtt, self.dry_run),
|
||||
WLEDTransport(requests, dry_run=self.dry_run),
|
||||
HTTPTransport(requests, dry_run=self.dry_run),
|
||||
TahomaTransport(requests, config.text("tahoma", "pin"),
|
||||
config.text("tahoma", "token"),
|
||||
@@ -415,7 +429,9 @@ class Runner:
|
||||
self.regelwerk = Regelwerk.laden(self.db)
|
||||
# Jeder Transport bekommt die Messwerte, fuer die er zustaendig ist.
|
||||
for transport in self.transporte:
|
||||
passende = [{"id": s["id"], "actor_url": s["actor_url"], "state_url": s["state_url"]}
|
||||
passende = [{"id": s["id"], "actor_url": s["actor_url"],
|
||||
"state_url": s["state_url"], "value_path": s["value_path"],
|
||||
"wertetabelle": s["wertetabelle"]}
|
||||
for s in self.regelwerk.states.values()
|
||||
if transport.passt(s["actor_url"])]
|
||||
transport.zustaende_anmelden(passende)
|
||||
|
||||
@@ -8,11 +8,14 @@ URL des Aktors in der Tabelle `actors`:
|
||||
|
||||
mqtt://... MQTT-Geraete (Home-Assistant-Discovery)
|
||||
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)
|
||||
|
||||
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.
|
||||
|
||||
Jeder Transport hat zwei Haelften:
|
||||
@@ -21,18 +24,71 @@ Jeder Transport hat zwei Haelften:
|
||||
zustaende_lesen() liefert {state_id: wert} - nur was neu ist
|
||||
senden(aktion) fuehrt ein Kommando aus
|
||||
|
||||
`states` ist eine Liste von Dicts mit actor_url, state_url und id,
|
||||
`aktion` ein Dict mit actor_url, command_url und params (Liste aus
|
||||
`states` ist eine Liste von Dicts mit actor_url, state_url, value_path und
|
||||
id, `aktion` ein Dict mit actor_url, command_url und params (Liste aus
|
||||
{url, name, wert}).
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import urllib3
|
||||
from datetime import datetime
|
||||
from urllib.parse import quote
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def wert_aus_pfad(daten, pfad):
|
||||
"""
|
||||
Einen Teilwert aus einer Nutzlast holen: "[3]", "ssid", "seg[0].col[0]".
|
||||
Ein leerer Pfad heisst: die Nutzlast selbst.
|
||||
|
||||
Gebraucht wird das an zwei Stellen. MQTT-Geraete legen mehrere Messwerte
|
||||
auf ein Topic - der go-eCharger schickt sechzehn Zahlen als JSON-Feld -,
|
||||
und WLED liefert seinen gesamten Zustand als ein Dokument.
|
||||
"""
|
||||
if not pfad:
|
||||
return daten
|
||||
for teil in pfad.split("."):
|
||||
treffer = re.match(r"^([^\[]*)((?:\[\d+\])*)$", teil)
|
||||
if not treffer:
|
||||
raise KeyError(pfad)
|
||||
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 uebersetze(wert, tabelle):
|
||||
"""
|
||||
Aus der gesendeten Zahl den Zustandsnamen machen: aus "2" wird "Charging".
|
||||
|
||||
Manche Geraete schicken einen Zahlencode und meinen einen Zustand. Welche
|
||||
Zahl welchen Namen hat, steht in possible_values - derselben Spalte, aus
|
||||
der auch der Editor seine Auswahlliste baut. Eine Bedingung vergleicht
|
||||
damit genau den Klartext, den man dort ausgewaehlt hat.
|
||||
|
||||
Steht die Zahl nicht in der Tabelle, bleibt sie stehen: ein erfundener
|
||||
Name waere schlimmer als ein roher Wert.
|
||||
"""
|
||||
if not tabelle:
|
||||
return wert
|
||||
if wert in tabelle:
|
||||
return tabelle[wert]
|
||||
try: # "2.0" und "2" meinen dieselbe Stufe
|
||||
ganz = str(int(float(wert)))
|
||||
except (TypeError, ValueError):
|
||||
return wert
|
||||
return tabelle.get(ganz, wert)
|
||||
|
||||
|
||||
class Transport:
|
||||
"""Gemeinsame Form. Wer nichts zu lesen hat, erbt die leeren Methoden."""
|
||||
@@ -68,7 +124,7 @@ class MQTTTransport(Transport):
|
||||
def __init__(self, client, dry_run=False):
|
||||
self.client = client
|
||||
self.dry_run = dry_run
|
||||
self.topics = {} # topic -> [state_id, ...]
|
||||
self.topics = {} # topic -> [(state_id, value_path, wertetabelle), ...]
|
||||
self.neu = {} # state_id -> wert
|
||||
|
||||
def zustaende_anmelden(self, states):
|
||||
@@ -76,34 +132,57 @@ class MQTTTransport(Transport):
|
||||
for s in states:
|
||||
if not s["state_url"]:
|
||||
continue
|
||||
self.topics.setdefault(s["state_url"], []).append(s["id"])
|
||||
self.topics.setdefault(s["state_url"], []).append(
|
||||
(s["id"], s.get("value_path"), s.get("wertetabelle") or {}))
|
||||
for topic in self.topics:
|
||||
self.client.subscribe(topic)
|
||||
logger.info("MQTT: %d Topics abonniert", len(self.topics))
|
||||
mehrfach = sum(1 for e in self.topics.values() if len(e) > 1)
|
||||
logger.info("MQTT: %d Topics abonniert, %d davon mit mehreren Messwerten",
|
||||
len(self.topics), mehrfach)
|
||||
|
||||
def nachricht(self, topic, payload):
|
||||
"""Wird vom Runner aus dem on_message-Rueckruf gerufen."""
|
||||
for state_id in self.topics.get(topic, []):
|
||||
self.neu[state_id] = self._wert(payload)
|
||||
|
||||
@staticmethod
|
||||
def _wert(payload):
|
||||
"""
|
||||
Rohtext, aber JSON-Skalare werden ausgepackt: manche Geraete schicken
|
||||
"21.4", andere 21.4 mit Anfuehrungszeichen, wieder andere true statt
|
||||
ON. Objekte bleiben wie sie sind - welches Feld gemeint ist, weiss die
|
||||
Datenbank nicht, und Raten waere schlimmer als Nichtstun.
|
||||
"""
|
||||
eintraege = self.topics.get(topic, [])
|
||||
if not eintraege:
|
||||
return
|
||||
text = payload.decode("utf-8", "replace").strip() if isinstance(payload, bytes) else str(payload)
|
||||
try:
|
||||
wert = json.loads(text)
|
||||
daten = json.loads(text)
|
||||
except ValueError:
|
||||
return text
|
||||
daten = None # kein JSON - dann gilt der Rohtext
|
||||
for state_id, pfad, tabelle in eintraege:
|
||||
wert = self._wert(text, daten, pfad, tabelle)
|
||||
if wert is not None:
|
||||
self.neu[state_id] = wert
|
||||
|
||||
@staticmethod
|
||||
def _wert(text, daten, pfad, tabelle=None):
|
||||
"""
|
||||
Aus der Nutzlast den Wert eines einzelnen Messwerts machen.
|
||||
|
||||
Mehrere Messwerte teilen sich oft ein Topic; welcher Teil gemeint ist,
|
||||
steht in value_path - gelesen aus dem value_template der
|
||||
Home-Assistant-Discovery. Ohne Pfad gilt die ganze Nutzlast, und
|
||||
JSON-Skalare werden ausgepackt: manche Geraete schicken 21.4 mit
|
||||
Anfuehrungszeichen, andere true statt ON.
|
||||
|
||||
Steht eine Werttabelle dabei, wird aus der gesendeten Zahl der
|
||||
Zustandsname: aus 2 wird "Charging".
|
||||
"""
|
||||
if daten is None:
|
||||
return uebersetze(text, tabelle)
|
||||
try:
|
||||
wert = wert_aus_pfad(daten, pfad)
|
||||
except (KeyError, IndexError, TypeError):
|
||||
logger.debug("Pfad %s nicht in der Nutzlast: %s", pfad, text[:80])
|
||||
return None
|
||||
if isinstance(wert, bool):
|
||||
return "true" if wert else "false"
|
||||
if isinstance(wert, (int, float, str)):
|
||||
return str(wert)
|
||||
return text
|
||||
wert = "true" if wert else "false"
|
||||
elif isinstance(wert, (int, float, str)):
|
||||
wert = str(wert)
|
||||
else:
|
||||
return json.dumps(wert, ensure_ascii=False)
|
||||
return uebersetze(wert, tabelle)
|
||||
|
||||
def zustaende_lesen(self):
|
||||
werte, self.neu = self.neu, {}
|
||||
@@ -137,20 +216,15 @@ class HTTPTransport(Transport):
|
||||
JSON-Antwort ("tC", "a_voltage"). Solche Geraete melden sich nicht von
|
||||
selbst, sie werden im Poll-Takt gefragt.
|
||||
|
||||
Beim Senden haengen die Parameter als Abfrageargumente an die actor_url.
|
||||
Die Schaltbefehle von Shelly heissen aber nicht so, wie sie in der
|
||||
Datenbank stehen - dafuer steht unten eine kleine Uebersetzungstabelle.
|
||||
Wer ein anderes HTTP-Geraet anschliesst, erweitert genau die.
|
||||
Beim Senden wird die command_url als Abfrageargument an die actor_url
|
||||
gehaengt ("turn=on") und die Parameter mit ihrem eigenen Namen dazu.
|
||||
Frueher stand hier eine Uebersetzungstabelle, weil die Shelly-Kommandos
|
||||
ohne URL in der Datenbank landeten - das ist im Discovery behoben, die
|
||||
Zuordnung gehoert dorthin und nicht in den Runner.
|
||||
"""
|
||||
|
||||
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):
|
||||
self.requests = requests_modul
|
||||
self.timeout = timeout
|
||||
@@ -181,14 +255,17 @@ class HTTPTransport(Transport):
|
||||
return werte
|
||||
|
||||
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"]:
|
||||
if p["url"]:
|
||||
argumente[p["url"]] = p["wert"]
|
||||
if not argumente and aktion["command_url"]:
|
||||
# Kein bekannter Schaltbefehl und keine Parameter: das Kommando
|
||||
# als Argument anhaengen, mehr laesst sich hier nicht ableiten.
|
||||
argumente[aktion["command_url"]] = "true"
|
||||
if not argumente:
|
||||
raise RuntimeError("Kommando ohne URL und ohne Parameter - im "
|
||||
"Geraetemodell fehlt die Angabe, was zu schicken ist")
|
||||
if self.dry_run:
|
||||
logger.info("[dry-run] HTTP %s %s", aktion["actor_url"], argumente)
|
||||
return
|
||||
@@ -197,16 +274,110 @@ class HTTPTransport(Transport):
|
||||
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}))
|
||||
|
||||
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:
|
||||
# Bei WLED ist die state_url selbst schon der Pfad. Ein
|
||||
# gesetzter value_path hat trotzdem Vorrang, falls das
|
||||
# Geraetemodell spaeter darauf umgestellt wird.
|
||||
pfad = s.get("value_path") or s["state_url"]
|
||||
try:
|
||||
werte[s["id"]] = str(wert_aus_pfad(daten, pfad))
|
||||
except (KeyError, IndexError, TypeError):
|
||||
logger.debug("WLED %s: Pfad %s nicht gefunden", actor_url, pfad)
|
||||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
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").
|
||||
Geschickt wird ueber exec/apply - genauso wie in ajax/tahoma.php, nur ohne
|
||||
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://"
|
||||
@@ -219,6 +390,9 @@ class TahomaTransport(Transport):
|
||||
self.dry_run = dry_run
|
||||
self.states = []
|
||||
|
||||
def passt(self, actor_url):
|
||||
return bool(self.pin) and ("://" + self.pin + "/") in actor_url
|
||||
|
||||
@property
|
||||
def basis(self):
|
||||
return "https://gateway-%s:8443/enduser-mobile-web/1/enduserAPI" % self.pin
|
||||
|
||||
+69
-13
@@ -100,6 +100,43 @@ function lockoutChoices()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Auswahlwerte aus possible_values in eine einheitliche Form bringen.
|
||||
*
|
||||
* In der Datenbank stehen zwei Schreibweisen. Eine schlichte Liste
|
||||
* (["open","close"]) und eine Liste aus Ein-Schluessel-Objekten
|
||||
* ([{"0":"Unknown"},{"1":"Idle"}]) - letztere ueberall dort, wo das Geraet
|
||||
* eine Zahl schickt und einen Zustand meint.
|
||||
*
|
||||
* Was davon der gespeicherte Wert ist, haengt an der Seite:
|
||||
*
|
||||
* Messwert: der Runner uebersetzt die Zahl schon beim Lesen in den
|
||||
* Klartext, eine Bedingung vergleicht also "Charging".
|
||||
* Parameter: das Geraet will die Zahl - gespeichert wird der Schluessel,
|
||||
* angezeigt der Klartext.
|
||||
*/
|
||||
function optionList($json, $klartextAlsWert)
|
||||
{
|
||||
$roh = json_decode($json, true);
|
||||
if (!is_array($roh)) {
|
||||
return [];
|
||||
}
|
||||
$liste = [];
|
||||
foreach ($roh as $eintrag) {
|
||||
if (is_array($eintrag)) {
|
||||
foreach ($eintrag as $wert => $bezeichnung) {
|
||||
$liste[] = [
|
||||
"value" => $klartextAlsWert ? strval($bezeichnung) : strval($wert),
|
||||
"label" => strval($bezeichnung),
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$liste[] = ["value" => strval($eintrag), "label" => strval($eintrag)];
|
||||
}
|
||||
}
|
||||
return $liste;
|
||||
}
|
||||
|
||||
/** Wie das Wertfeld im Editor aussieht. */
|
||||
function inputForType($type, $hasOptions)
|
||||
{
|
||||
@@ -187,10 +224,7 @@ function deviceCatalog()
|
||||
if (!isset($devices[$id])) {
|
||||
continue;
|
||||
}
|
||||
$options = json_decode($row["possible_values"], true);
|
||||
if (!is_array($options)) {
|
||||
$options = [];
|
||||
}
|
||||
$options = optionList($row["possible_values"], true);
|
||||
$type = $row["type"] ?: "string";
|
||||
$devices[$id]["states"][] = [
|
||||
"id" => intval($row["id"]),
|
||||
@@ -198,7 +232,7 @@ function deviceCatalog()
|
||||
"type" => $type,
|
||||
"input" => inputForType($type, count($options) > 0),
|
||||
"operators" => operatorChoices($type),
|
||||
"options" => array_values($options),
|
||||
"options" => $options,
|
||||
"unit" => $row["unit"],
|
||||
"value" => $row["current_value"],
|
||||
"url" => $row["url"],
|
||||
@@ -232,10 +266,7 @@ function deviceCatalog()
|
||||
if (!isset($commands[$cmdId])) {
|
||||
continue;
|
||||
}
|
||||
$options = json_decode($row["possible_values"], true);
|
||||
if (!is_array($options)) {
|
||||
$options = [];
|
||||
}
|
||||
$options = optionList($row["possible_values"], false);
|
||||
$type = $row["type"] ?: "string";
|
||||
// Das passende Kommando im Geraet suchen. Wenige Kommandos je
|
||||
// Geraet, deshalb reicht die lineare Suche.
|
||||
@@ -246,7 +277,7 @@ function deviceCatalog()
|
||||
"name" => $row["parameter_name"],
|
||||
"type" => $type,
|
||||
"input" => inputForType($type, count($options) > 0),
|
||||
"options" => array_values($options),
|
||||
"options" => $options,
|
||||
"min" => $row["min_value"] === null ? null : floatval($row["min_value"]),
|
||||
"max" => $row["max_value"] === null ? null : floatval($row["max_value"]),
|
||||
"url" => $row["url"],
|
||||
@@ -564,6 +595,7 @@ function listAutomations($floor)
|
||||
|
||||
$namesState = stateNames();
|
||||
$namesCommand = commandNames();
|
||||
$labelsParam = parameterLabels();
|
||||
|
||||
$list = [];
|
||||
foreach ($ids as $id) {
|
||||
@@ -572,7 +604,7 @@ function listAutomations($floor)
|
||||
continue;
|
||||
}
|
||||
$auto["conditionText"] = describeConditions($auto["conditions"], $namesState);
|
||||
$auto["actionText"] = describeActions($auto["actions"], $namesCommand);
|
||||
$auto["actionText"] = describeActions($auto["actions"], $namesCommand, $labelsParam);
|
||||
$list[] = $auto;
|
||||
}
|
||||
return $list;
|
||||
@@ -597,6 +629,25 @@ function stateNames()
|
||||
return $names;
|
||||
}
|
||||
|
||||
/**
|
||||
* id => [gespeicherter Wert => Bezeichnung] fuer alle Kommando-Parameter,
|
||||
* die eine Auswahl haben. Gespeichert wird bei Parametern die Zahl, die das
|
||||
* Geraet erwartet - in der Uebersicht soll aber "Blink" stehen und nicht "1".
|
||||
*/
|
||||
function parameterLabels()
|
||||
{
|
||||
$db = meshDb();
|
||||
$res = $db->query("SELECT id, possible_values FROM command_parameters
|
||||
WHERE possible_values <> '' AND possible_values <> '[]'");
|
||||
$labels = [];
|
||||
while ($row = $res->fetch_assoc()) {
|
||||
foreach (optionList($row["possible_values"], false) as $option) {
|
||||
$labels[intval($row["id"])][$option["value"]] = $option["label"];
|
||||
}
|
||||
}
|
||||
return $labels;
|
||||
}
|
||||
|
||||
/** id => "Geraet: Kommando" fuer alle Kommandos. */
|
||||
function commandNames()
|
||||
{
|
||||
@@ -642,12 +693,17 @@ function describeConditions($conditions, $names)
|
||||
return implode(" oder ", $parts);
|
||||
}
|
||||
|
||||
function describeActions($actions, $names)
|
||||
function describeActions($actions, $names, $labels = [])
|
||||
{
|
||||
$parts = [];
|
||||
foreach ($actions as $a) {
|
||||
$text = $names[$a["command_id"]] ?? ("Kommando " . $a["command_id"]);
|
||||
$values = array_values($a["params"]);
|
||||
$values = [];
|
||||
foreach ($a["params"] as $parameterId => $wert) {
|
||||
// Wo es eine Auswahl gibt, die Bezeichnung nennen: "Blink" sagt
|
||||
// mehr als die 1, die das Geraet tatsaechlich bekommt.
|
||||
$values[] = $labels[intval($parameterId)][$wert] ?? $wert;
|
||||
}
|
||||
if ($values) {
|
||||
$text .= " (" . implode(", ", $values) . ")";
|
||||
}
|
||||
|
||||
@@ -365,13 +365,13 @@ class DatabaseManager:
|
||||
INSERT INTO actor_commands (actor_id, command_name, command_url)
|
||||
VALUES (%s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
command_name = VALUES(command_name)
|
||||
command_url = VALUES(command_url)
|
||||
"""
|
||||
cursor.execute(cmd_query, (actor_id, command_name, command_url))
|
||||
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]
|
||||
|
||||
# Parameter einfügen
|
||||
@@ -381,14 +381,14 @@ class DatabaseManager:
|
||||
(command_id, parameter_name, parameter_type, min_value, max_value, possible_values, url)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
parameter_name = VALUES(parameter_name),
|
||||
parameter_type = VALUES(parameter_type),
|
||||
min_value = VALUES(min_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_type = param.get('type', '')
|
||||
param_type = self.PARAMETER_TYPES_MAPPING.get(param.get('type', ''), 4)
|
||||
min_val = param.get('min')
|
||||
max_val = param.get('max')
|
||||
possible_vals = json.dumps(param.get('values')) if 'values' in param else ""
|
||||
@@ -401,22 +401,24 @@ class DatabaseManager:
|
||||
for state in states:
|
||||
state_query = """
|
||||
INSERT INTO actor_states
|
||||
(actor_id, state_name, state_type, current_value, unit, url, possible_values)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||
(actor_id, state_name, state_type, current_value, unit, url, possible_values, value_path)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
state_name = VALUES(state_name),
|
||||
state_type = VALUES(state_type),
|
||||
current_value = VALUES(current_value),
|
||||
possible_values = VALUES(possible_values),
|
||||
unit = VALUES(unit)
|
||||
unit = VALUES(unit),
|
||||
url = VALUES(url),
|
||||
value_path = VALUES(value_path)
|
||||
"""
|
||||
state_name = state.get('name', '')
|
||||
state_type = self.PARAMETER_TYPES_MAPPING.get(state.get('type', 0), 4)
|
||||
current_value = str(state.get('current_value', '')) if 'current_value' in state else None
|
||||
unit = state.get('unit')
|
||||
state_url = state.get('url')
|
||||
value_path = state.get('value_path')
|
||||
possible_vals = json.dumps(state.get('values')) if 'values' in state else ""
|
||||
cursor.execute(state_query, (actor_id, state_name, state_type, current_value, unit, state_url, possible_vals))
|
||||
cursor.execute(state_query, (actor_id, state_name, state_type, current_value, unit, state_url, possible_vals, value_path))
|
||||
|
||||
self.connection.commit()
|
||||
cursor.close()
|
||||
@@ -471,21 +473,25 @@ class DatabaseManager:
|
||||
for state in states:
|
||||
state_query = """
|
||||
INSERT INTO actor_states
|
||||
(actor_id, state_name, state_type, current_value, unit, url)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
(actor_id, state_name, state_type, current_value, unit, url, value_path, possible_values)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
state_name = VALUES(state_name),
|
||||
state_type = VALUES(state_type),
|
||||
current_value = VALUES(current_value),
|
||||
unit = VALUES(unit)
|
||||
unit = VALUES(unit),
|
||||
url = VALUES(url),
|
||||
value_path = VALUES(value_path),
|
||||
possible_values = VALUES(possible_values)
|
||||
"""
|
||||
state_name = state.get('name', '')
|
||||
state_type = self.PARAMETER_TYPES_MAPPING.get(state.get('type', 0), 4)
|
||||
current_value = str(state.get('current_value', '')) if 'current_value' in state else None
|
||||
unit = state.get('unit')
|
||||
state_url = state.get('url')
|
||||
value_path = state.get('value_path')
|
||||
possible_vals = json.dumps(state.get('values')) if 'values' in state else ""
|
||||
|
||||
cursor.execute(state_query, (sensor_id, state_name, state_type, current_value, unit, state_url))
|
||||
cursor.execute(state_query, (sensor_id, state_name, state_type, current_value, unit, state_url, value_path, possible_vals))
|
||||
|
||||
self.connection.commit()
|
||||
cursor.close()
|
||||
|
||||
@@ -6,6 +6,7 @@ Erweitert das Tahoma Script um MQTT-Geräte via Home Assistant Discovery
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from typing import Dict, List, Optional
|
||||
import logging
|
||||
@@ -506,6 +507,82 @@ class MQTTDeviceConverter:
|
||||
|
||||
return command_entry
|
||||
|
||||
@staticmethod
|
||||
def _value_path(config: Dict) -> Optional[str]:
|
||||
"""
|
||||
Liest aus dem value_template heraus, welcher Teil der Nutzlast gemeint
|
||||
ist.
|
||||
|
||||
Mehrere Messwerte teilen sich oft ein Topic: der go-eCharger schickt
|
||||
sechzehn Zahlen als JSON-Feld auf "nrg", und erst das value_template
|
||||
sagt, dass "Strom L1" das vierte Element ist. Ohne diese Angabe
|
||||
bekaemen alle sechzehn denselben Rohtext.
|
||||
|
||||
Zurueck kommt ein Pfad in derselben Schreibweise, die auch WLED
|
||||
benutzt: "[3]", "ssid", "foo[0].bar". None heisst: die ganze Nutzlast.
|
||||
|
||||
Home Assistant erlaubt in value_template beliebiges Jinja. Hier wird
|
||||
nur der einfache Fall gelesen - ein Zugriff auf value_json und was
|
||||
danach an Punkten und Klammern folgt. Bei allem anderen (Werttabellen
|
||||
wie ['Idle','Charging'][value_json|int], Bedingungen) bleibt es beim
|
||||
Rohtext, also beim bisherigen Verhalten.
|
||||
"""
|
||||
vorlage = config.get('value_template') or config.get('val_tpl')
|
||||
if not vorlage:
|
||||
return None
|
||||
treffer = re.search(
|
||||
r"value_json((?:\s*(?:\.\w+|\[\s*'[^']*'\s*\]|\[\s*\"[^\"]*\"\s*\]|\[\s*\d+\s*\]))*)",
|
||||
vorlage)
|
||||
if not treffer or not treffer.group(1).strip():
|
||||
return None
|
||||
teile = []
|
||||
for stueck in re.finditer(
|
||||
r"\.(\w+)|\[\s*'([^']*)'\s*\]|\[\s*\"([^\"]*)\"\s*\]|\[\s*(\d+)\s*\]",
|
||||
treffer.group(1)):
|
||||
name = stueck.group(1) or stueck.group(2) or stueck.group(3)
|
||||
teile.append(name if name else "[%s]" % stueck.group(4))
|
||||
if not teile:
|
||||
return None
|
||||
# "[3]" haengt sich ohne Punkt an, Namen mit Punkt.
|
||||
pfad = ""
|
||||
for teil in teile:
|
||||
pfad += teil if teil.startswith("[") else (("." + teil) if pfad else teil)
|
||||
return pfad[:120]
|
||||
|
||||
@staticmethod
|
||||
def _value_map(config: Dict) -> Optional[List[Dict]]:
|
||||
"""
|
||||
Liest eine Werttabelle aus dem value_template.
|
||||
|
||||
Manche Geraete schicken eine Zahl und meinen einen Zustand:
|
||||
|
||||
{{ ['Unknown','Idle','Charging','WaitCar','Complete','Error'][value_json|int ]}}
|
||||
{{ ['Default','Eco','NextTrip'][value_json|int-3]}}
|
||||
|
||||
Das ist kein Pfad in die Nutzlast, sondern eine Uebersetzung von Zahl
|
||||
nach Text. Zurueck kommt sie in der Schreibweise, die WLED fuer seine
|
||||
Effektliste schon benutzt - eine Liste aus {Wert: Bezeichnung} -,
|
||||
denn die versteht der Editor bereits und macht eine Auswahlliste
|
||||
daraus.
|
||||
|
||||
Der Versatz im Ausdruck wandert dabei in die Schluessel: bei
|
||||
"value_json|int-3" gehoert der erste Eintrag zur gesendeten 3.
|
||||
"""
|
||||
vorlage = config.get('value_template') or config.get('val_tpl')
|
||||
if not vorlage:
|
||||
return None
|
||||
treffer = re.search(
|
||||
r"\[\s*((?:'[^']*'|\"[^\"]*\")(?:\s*,\s*(?:'[^']*'|\"[^\"]*\"))*)\s*\]"
|
||||
r"\s*\[\s*value_json\s*\|\s*int\s*([+-]\s*\d+)?\s*\]",
|
||||
vorlage)
|
||||
if not treffer:
|
||||
return None
|
||||
bezeichnungen = [t[1:-1] for t in
|
||||
re.findall(r"'[^']*'|\"[^\"]*\"", treffer.group(1))]
|
||||
versatz = int(treffer.group(2).replace(' ', '')) if treffer.group(2) else 0
|
||||
# index = wert + versatz -> wert = index - versatz
|
||||
return [{str(i - versatz): name} for i, name in enumerate(bezeichnungen)]
|
||||
|
||||
@staticmethod
|
||||
def _entity_to_states(component: str, object_id: str, config: Dict) -> List[Dict]:
|
||||
"""
|
||||
@@ -539,8 +616,12 @@ class MQTTDeviceConverter:
|
||||
state_entry = {
|
||||
'name': object_id,
|
||||
'type': 'string',
|
||||
'url': state_topic
|
||||
'url': state_topic,
|
||||
'value_path': MQTTDeviceConverter._value_path(config)
|
||||
}
|
||||
wertetabelle = MQTTDeviceConverter._value_map(config)
|
||||
if wertetabelle:
|
||||
state_entry['values'] = wertetabelle
|
||||
|
||||
# Unit hinzufügen - verschiedene mögliche Feldnamen
|
||||
unit = (
|
||||
@@ -567,6 +648,11 @@ class MQTTDeviceConverter:
|
||||
# Bei Sensor den Typ aus value_template ableiten oder number annehmen
|
||||
state_entry['type'] = 'number' # Default für Sensoren
|
||||
|
||||
# Eine Werttabelle schlaegt den geratenen Typ: was beim Geraet als
|
||||
# Zahl ankommt, ist fuer den Benutzer ein Zustandsname.
|
||||
if 'values' in state_entry:
|
||||
state_entry['type'] = 'string'
|
||||
|
||||
states.append(state_entry)
|
||||
|
||||
# Zusätzliche State Topics (z.B. brightness bei Light)
|
||||
|
||||
@@ -267,14 +267,15 @@ class ShellyModule(BaseModule):
|
||||
'name': f"{device_name}_Switch_{i}",
|
||||
'url': f"http://{ip}/rpc/Switch.Set?id={i}",
|
||||
'commands': [
|
||||
{'command': 'turn_on', 'parameters': []},
|
||||
{'command': 'turn_off', 'parameters': []},
|
||||
{'command': 'turn_on', 'url': 'on=true', 'parameters': []},
|
||||
{'command': 'turn_off', 'url': 'on=false', 'parameters': []},
|
||||
{'command': 'toggle', 'parameters': []}
|
||||
],
|
||||
'states': [
|
||||
{
|
||||
'name': 'output',
|
||||
'type': 'boolean',
|
||||
'url': 'output',
|
||||
'current_value': switch_data.get('output', False)
|
||||
}
|
||||
]
|
||||
@@ -444,14 +445,15 @@ class ShellyModule(BaseModule):
|
||||
'name': f"{device_name}_Relay_{i}",
|
||||
'url': f"http://{ip}/relay/{i}",
|
||||
'commands': [
|
||||
{'command': 'turn_on', 'parameters': []},
|
||||
{'command': 'turn_off', 'parameters': []},
|
||||
{'command': 'toggle', 'parameters': []}
|
||||
{'command': 'turn_on', 'url': 'turn=on', 'parameters': []},
|
||||
{'command': 'turn_off', 'url': 'turn=off', 'parameters': []},
|
||||
{'command': 'toggle', 'url': 'turn=toggle', 'parameters': []}
|
||||
],
|
||||
'states': [
|
||||
{
|
||||
'name': 'ison',
|
||||
'type': 'boolean',
|
||||
'url': 'ison',
|
||||
'current_value': relay.get('ison', False)
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user