Zahlencodes als Klartext
Manche Geraete schicken eine Zahl und meinen einen Zustand - der
go-eCharger etwa 2 fuer "Charging". Welche Zahl welchen Namen hat, steht im
value_template der Home-Assistant-Discovery als Werttabelle:
{{ ['Unknown','Idle','Charging','WaitCar','Complete','Error'][value_json|int] }}
Das ist kein Pfad in die Nutzlast, sondern eine Uebersetzung, und sie wurde
bisher verworfen. Jetzt landet sie in possible_values - in derselben
Schreibweise, die WLED fuer seine Effektliste schon benutzt, naemlich einer
Liste aus {Wert: Bezeichnung}. Damit versteht der Editor sie ohne
Zusatzarbeit und macht eine Auswahlliste daraus. Ein Versatz im Ausdruck
wandert in die Schluessel: aus [value_json|int-3] wird {"3":"Default"}.
Der Runner uebersetzt beim Lesen, eine Bedingung vergleicht also den
Klartext. Steht die Zahl nicht in der Tabelle, bleibt sie stehen - ein
erfundener Name waere schlimmer als ein roher Wert.
Dabei ist ein Folgefehler aufgefallen: Auswahlwerte in dieser Schreibweise
kamen im Editor als "[object Object]" an. Die alte addOptions kannte die
Form, der neue Modell-Aufbau nicht - aufgefallen ist es nie, weil WLED beim
Umbau abgeschaltet war. Der Katalog liefert Auswahlwerte jetzt einheitlich
als {value, label}, und zwar seitenrichtig:
Messwert angezeigt Charging, gespeichert Charging
(der Runner hat schon uebersetzt)
Parameter angezeigt Blink, gespeichert 1
(das Geraet will die Zahl)
Die Uebersicht loest die Bezeichnung ebenfalls auf: "Effekt (Blink)" statt
"Effekt (1)".
Nachgemessen: alle fuenf Werttabellen auf dem Broker richtig erkannt,
einschliesslich des Versatzes, kein Fehltreffer unter den 35 uebrigen
Vorlagen. Live liefert der go-eCharger jetzt Idle, Neutral, Auto, Eco und
None statt 1, 0, 0, 4, 0. Ein weiterer Discovery-Lauf aendert keine Zeile.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -84,9 +84,11 @@ function wertFeld(spec, wert) {
|
|||||||
optionListe(feld, [{ value: "true", text: "JA" }, { value: "false", text: "NEIN" }],
|
optionListe(feld, [{ value: "true", text: "JA" }, { value: "false", text: "NEIN" }],
|
||||||
wert === "" ? "true" : wert);
|
wert === "" ? "true" : wert);
|
||||||
} else if (spec.input === "select") {
|
} 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 = document.createElement("select");
|
||||||
feld.className = "form-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 {
|
} else {
|
||||||
feld = document.createElement("input");
|
feld = document.createElement("input");
|
||||||
feld.className = "form-control";
|
feld.className = "form-control";
|
||||||
@@ -104,7 +106,7 @@ function wertFeld(spec, wert) {
|
|||||||
/** Standardwert, wenn eine Zeile auf einen anderen Messwert umgestellt wird. */
|
/** Standardwert, wenn eine Zeile auf einen anderen Messwert umgestellt wird. */
|
||||||
function standardWert(spec) {
|
function standardWert(spec) {
|
||||||
if (spec.input === "bool") return "true";
|
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 === "time") return "00:00";
|
||||||
if (spec.input === "date") return new Date().toISOString().split("T")[0];
|
if (spec.input === "date") return new Date().toISOString().split("T")[0];
|
||||||
if (spec.input === "number") return "0";
|
if (spec.input === "number") return "0";
|
||||||
|
|||||||
@@ -140,11 +140,34 @@ auch WLED benutzt: `[4]`, `ssid`, `seg[0].col[0]`. Ohne Pfad gilt die ganze
|
|||||||
Nutzlast.
|
Nutzlast.
|
||||||
|
|
||||||
Gelesen wird nur der einfache Fall aus dem Template: ein Zugriff auf
|
Gelesen wird nur der einfache Fall aus dem Template: ein Zugriff auf
|
||||||
`value_json` und was danach an Punkten und Klammern folgt. Werttabellen wie
|
`value_json` und was danach an Punkten und Klammern folgt.
|
||||||
`{{ ['Idle','Charging'][value_json|int] }}` sind keine Pfade, sondern eine
|
|
||||||
Übersetzung von Zahl nach Text — dort bleibt es beim Rohwert. Das betrifft
|
### Werttabellen
|
||||||
hier acht Messwerte (Ladezustand, Fehlercode und Ähnliches am go-eCharger);
|
|
||||||
sie sind als Zahl vergleichbar, nur nicht als Klartext.
|
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%]]}]}`
|
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
|
wird mit den Parameterwerten gefüllt und am Stück geschickt. Deshalb haben die
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ Tabellen siehe homeMesh_automations.sql, Konfiguration siehe config.ini.example.
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import configparser
|
import configparser
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@@ -146,13 +147,24 @@ class Regelwerk:
|
|||||||
states = {}
|
states = {}
|
||||||
with db.cursor() as c:
|
with db.cursor() as c:
|
||||||
c.execute("""SELECT s.id, s.state_name, s.url AS state_url, s.value_path,
|
c.execute("""SELECT s.id, s.state_name, s.url AS state_url, s.value_path,
|
||||||
s.current_value,
|
s.current_value, s.possible_values,
|
||||||
a.url AS actor_url, a.name AS actor_name, t.type
|
a.url AS actor_url, a.name AS actor_name, t.type
|
||||||
FROM actor_states s
|
FROM actor_states s
|
||||||
JOIN actors a ON a.id = s.actor_id
|
JOIN actors a ON a.id = s.actor_id
|
||||||
LEFT JOIN state_types t ON s.state_type = t.id""")
|
LEFT JOIN state_types t ON s.state_type = t.id""")
|
||||||
for row in c.fetchall():
|
for row in c.fetchall():
|
||||||
row["type"] = row["type"] or "string"
|
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
|
states[row["id"]] = row
|
||||||
|
|
||||||
kommandos = {}
|
kommandos = {}
|
||||||
@@ -418,7 +430,8 @@ class Runner:
|
|||||||
# Jeder Transport bekommt die Messwerte, fuer die er zustaendig ist.
|
# Jeder Transport bekommt die Messwerte, fuer die er zustaendig ist.
|
||||||
for transport in self.transporte:
|
for transport in self.transporte:
|
||||||
passende = [{"id": s["id"], "actor_url": s["actor_url"],
|
passende = [{"id": s["id"], "actor_url": s["actor_url"],
|
||||||
"state_url": s["state_url"], "value_path": s["value_path"]}
|
"state_url": s["state_url"], "value_path": s["value_path"],
|
||||||
|
"wertetabelle": s["wertetabelle"]}
|
||||||
for s in self.regelwerk.states.values()
|
for s in self.regelwerk.states.values()
|
||||||
if transport.passt(s["actor_url"])]
|
if transport.passt(s["actor_url"])]
|
||||||
transport.zustaende_anmelden(passende)
|
transport.zustaende_anmelden(passende)
|
||||||
|
|||||||
@@ -67,6 +67,29 @@ def wert_aus_pfad(daten, pfad):
|
|||||||
return daten
|
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:
|
class Transport:
|
||||||
"""Gemeinsame Form. Wer nichts zu lesen hat, erbt die leeren Methoden."""
|
"""Gemeinsame Form. Wer nichts zu lesen hat, erbt die leeren Methoden."""
|
||||||
|
|
||||||
@@ -101,7 +124,7 @@ class MQTTTransport(Transport):
|
|||||||
def __init__(self, client, dry_run=False):
|
def __init__(self, client, dry_run=False):
|
||||||
self.client = client
|
self.client = client
|
||||||
self.dry_run = dry_run
|
self.dry_run = dry_run
|
||||||
self.topics = {} # topic -> [(state_id, value_path), ...]
|
self.topics = {} # topic -> [(state_id, value_path, wertetabelle), ...]
|
||||||
self.neu = {} # state_id -> wert
|
self.neu = {} # state_id -> wert
|
||||||
|
|
||||||
def zustaende_anmelden(self, states):
|
def zustaende_anmelden(self, states):
|
||||||
@@ -109,7 +132,8 @@ class MQTTTransport(Transport):
|
|||||||
for s in states:
|
for s in states:
|
||||||
if not s["state_url"]:
|
if not s["state_url"]:
|
||||||
continue
|
continue
|
||||||
self.topics.setdefault(s["state_url"], []).append((s["id"], s.get("value_path")))
|
self.topics.setdefault(s["state_url"], []).append(
|
||||||
|
(s["id"], s.get("value_path"), s.get("wertetabelle") or {}))
|
||||||
for topic in self.topics:
|
for topic in self.topics:
|
||||||
self.client.subscribe(topic)
|
self.client.subscribe(topic)
|
||||||
mehrfach = sum(1 for e in self.topics.values() if len(e) > 1)
|
mehrfach = sum(1 for e in self.topics.values() if len(e) > 1)
|
||||||
@@ -126,13 +150,13 @@ class MQTTTransport(Transport):
|
|||||||
daten = json.loads(text)
|
daten = json.loads(text)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
daten = None # kein JSON - dann gilt der Rohtext
|
daten = None # kein JSON - dann gilt der Rohtext
|
||||||
for state_id, pfad in eintraege:
|
for state_id, pfad, tabelle in eintraege:
|
||||||
wert = self._wert(text, daten, pfad)
|
wert = self._wert(text, daten, pfad, tabelle)
|
||||||
if wert is not None:
|
if wert is not None:
|
||||||
self.neu[state_id] = wert
|
self.neu[state_id] = wert
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _wert(text, daten, pfad):
|
def _wert(text, daten, pfad, tabelle=None):
|
||||||
"""
|
"""
|
||||||
Aus der Nutzlast den Wert eines einzelnen Messwerts machen.
|
Aus der Nutzlast den Wert eines einzelnen Messwerts machen.
|
||||||
|
|
||||||
@@ -141,19 +165,24 @@ class MQTTTransport(Transport):
|
|||||||
Home-Assistant-Discovery. Ohne Pfad gilt die ganze Nutzlast, und
|
Home-Assistant-Discovery. Ohne Pfad gilt die ganze Nutzlast, und
|
||||||
JSON-Skalare werden ausgepackt: manche Geraete schicken 21.4 mit
|
JSON-Skalare werden ausgepackt: manche Geraete schicken 21.4 mit
|
||||||
Anfuehrungszeichen, andere true statt ON.
|
Anfuehrungszeichen, andere true statt ON.
|
||||||
|
|
||||||
|
Steht eine Werttabelle dabei, wird aus der gesendeten Zahl der
|
||||||
|
Zustandsname: aus 2 wird "Charging".
|
||||||
"""
|
"""
|
||||||
if daten is None:
|
if daten is None:
|
||||||
return text
|
return uebersetze(text, tabelle)
|
||||||
try:
|
try:
|
||||||
wert = wert_aus_pfad(daten, pfad)
|
wert = wert_aus_pfad(daten, pfad)
|
||||||
except (KeyError, IndexError, TypeError):
|
except (KeyError, IndexError, TypeError):
|
||||||
logger.debug("Pfad %s nicht in der Nutzlast: %s", pfad, text[:80])
|
logger.debug("Pfad %s nicht in der Nutzlast: %s", pfad, text[:80])
|
||||||
return None
|
return None
|
||||||
if isinstance(wert, bool):
|
if isinstance(wert, bool):
|
||||||
return "true" if wert else "false"
|
wert = "true" if wert else "false"
|
||||||
if isinstance(wert, (int, float, str)):
|
elif isinstance(wert, (int, float, str)):
|
||||||
return str(wert)
|
wert = str(wert)
|
||||||
return json.dumps(wert, ensure_ascii=False)
|
else:
|
||||||
|
return json.dumps(wert, ensure_ascii=False)
|
||||||
|
return uebersetze(wert, tabelle)
|
||||||
|
|
||||||
def zustaende_lesen(self):
|
def zustaende_lesen(self):
|
||||||
werte, self.neu = self.neu, {}
|
werte, self.neu = self.neu, {}
|
||||||
|
|||||||
+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. */
|
/** Wie das Wertfeld im Editor aussieht. */
|
||||||
function inputForType($type, $hasOptions)
|
function inputForType($type, $hasOptions)
|
||||||
{
|
{
|
||||||
@@ -187,10 +224,7 @@ function deviceCatalog()
|
|||||||
if (!isset($devices[$id])) {
|
if (!isset($devices[$id])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$options = json_decode($row["possible_values"], true);
|
$options = optionList($row["possible_values"], true);
|
||||||
if (!is_array($options)) {
|
|
||||||
$options = [];
|
|
||||||
}
|
|
||||||
$type = $row["type"] ?: "string";
|
$type = $row["type"] ?: "string";
|
||||||
$devices[$id]["states"][] = [
|
$devices[$id]["states"][] = [
|
||||||
"id" => intval($row["id"]),
|
"id" => intval($row["id"]),
|
||||||
@@ -198,7 +232,7 @@ function deviceCatalog()
|
|||||||
"type" => $type,
|
"type" => $type,
|
||||||
"input" => inputForType($type, count($options) > 0),
|
"input" => inputForType($type, count($options) > 0),
|
||||||
"operators" => operatorChoices($type),
|
"operators" => operatorChoices($type),
|
||||||
"options" => array_values($options),
|
"options" => $options,
|
||||||
"unit" => $row["unit"],
|
"unit" => $row["unit"],
|
||||||
"value" => $row["current_value"],
|
"value" => $row["current_value"],
|
||||||
"url" => $row["url"],
|
"url" => $row["url"],
|
||||||
@@ -232,10 +266,7 @@ function deviceCatalog()
|
|||||||
if (!isset($commands[$cmdId])) {
|
if (!isset($commands[$cmdId])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$options = json_decode($row["possible_values"], true);
|
$options = optionList($row["possible_values"], false);
|
||||||
if (!is_array($options)) {
|
|
||||||
$options = [];
|
|
||||||
}
|
|
||||||
$type = $row["type"] ?: "string";
|
$type = $row["type"] ?: "string";
|
||||||
// Das passende Kommando im Geraet suchen. Wenige Kommandos je
|
// Das passende Kommando im Geraet suchen. Wenige Kommandos je
|
||||||
// Geraet, deshalb reicht die lineare Suche.
|
// Geraet, deshalb reicht die lineare Suche.
|
||||||
@@ -246,7 +277,7 @@ function deviceCatalog()
|
|||||||
"name" => $row["parameter_name"],
|
"name" => $row["parameter_name"],
|
||||||
"type" => $type,
|
"type" => $type,
|
||||||
"input" => inputForType($type, count($options) > 0),
|
"input" => inputForType($type, count($options) > 0),
|
||||||
"options" => array_values($options),
|
"options" => $options,
|
||||||
"min" => $row["min_value"] === null ? null : floatval($row["min_value"]),
|
"min" => $row["min_value"] === null ? null : floatval($row["min_value"]),
|
||||||
"max" => $row["max_value"] === null ? null : floatval($row["max_value"]),
|
"max" => $row["max_value"] === null ? null : floatval($row["max_value"]),
|
||||||
"url" => $row["url"],
|
"url" => $row["url"],
|
||||||
@@ -564,6 +595,7 @@ function listAutomations($floor)
|
|||||||
|
|
||||||
$namesState = stateNames();
|
$namesState = stateNames();
|
||||||
$namesCommand = commandNames();
|
$namesCommand = commandNames();
|
||||||
|
$labelsParam = parameterLabels();
|
||||||
|
|
||||||
$list = [];
|
$list = [];
|
||||||
foreach ($ids as $id) {
|
foreach ($ids as $id) {
|
||||||
@@ -572,7 +604,7 @@ function listAutomations($floor)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$auto["conditionText"] = describeConditions($auto["conditions"], $namesState);
|
$auto["conditionText"] = describeConditions($auto["conditions"], $namesState);
|
||||||
$auto["actionText"] = describeActions($auto["actions"], $namesCommand);
|
$auto["actionText"] = describeActions($auto["actions"], $namesCommand, $labelsParam);
|
||||||
$list[] = $auto;
|
$list[] = $auto;
|
||||||
}
|
}
|
||||||
return $list;
|
return $list;
|
||||||
@@ -597,6 +629,25 @@ function stateNames()
|
|||||||
return $names;
|
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. */
|
/** id => "Geraet: Kommando" fuer alle Kommandos. */
|
||||||
function commandNames()
|
function commandNames()
|
||||||
{
|
{
|
||||||
@@ -642,12 +693,17 @@ function describeConditions($conditions, $names)
|
|||||||
return implode(" oder ", $parts);
|
return implode(" oder ", $parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
function describeActions($actions, $names)
|
function describeActions($actions, $names, $labels = [])
|
||||||
{
|
{
|
||||||
$parts = [];
|
$parts = [];
|
||||||
foreach ($actions as $a) {
|
foreach ($actions as $a) {
|
||||||
$text = $names[$a["command_id"]] ?? ("Kommando " . $a["command_id"]);
|
$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) {
|
if ($values) {
|
||||||
$text .= " (" . implode(", ", $values) . ")";
|
$text .= " (" . implode(", ", $values) . ")";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -473,14 +473,15 @@ class DatabaseManager:
|
|||||||
for state in states:
|
for state in states:
|
||||||
state_query = """
|
state_query = """
|
||||||
INSERT INTO actor_states
|
INSERT INTO actor_states
|
||||||
(actor_id, state_name, state_type, current_value, unit, url, value_path)
|
(actor_id, state_name, state_type, current_value, unit, url, value_path, possible_values)
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
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),
|
url = VALUES(url),
|
||||||
value_path = VALUES(value_path)
|
value_path = VALUES(value_path),
|
||||||
|
possible_values = VALUES(possible_values)
|
||||||
"""
|
"""
|
||||||
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)
|
||||||
@@ -488,8 +489,9 @@ class DatabaseManager:
|
|||||||
unit = state.get('unit')
|
unit = state.get('unit')
|
||||||
state_url = state.get('url')
|
state_url = state.get('url')
|
||||||
value_path = state.get('value_path')
|
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, value_path))
|
cursor.execute(state_query, (sensor_id, state_name, state_type, current_value, unit, state_url, value_path, possible_vals))
|
||||||
|
|
||||||
self.connection.commit()
|
self.connection.commit()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
|||||||
@@ -549,6 +549,40 @@ class MQTTDeviceConverter:
|
|||||||
pfad += teil if teil.startswith("[") else (("." + teil) if pfad else teil)
|
pfad += teil if teil.startswith("[") else (("." + teil) if pfad else teil)
|
||||||
return pfad[:120]
|
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
|
@staticmethod
|
||||||
def _entity_to_states(component: str, object_id: str, config: Dict) -> List[Dict]:
|
def _entity_to_states(component: str, object_id: str, config: Dict) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
@@ -585,6 +619,9 @@ class MQTTDeviceConverter:
|
|||||||
'url': state_topic,
|
'url': state_topic,
|
||||||
'value_path': MQTTDeviceConverter._value_path(config)
|
'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 hinzufügen - verschiedene mögliche Feldnamen
|
||||||
unit = (
|
unit = (
|
||||||
@@ -610,6 +647,11 @@ class MQTTDeviceConverter:
|
|||||||
elif component == 'sensor':
|
elif component == 'sensor':
|
||||||
# Bei Sensor den Typ aus value_template ableiten oder number annehmen
|
# Bei Sensor den Typ aus value_template ableiten oder number annehmen
|
||||||
state_entry['type'] = 'number' # Default für Sensoren
|
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)
|
states.append(state_entry)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user