Shelly MQTT Modul implementieren
Tests / test (push) Successful in 42s

This commit is contained in:
dh
2026-09-17 09:46:07 +00:00
parent b7c4b16987
commit 4cbec75d56
9 changed files with 726 additions and 24 deletions
+5 -5
View File
@@ -4,18 +4,18 @@ Unabhaengige Zusatzmodule fuer IP-Symcon. Dieses Repository ist nicht vom Enelix
## Status ## Status
Das Repository befindet sich im Aufbau. Der Verbrauchskostenreport ist als Das Repository befindet sich im Aufbau. Verbrauchskostenreport und Shelly
installierbares IP-Symcon-Modul enthalten; die weiteren Module sind derzeit Modul sind als installierbare IP-Symcon-Module enthalten; die weiteren Module
als Diskussionsentwürfe dokumentiert. sind derzeit als Diskussionsentwürfe dokumentiert.
## Geplante Module ## Module
- Verbrauchskostenreport (implementiert) - Verbrauchskostenreport (implementiert)
- Virtuelle Batterie - Virtuelle Batterie
- CC100 Hardware - CC100 Hardware
- Energiediagramm - Energiediagramm
- VGT-Schnittstelle - VGT-Schnittstelle
- Shelly Modul - Shelly Modul (implementiert)
Die vollständigen Tabellen mit Properties, Variablen, Verhalten und offenen Die vollständigen Tabellen mit Properties, Variablen, Verhalten und offenen
Punkten stehen in der [Modulübersicht](docs/module/README.md). Punkten stehen in der [Modulübersicht](docs/module/README.md).
+107
View File
@@ -0,0 +1,107 @@
# Shelly Modul
Das Modul bindet Shelly-Geräte ab Generation 2 über deren MQTT/RPC-Protokoll
in IP-Symcon ein. Es erkennt Geräte und Datenpunkte aus Statusmeldungen
automatisch und benötigt keine Abhängigkeit zum Enelix EMS.
## Voraussetzungen
- IP-Symcon ab Version 8.0
- ein eingerichteter IP-Symcon MQTT Server oder MQTT Client
- ein Shelly Gen2+-Gerät mit aktivierter MQTT-Verbindung
- MQTT-RPC-Ereignisse auf `<Topic>/events/rpc` und der Online-Status auf
`<Topic>/online`
Shelly-Geräte der ersten Generation mit `shellies/...`-Topics werden nicht
unterstützt.
## Einrichtung
1. Das Modul über die Enelix-Utils-Bibliothek installieren und eine Instanz
`Shelly Modul` anlegen.
2. Das gewünschte MQTT-Gateway verbinden. Neue Instanzen verwenden
standardmässig den internen MQTT Server; für einen externen Broker kann ein
MQTT Client als Gateway gewählt werden.
3. Am Shelly MQTT aktivieren und den Broker, Port und gegebenenfalls
Zugangsdaten eintragen. Zugangsdaten gehören ausschliesslich in die
MQTT-/Shelly-Konfiguration und nicht in dieses Modul.
4. Den Topic-Präfix prüfen und die Instanz übernehmen.
5. Eine Statusänderung am Gerät auslösen. Der Geräteordner und seine Variablen
werden mit der ersten passenden Meldung angelegt.
## Konfiguration
| Property | Standard | Beschreibung |
| --- | --- | --- |
| `DeviceTopicPrefix` | `shelly` | Nur erste Topic-Ebenen mit diesem Präfix werden verarbeitet; Vergleich ohne Beachtung der Gross-/Kleinschreibung. Leer erlaubt alle gültigen Topic-Namen. |
| `Debug` | `false` | Schreibt verarbeitete und gesendete MQTT-Nachrichten in den Instanz-Debug. |
Ein benutzerdefinierter Shelly-`topic_prefix` muss mit
`DeviceTopicPrefix` übereinstimmen. Das Feld ist ein einfacher Präfix und
kein regulärer Ausdruck.
## Automatisch angelegte Objekte
Für jede erste MQTT-Topic-Ebene wird ein Geräteordner angelegt. Technische
Idents bestehen nur aus den von IP-Symcon erlaubten Zeichen und enthalten
zusätzlich einen kurzen Hash des originalen Topic-Namens. Damit bleiben auch
Namen mit Bindestrichen stabil und kollisionsarm.
| Anzeige | Typ / Zugriff | MQTT-Quelle |
| --- | --- | --- |
| Online | Boolean / Anzeige | `<Topic>/online` |
| Typ | String / Anzeige | `src` einer RPC-Meldung |
| Input n | Boolean / Anzeige | `input:n.state` oder `switch:n.input` |
| Output n | Boolean / bedienbar | `switch:n.output` |
| Temperatur | Float / Anzeige | erster erkannter Celsiuswert in `temperature`/`tC` |
Neue Inputs und Outputs werden dynamisch ergänzt. Bestehende Objekte werden
nicht automatisch gelöscht, wenn ein Datenpunkt später nicht mehr gemeldet
wird.
## Ausgänge schalten
Eine Bedienung von `Output n` veröffentlicht einen Shelly-RPC-Aufruf
`Switch.Set` auf `<Topic>/rpc`. Der Variablenwert wird nicht optimistisch
gesetzt; er folgt der nächsten Statusmeldung des Geräts. Dadurch zeigt
IP-Symcon keinen erfolgreichen Schaltvorgang an, wenn der Broker oder das
Gerät den Auftrag nicht ausführt.
Aus Skripten kann derselbe Befehl direkt aufgerufen werden:
```php
SHELLY_SetOutput($instanceID, 'shellyplus1pm-a1b2c3', 0, true);
```
## Unterstützte RPC-Struktur
Das Modul verarbeitet Publish-Pakete (`PacketType = 3`) mit JSON-Payloads
nach folgendem Muster:
```json
{
"src": "shellyplus1pm-a1b2c3",
"method": "NotifyStatus",
"params": {
"switch:0": {
"output": true,
"temperature": {"tC": 42.5}
},
"input:0": {"state": false}
}
}
```
Unvollständige, ungültige oder nicht zum konfigurierten Topic-Präfix passende
Nachrichten werden ignoriert. Bei aktiviertem Debug werden fehlerhafte
JSON-Payloads und ignorierte Online-Werte nachvollziehbar protokolliert.
## Hinweise zur Adaption
Aus `Shelly_Parser_MQTT` von Enelix 1 wurden MQTT/RPC-Datenfluss,
Geräteordner, dynamische Inputs/Outputs, Temperatur und `Switch.Set`
fachlich übernommen. Neu implementiert wurden gültige stabile Idents,
tatsächlich schaltbares Debug-Logging, strikte Payload-Prüfung, ein
konfigurierbarer Topic-Präfix sowie verständliche Fehler bei einem inaktiven
MQTT-Gateway. Die pauschale MQTT-`#`-Subscription und dauerhaftes
System-Logging wurden verworfen.
+24
View File
@@ -0,0 +1,24 @@
{
"elements": [
{
"type": "Label",
"caption": "Verarbeitet MQTT/RPC-Statusmeldungen von Shelly Gen2+ und legt erkannte Geräte und Datenpunkte automatisch an."
},
{
"type": "ValidationTextBox",
"name": "DeviceTopicPrefix",
"caption": "Erlaubter MQTT-Topic-Präfix"
},
{
"type": "CheckBox",
"name": "Debug",
"caption": "Debug-Ausgaben aktivieren"
}
],
"actions": [
{
"type": "Label",
"caption": "MQTT wird am Shelly-Gerät konfiguriert. Standardmässig wird der interne MQTT Server verwendet; ein vorhandener MQTT Client kann als Gateway gewählt werden."
}
]
}
+136
View File
@@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
final class ShellyParser
{
/**
* Extrahiert den Modellteil aus einer Shelly-RPC-Source.
*/
public static function extractType(string $source): string
{
if (preg_match('/^shelly([a-z0-9]+)-/i', trim($source), $matches) !== 1) {
return 'unknown';
}
return strtolower($matches[1]);
}
/**
* @return array{outputs: array<int, bool>, inputs: array<int, bool>, temperature: ?float}
*/
public static function mapParams(array $params): array
{
$mapped = [
'outputs' => [],
'inputs' => [],
'temperature' => null,
];
foreach ($params as $component => $value) {
if (!is_string($component) || !is_array($value)) {
continue;
}
if (preg_match('/^switch:(\d+)$/', $component, $matches) === 1) {
$index = (int) $matches[1];
$output = self::toBoolean($value['output'] ?? null);
$input = self::toBoolean($value['input'] ?? null);
if ($output !== null) {
$mapped['outputs'][$index] = $output;
}
if ($input !== null) {
$mapped['inputs'][$index] = $input;
}
}
if (preg_match('/^input:(\d+)$/', $component, $matches) === 1) {
$state = self::toBoolean($value['state'] ?? null);
if ($state !== null) {
$mapped['inputs'][(int) $matches[1]] = $state;
}
}
}
$mapped['temperature'] = self::findTemperature($params);
ksort($mapped['outputs']);
ksort($mapped['inputs']);
return $mapped;
}
public static function toBoolean($value): ?bool
{
if (is_bool($value)) {
return $value;
}
if ($value === 1 || $value === '1') {
return true;
}
if ($value === 0 || $value === '0') {
return false;
}
if (is_string($value)) {
$normalized = strtolower(trim($value));
if (in_array($normalized, ['true', 'on', 'online'], true)) {
return true;
}
if (in_array($normalized, ['false', 'off', 'offline'], true)) {
return false;
}
}
return null;
}
private static function findTemperature(array $data): ?float
{
foreach ($data as $key => $value) {
if (!is_string($key)) {
continue;
}
$lowerKey = strtolower($key);
if ($lowerKey === 'temperature') {
$temperature = self::temperatureFromValue($value);
if ($temperature !== null) {
return $temperature;
}
}
if ($lowerKey === 'tc' && is_numeric($value)) {
return (float) $value;
}
if (is_array($value)) {
$temperature = self::findTemperature($value);
if ($temperature !== null) {
return $temperature;
}
}
}
return null;
}
private static function temperatureFromValue($value): ?float
{
if (is_numeric($value)) {
return (float) $value;
}
if (!is_array($value)) {
return null;
}
foreach (['tC', 'tc', 't'] as $key) {
if (array_key_exists($key, $value) && is_numeric($value[$key])) {
return (float) $value[$key];
}
}
return null;
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"id": "{21D82EDB-AC8E-4F3D-9FEE-D20BCAFC1C33}",
"name": "Shelly Modul",
"type": 3,
"vendor": "Belevo AG",
"aliases": [
"Shelly MQTT Parser"
],
"parentRequirements": [
"{043EA491-0325-4ADD-8FC2-A30C8EEB4D3F}"
],
"childRequirements": [],
"implemented": [
"{7F7632D9-FA40-4F38-8DEA-C83CD4325A32}"
],
"prefix": "SHELLY",
"url": ""
}
+350
View File
@@ -0,0 +1,350 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/libs/ShellyParser.php';
class ShellyModul extends IPSModule
{
private const MQTT_SERVER_MODULE_ID = '{C6D2AEB3-6E1F-4B2E-8E69-3A1A00246850}';
private const MQTT_TX_DATA_ID = '{043EA491-0325-4ADD-8FC2-A30C8EEB4D3F}';
public function Create()
{
parent::Create();
$this->RegisterPropertyString('DeviceTopicPrefix', 'shelly');
$this->RegisterPropertyBoolean('Debug', false);
$this->ConnectParent(self::MQTT_SERVER_MODULE_ID);
}
public function ApplyChanges()
{
parent::ApplyChanges();
}
public function ReceiveData($JSONString)
{
try {
$data = json_decode((string) $JSONString, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
$this->debug('Ungültiges MQTT-Datenpaket', $exception->getMessage());
return;
}
if (!is_array($data) || (int) ($data['PacketType'] ?? 0) !== 3) {
return;
}
$topic = $data['Topic'] ?? null;
$payload = $data['Payload'] ?? null;
if (!is_string($topic) || !is_string($payload)) {
return;
}
$topicParts = explode('/', $topic);
if (count($topicParts) < 2) {
return;
}
$deviceTopic = array_shift($topicParts);
if (!$this->isAcceptedDeviceTopic($deviceTopic)) {
return;
}
$this->debug('MQTT empfangen', $topic . ' -> ' . $payload);
if ($topicParts === ['online']) {
$this->handleOnline($deviceTopic, $payload);
return;
}
if ($topicParts === ['events', 'rpc']) {
$this->handleRpc($deviceTopic, $payload);
}
}
public function RequestAction($Ident, $Value)
{
$parts = explode(':', (string) $Ident, 2);
if (count($parts) !== 2 || !ctype_digit($parts[0])) {
throw new InvalidArgumentException('Ungültige Shelly-Aktion: ' . (string) $Ident);
}
$folderID = (int) $parts[0];
$variableIdent = $parts[1];
if (!IPS_ObjectExists($folderID) || IPS_GetParent($folderID) !== $this->InstanceID) {
throw new InvalidArgumentException('Der Geräteordner gehört nicht zu dieser Instanz.');
}
if (preg_match('/_output_(\d+)$/', $variableIdent, $matches) !== 1) {
throw new InvalidArgumentException('Unbekannter Shelly-Ausgang: ' . $variableIdent);
}
$variableID = @IPS_GetObjectIDByIdent($variableIdent, $folderID);
if ($variableID === false || !IPS_VariableExists($variableID)) {
throw new InvalidArgumentException('Die Ausgangsvariable wurde nicht gefunden.');
}
$this->SetOutput(IPS_GetName($folderID), (int) $matches[1], (bool) $Value);
}
public function SetOutput(string $DeviceTopic, int $Output, bool $Value): void
{
if (!$this->isAcceptedDeviceTopic($DeviceTopic)) {
throw new InvalidArgumentException('Das Geräte-Topic entspricht nicht dem konfigurierten Präfix.');
}
if ($Output < 0) {
throw new InvalidArgumentException('Der Ausgangsindex darf nicht negativ sein.');
}
$payload = json_encode([
'id' => random_int(1, 2147483647),
'src' => 'enelix-symcon-' . $this->InstanceID,
'method' => 'Switch.Set',
'params' => [
'id' => $Output,
'on' => $Value,
],
], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
$this->publish($DeviceTopic . '/rpc', $payload);
}
private function handleOnline(string $deviceTopic, string $payload): void
{
$value = ShellyParser::toBoolean($payload);
if ($value === null) {
$this->debug('Online-Status ignoriert', $payload);
return;
}
$variableID = $this->ensureVariable($deviceTopic, 'online', 'Online', 0, '', 10);
if ($variableID > 0) {
SetValue($variableID, $value);
}
}
private function handleRpc(string $deviceTopic, string $payload): void
{
try {
$rpc = json_decode($payload, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
$this->debug('Ungültiges Shelly-RPC', $exception->getMessage());
return;
}
if (!is_array($rpc)) {
return;
}
$source = $rpc['src'] ?? '';
if (!is_string($source) || ShellyParser::extractType($source) === 'unknown') {
return;
}
$params = $rpc['params'] ?? null;
if (!is_array($params)) {
return;
}
$typeID = $this->ensureVariable($deviceTopic, 'type', 'Typ', 3, '', 20);
if ($typeID > 0) {
SetValue($typeID, ShellyParser::extractType($source));
}
$mapped = ShellyParser::mapParams($params);
foreach ($mapped['inputs'] as $index => $value) {
$variableID = $this->ensureVariable(
$deviceTopic,
'input_' . $index,
'Input ' . $index,
0,
'',
100 + $index
);
if ($variableID > 0) {
SetValue($variableID, $value);
}
}
foreach ($mapped['outputs'] as $index => $value) {
$variableID = $this->ensureVariable(
$deviceTopic,
'output_' . $index,
'Output ' . $index,
0,
'~Switch',
200 + $index,
true
);
if ($variableID > 0) {
SetValue($variableID, $value);
}
}
if ($mapped['temperature'] !== null) {
$variableID = $this->ensureVariable(
$deviceTopic,
'temperature',
'Temperatur',
2,
'~Temperature',
300
);
if ($variableID > 0) {
SetValue($variableID, $mapped['temperature']);
}
}
}
private function ensureVariable(
string $deviceTopic,
string $suffix,
string $name,
int $type,
string $profile,
int $position,
bool $action = false
): int {
$folderID = $this->ensureDeviceFolder($deviceTopic);
$ident = $this->deviceKey($deviceTopic) . '_' . $suffix;
$variableID = @IPS_GetObjectIDByIdent($ident, $folderID);
if ($variableID !== false) {
if (!IPS_VariableExists($variableID) || IPS_GetVariable($variableID)['VariableType'] !== $type) {
$this->debug('Variablentyp-Konflikt', $ident);
return 0;
}
} else {
$variableID = IPS_CreateVariable($type);
IPS_SetParent($variableID, $folderID);
IPS_SetIdent($variableID, $ident);
}
IPS_SetName($variableID, $name);
IPS_SetPosition($variableID, $position);
if ($profile !== '') {
IPS_SetVariableCustomProfile($variableID, $profile);
}
if ($action) {
$scriptID = $this->ensureActionScript();
if ($scriptID > 0) {
IPS_SetVariableCustomAction($variableID, $scriptID);
}
}
return $variableID;
}
private function ensureDeviceFolder(string $deviceTopic): int
{
$ident = 'folder_' . $this->deviceKey($deviceTopic);
$folderID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
if ($folderID !== false) {
return $folderID;
}
$folderID = IPS_CreateCategory();
IPS_SetParent($folderID, $this->InstanceID);
IPS_SetIdent($folderID, $ident);
IPS_SetName($folderID, $deviceTopic);
IPS_SetInfo($folderID, 'Automatisch aus MQTT-Topic ' . $deviceTopic . ' angelegt.');
return $folderID;
}
private function ensureActionScript(): int
{
$ident = 'action_handler';
$scriptID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
if ($scriptID === false) {
$scriptID = IPS_CreateScript(0);
IPS_SetParent($scriptID, $this->InstanceID);
IPS_SetIdent($scriptID, $ident);
} elseif (!IPS_ScriptExists($scriptID)) {
$this->debug('Action Handler', 'Ident ist bereits durch ein anderes Objekt belegt.');
return 0;
}
IPS_SetName($scriptID, 'Shelly Action Handler');
IPS_SetHidden($scriptID, true);
IPS_SetScriptContent($scriptID, <<<'PHP'
<?php
$variableID = $_IPS['VARIABLE'];
$folderID = IPS_GetParent($variableID);
$moduleID = IPS_GetParent($folderID);
$ident = IPS_GetObject($variableID)['ObjectIdent'];
IPS_RequestAction($moduleID, $folderID . ':' . $ident, $_IPS['VALUE']);
PHP
);
return $scriptID;
}
private function publish(string $topic, string $payload): void
{
$parentID = (int) (IPS_GetInstance($this->InstanceID)['ConnectionID'] ?? 0);
if ($parentID === 0 || !IPS_InstanceExists($parentID)) {
throw new RuntimeException('Kein MQTT-Gateway verbunden.');
}
if ((int) IPS_GetInstance($parentID)['InstanceStatus'] !== 102) {
throw new RuntimeException('Das verbundene MQTT-Gateway ist nicht aktiv.');
}
$packet = json_encode([
'DataID' => self::MQTT_TX_DATA_ID,
'PacketType' => 3,
'QualityOfService' => 0,
'Retain' => false,
'Topic' => $topic,
'Payload' => $payload,
], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
$this->SendDataToParent($packet);
$this->debug('MQTT gesendet', $topic . ' -> ' . $payload);
}
private function isAcceptedDeviceTopic(string $deviceTopic): bool
{
if ($deviceTopic === '' || strlen($deviceTopic) > 128) {
return false;
}
if (preg_match('//u', $deviceTopic) !== 1) {
return false;
}
if (preg_match('/[\x00-\x1F\x7F\/#\+]/u', $deviceTopic) === 1) {
return false;
}
$prefix = trim($this->ReadPropertyString('DeviceTopicPrefix'));
return $prefix === '' || strncasecmp($deviceTopic, $prefix, strlen($prefix)) === 0;
}
private function deviceKey(string $deviceTopic): string
{
$normalized = strtolower($deviceTopic);
$normalized = preg_replace('/[^a-z0-9_]+/', '_', $normalized) ?? 'device';
$normalized = trim($normalized, '_');
if ($normalized === '') {
$normalized = 'device';
}
return substr($normalized, 0, 48) . '_' . substr(hash('sha256', $deviceTopic), 0, 8);
}
private function debug(string $title, $message): void
{
if (!$this->ReadPropertyBoolean('Debug')) {
return;
}
if (!is_string($message)) {
$message = json_encode($message, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
$this->SendDebug($title, (string) $message, 0);
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
# Modulübersicht Enelix Utils # Modulübersicht Enelix Utils
> Status: Der Verbrauchskostenreport ist implementiert. Die übrigen Module > Status: Verbrauchskostenreport und Shelly Modul sind implementiert. Die
> sind als Diskussionsentwürfe dokumentiert. > übrigen Module sind als Diskussionsentwürfe dokumentiert.
| Modul | Herkunft | EMS-Abhängigkeit | | Modul | Herkunft | EMS-Abhängigkeit |
| --- | --- | --- | | --- | --- | --- |
+83 -16
View File
@@ -1,31 +1,98 @@
# Shelly Modul # Shelly Modul
> Status: Diskussionsentwurf. Übernimmt `Shelly_Parser_MQTT` möglichst > Status: Implementiert. Die fachliche Funktion von
> unverändert und ohne EMS-Abhängigkeit. > `Shelly_Parser_MQTT` wurde für IP-Symcon 8.0 bereinigt und ohne
> EMS-Abhängigkeit in Enelix Utils adaptiert.
Das Modul lauscht auf MQTT und legt pro erkanntem Gerät weiterhin einen Ordner Das Modul verarbeitet Shelly Gen2+-Statusmeldungen über das native
mit Online-Status, Typ, Inputs, Outputs und Temperatur an. IP-Symcon-MQTT-Datenflussinterface. Ein interner MQTT Server wird beim
Erstellen als Standard-Parent angeboten; ein kompatibler MQTT Client kann
manuell als Gateway verbunden werden.
## Ziel und Abgrenzung
- automatische Erkennung von Geräten und vorhandenen Schaltkanälen
- Anzeige von Online-Status, Typ, Inputs, Outputs und Temperatur
- Schalten erkannter Outputs über Shelly RPC `Switch.Set`
- keine Geräteauswahl, Discovery-Instanz oder EMS-Kopplung
- keine Unterstützung für Shelly Gen1-`shellies/...`-Topics
- keine Verwaltung von MQTT-Zugangsdaten oder Shelly-Gerätekonfiguration
## MQTT-Datenfluss
| Richtung | Topic | Inhalt |
| --- | --- | --- |
| Shelly nach Symcon | `<Topic>/online` | `true`/`false`, `1`/`0` oder `online`/`offline` |
| Shelly nach Symcon | `<Topic>/events/rpc` | RPC-Payload mit `src` und `params` |
| Symcon nach Shelly | `<Topic>/rpc` | RPC-Aufruf `Switch.Set` |
Nur MQTT-Publish-Pakete mit `PacketType = 3` werden ausgewertet. Die erste
Topic-Ebene wird als Geräte-Topic verwendet. Der konfigurierte Präfix wird
ohne Beachtung der Gross-/Kleinschreibung geprüft.
## Dynamische Variablen ## Dynamische Variablen
`<Schlüssel>` ist ein aus dem Geräte-Topic abgeleiteter, für IP-Symcon
gültiger Ident-Bestandteil mit kurzem Hash.
| Ident-Muster | Typ / Zugriff | Beschreibung | | Ident-Muster | Typ / Zugriff | Beschreibung |
| --- | --- | --- | | --- | --- | --- |
| `<Geraet>_online` | Boolean / Anzeige | Online-Meldung. | | `<Schlüssel>_online` | Boolean / Anzeige | Online-Meldung. |
| `<Geraet>_type` | String / Anzeige | Erkannter Gerätetyp. | | `<Schlüssel>_type` | String / Anzeige | Aus der RPC-`src` extrahierter Modellteil. |
| `<Geraet>_input_n` | Boolean / Anzeige | Dynamisch erkannter Eingang. | | `<Schlüssel>_input_n` | Boolean / Anzeige | `input:n.state` oder `switch:n.input`. |
| `<Geraet>_output_n` | Boolean / bedienbar | Ausgang über vorhandenen Action Handler. | | `<Schlüssel>_output_n` | Boolean / bedienbar | `switch:n.output`; schaltet per RPC. |
| `<Geraet>_temperature` | Float / Anzeige | Erkannte Temperatur. | | `<Schlüssel>_temperature` | Float / Anzeige | Erster eindeutig erkannter Celsiuswert. |
Die Output-Aktion wartet auf die Rückmeldung des Geräts und setzt den
Variablenwert nicht vorab. Dynamisch angelegte Datenpunkte werden bei später
fehlenden Meldungen nicht gelöscht.
## Properties ## Properties
| Ident | Typ | Standard / Beschreibung | | Ident | Typ | Standard / Beschreibung |
| --- | --- | --- | | --- | --- | --- |
| `Debug` | Boolean | `false`; bestehendes Debug-Logging. | | `DeviceTopicPrefix` | String | `shelly`; einfacher Präfix für die erste Topic-Ebene, leer akzeptiert alle gültigen Namen. |
| `Debug` | Boolean | `false`; Instanz-Debug für empfangene/gesendete MQTT-Daten und Validierungsfehler. |
## Verhalten und offene Punkte ## Öffentliche Funktion
- MQTT-Abonnements, Geräteordner, technische Idents und der interne Action ```php
Handler bleiben erhalten. SHELLY_SetOutput(
- In diesem Schritt keine Geräte- oder Datenpunktauswahl und keine neue int $InstanzID,
Objektstruktur einführen. string $DeviceTopic,
- Beim Praxistest klären, ob der bestehende Debug-Schalter ausreicht. int $Output,
bool $Value
): void;
```
Die Funktion validiert Topic-Präfix und Ausgangsindex. Ein fehlendes oder
inaktives MQTT-Gateway führt zu einer verständlichen Exception.
## Adaption aus Enelix 1
| Teil | Entscheidung |
| --- | --- |
| MQTT-Datenfluss-GUIDs und RPC-`Switch.Set` | angepasst übernommen |
| dynamische Geräteordner und Datenpunkte | angepasst übernommen |
| Parser für Inputs, Outputs und Temperatur | neu und strenger implementiert |
| technische Idents mit roher Shelly-ID | verworfen; Bindestriche sind in IP-Symcon-Idents ungültig |
| gemeinsames Action-Skript | angepasst übernommen und verborgen |
| globale `#`-Subscription | verworfen; das native MQTT-Gateway liefert Publish-Daten über den Datenfluss |
| bedingungsloses `IPS_LogMessage` | verworfen; Debug-Ausgabe respektiert die Property |
| optimistisches Setzen eines Outputs | verworfen; Status folgt der Gerätebestätigung |
## Betrieb und Diagnose
1. MQTT-Gateway muss verbunden und aktiv sein.
2. Shelly MQTT muss RPC-Statusmeldungen publizieren.
3. Geräte-Topic und `DeviceTopicPrefix` müssen zusammenpassen.
4. Bei fehlenden Variablen kurzzeitig `Debug` aktivieren und eine
Statusänderung auslösen.
5. Zugangsdaten niemals in Logs oder Repository-Dokumentation übernehmen.
## Offene Punkte
- Praxistest mit den im Projekt eingesetzten Shelly-Modellen und deren
Firmwareständen.
- Bei Bedarf spätere Erweiterung um weitere Komponenten wie Cover, Light,
Meter oder Batteriestatus als separat beschlossene Funktionalität.
+1 -1
View File
@@ -17,7 +17,7 @@ final class DokumentationsstrukturTest extends TestCase
'CC100 Hardware' => ['CC100-Hardware', 'Status: Diskussionsentwurf'], 'CC100 Hardware' => ['CC100-Hardware', 'Status: Diskussionsentwurf'],
'Energiediagramm' => ['Energiediagramm', 'Status: Diskussionsentwurf'], 'Energiediagramm' => ['Energiediagramm', 'Status: Diskussionsentwurf'],
'VGT-Schnittstelle' => ['VGT-Schnittstelle', 'Status: Diskussionsentwurf'], 'VGT-Schnittstelle' => ['VGT-Schnittstelle', 'Status: Diskussionsentwurf'],
'Shelly-Modul' => ['Shelly-Modul', 'Status: Diskussionsentwurf'], 'Shelly-Modul' => ['Shelly-Modul', 'Status: Implementiert'],
]; ];
} }