diff --git a/Shelly_Parser_MQTT/form.json b/Shelly_Parser_MQTT/form.json index f628176..5c74efe 100644 --- a/Shelly_Parser_MQTT/form.json +++ b/Shelly_Parser_MQTT/form.json @@ -2,48 +2,12 @@ "elements": [ { "type": "Label", - "caption": "MQTT Einstellungen" - }, - { - "type": "Select", - "name": "MQTTServer", - "caption": "MQTT Server", - "options": [ - { - "caption": "Automatisch (Parent)", - "value": "auto" - } - ] + "caption": "Dieses Modul lauscht auf MQTT-Nachrichten und erstellt pro Shelly-Gerät Variablen (input, output, temperature, online, type)." }, { "type": "CheckBox", "name": "Debug", - "caption": "Debug Log aktivieren" - }, - { - "type": "Label", - "caption": "Erkannte Shelly Geräte" - }, - { - "type": "List", - "name": "Devices", - "columns": [ - { - "caption": "Geräte-ID", - "name": "id", - "width": "200px" - }, - { - "caption": "Typ", - "name": "type", - "width": "150px" - } - ] - }, - { - "type": "Button", - "caption": "Geräte neu scannen", - "onClick": "ShellyScanDevices();" + "caption": "Debug-Log aktivieren" } ], "actions": [] diff --git a/Shelly_Parser_MQTT/libs/ShellyParser.php b/Shelly_Parser_MQTT/libs/ShellyParser.php index c542b86..da286b7 100644 --- a/Shelly_Parser_MQTT/libs/ShellyParser.php +++ b/Shelly_Parser_MQTT/libs/ShellyParser.php @@ -4,42 +4,80 @@ declare(strict_types=1); class ShellyParser { - /** Modell aus src extrahieren */ + /** + * Extrahiert den Modell-Typ aus src, z.B.: + * "shelly1g4-12345" => "1g4" + * "shellyplusplugs-xyz" => "plusplugs" + */ public static function ExtractType(string $src): string { - // Beispiel: "shelly1g4-12345" - if (!str_starts_with($src, "shelly")) { - return "unknown"; + if (!str_starts_with($src, 'shelly')) { + return 'unknown'; } - $str = substr($src, 6); // "1g4-12345" - $parts = explode("-", $str); // ["1g4", "12345"] - return $parts[0] ?? "unknown"; + // alles nach "shelly" + $rest = substr($src, 6); // z.B. "1g4-12345" oder "plusplugs-xyz" + $parts = explode('-', $rest); + return $parts[0] ?? 'unknown'; } - - /** generisches Mapping für Shelly Daten */ + /** + * Geht rekursiv durch params und sammelt bekannte Werte: + * - input (bool) + * - output (bool) + * - temperature (float, inkl. tC) + */ public static function MapParams(array $params): array { - $mapped = []; + $mapped = [ + 'input' => null, + 'output' => null, + 'temperature' => null + ]; - foreach ($params as $key => $val) { + self::ExtractRecursive($params, $mapped); - if ($key === "input") { - $mapped["input"] = (bool)$val; + // null-Werte rauswerfen + return array_filter($mapped, static function ($v) { + return $v !== null; + }); + } + + private static function ExtractRecursive(array $data, array &$mapped): void + { + foreach ($data as $key => $value) { + $lowerKey = strtolower((string)$key); + + if (is_array($value)) { + // Temperatur in verschachtelter Struktur, z.B. ["temperature" => ["tC" => 41.2]] + if ($lowerKey === 'temperature') { + if (isset($value['tC']) && is_numeric($value['tC'])) { + $mapped['temperature'] = (float)$value['tC']; + } elseif (isset($value['t']) && is_numeric($value['t'])) { + $mapped['temperature'] = (float)$value['t']; + } + } + self::ExtractRecursive($value, $mapped); + continue; } - if ($key === "output") { - $mapped["output"] = (bool)$val; - } + switch ($lowerKey) { + case 'input': + $mapped['input'] = (bool)$value; + break; - if ($key === "temperature" || $key === "temp") { - $mapped["temperature"] = (float)$val; - } + case 'output': + $mapped['output'] = (bool)$value; + break; - // weitere Shelly-Geräte können später hier ergänzt werden + case 'temperature': + case 'tc': + case 't': + if (is_numeric($value)) { + $mapped['temperature'] = (float)$value; + } + break; + } } - - return $mapped; } } diff --git a/Shelly_Parser_MQTT/module.json b/Shelly_Parser_MQTT/module.json index dc72707..7da1398 100644 --- a/Shelly_Parser_MQTT/module.json +++ b/Shelly_Parser_MQTT/module.json @@ -1,10 +1,10 @@ { - "id": "{CD0DFA85-F112-BD29-D304-8E5883C0EA79}", + "id": "{ED0ED0AC-3843-0888-DF27-FA45435BCEF3}", "name": "Shelly_Parser_MQTT", "type": 3, - "vendor": "Belevo AG", + "vendor": "Belevo", "aliases": [ - "Shelly_Parser_MQTT-Gateway" + "Shelly MQTT Parser" ], "prefix": "Shelly_Parser_MQTT", "parentRequirements": [ @@ -13,5 +13,6 @@ "childRequirements": [], "implemented": [ "{7F7632D9-FA40-4F38-8DEA-C83CD4325A32}" - ] + ], + "version": "1.0" } diff --git a/Shelly_Parser_MQTT/module.php b/Shelly_Parser_MQTT/module.php index b61fed8..38e9a58 100644 --- a/Shelly_Parser_MQTT/module.php +++ b/Shelly_Parser_MQTT/module.php @@ -2,7 +2,7 @@ declare(strict_types=1); -require_once __DIR__ . "/libs/ShellyParser.php"; +require_once __DIR__ . '/libs/ShellyParser.php'; class Shelly_Parser_MQTT extends IPSModule { @@ -10,213 +10,238 @@ class Shelly_Parser_MQTT extends IPSModule { parent::Create(); + // Verbindung zum MQTT-Server herstellen $this->ConnectParent('{C6D2AEB3-6E1F-4B2E-8E69-3A1A00246850}'); - $this->Subscribe("#"); - $this->RegisterPropertyString("Devices", "[]"); - $this->RegisterPropertyBoolean("Debug", false); + // Auf alle Topics lauschen, Filter machen wir selbst + $this->Subscribe('#'); + + // Debug-Property + $this->RegisterPropertyBoolean('Debug', false); } - public function ApplyChanges() { parent::ApplyChanges(); + $this->ConnectParent('{C6D2AEB3-6E1F-4B2E-8E69-3A1A00246850}'); - $this->Subscribe("#"); + $this->Subscribe('#'); } - - /* ------------------------------------------------------------------ + /* --------------------------------------------------------- * MQTT SUBSCRIBE - * ------------------------------------------------------------------*/ - private function Subscribe(string $topic) + * ---------------------------------------------------------*/ + private function Subscribe(string $topic): void { $packet = [ - "PacketType" => 8, - "QualityOfService" => 0, - "Retain" => false, - "Topic" => $topic, - "Payload" => "" + 'PacketType' => 8, + 'QualityOfService' => 0, + 'Retain' => false, + 'Topic' => $topic, + 'Payload' => '' ]; $this->SendDataToParent(json_encode([ - "DataID" => "{043EA491-0325-4ADD-8FC2-A30C8EEB4D3F}" + 'DataID' => '{043EA491-0325-4ADD-8FC2-A30C8EEB4D3F}' ] + $packet)); } - - /* ------------------------------------------------------------------ + /* --------------------------------------------------------- * MQTT PUBLISH - * ------------------------------------------------------------------*/ - private function Publish(string $topic, string $payload) + * ---------------------------------------------------------*/ + private function Publish(string $topic, string $payload): void { $packet = [ - "PacketType" => 3, - "QualityOfService" => 0, - "Retain" => false, - "Topic" => $topic, - "Payload" => $payload + 'PacketType' => 3, + 'QualityOfService' => 0, + 'Retain' => false, + 'Topic' => $topic, + 'Payload' => $payload ]; $this->SendDataToParent(json_encode([ - "DataID" => "{043EA491-0325-4ADD-8FC2-A30C8EEB4D3F}" + 'DataID' => '{043EA491-0325-4ADD-8FC2-A30C8EEB4D3F}' ] + $packet)); + + if ($this->ReadPropertyBoolean('Debug')) { + IPS_LogMessage('Shelly_Parser_MQTT', 'Publish: ' . $topic . ' -> ' . $payload); + } } - - /* ------------------------------------------------------------------ - * RequestAction → Schalten des Outputs - * ------------------------------------------------------------------*/ + /* --------------------------------------------------------- + * RequestAction → Schalten von Outputs + * ---------------------------------------------------------*/ public function RequestAction($Ident, $Value) { - $iid = IPS_GetParent($this->InstanceID); - + // Wert lokal setzen $this->SetValue($Ident, $Value); - // output? - if ($Ident === "output") { + // Wir erwarten Ident im Format: "_output" + if (str_ends_with($Ident, '_output')) { + $deviceID = substr($Ident, 0, -strlen('_output')); - $deviceID = IPS_GetName($iid); - - $topic = $deviceID . "/rpc/Switch.Set"; + $topic = $deviceID . '/rpc/Switch.Set'; $payload = json_encode([ - "id" => 0, - "on" => (bool)$Value + 'id' => 0, + 'on' => (bool)$Value ]); $this->Publish($topic, $payload); } } - - /* ------------------------------------------------------------------ - * RECEIVE MQTT - * ------------------------------------------------------------------*/ + /* --------------------------------------------------------- + * RECEIVE MQTT DATA + * ---------------------------------------------------------*/ public function ReceiveData($JSONString) { + if ($this->ReadPropertyBoolean('Debug')) { + IPS_LogMessage('Shelly_Parser_MQTT', 'ReceiveData: ' . $JSONString); + } + $data = json_decode($JSONString, true); - - $topic = $data["Topic"] ?? ""; - $payload = $data["Payload"] ?? ""; - - if ($topic === "") { + if (!is_array($data)) { return; } - $parts = explode("/", $topic); - $deviceID = $parts[0] ?? ""; + $topic = $data['Topic'] ?? ''; + $payload = $data['Payload'] ?? ''; - if ($deviceID === "") { + if ($topic === '') { return; } - // ONLINE - if (isset($parts[1]) && $parts[1] === "online") { + $parts = explode('/', $topic); + $deviceID = $parts[0] ?? ''; + + if ($deviceID === '') { + return; + } + + // /online + if ((isset($parts[1])) && ($parts[1] === 'online')) { $this->HandleOnline($deviceID, $payload); return; } - // RPC - if ((($parts[1] ?? "") === "events") && (($parts[2] ?? "") === "rpc")) { + // /events/rpc + if ((isset($parts[1]) && $parts[1] === 'events') && + (isset($parts[2]) && $parts[2] === 'rpc')) { $this->HandleRPC($deviceID, $payload); return; } } - - /* ------------------------------------------------------------------ - * ONLINE - * ------------------------------------------------------------------*/ - private function HandleOnline(string $deviceID, string $payload) + /* --------------------------------------------------------- + * ONLINE-STATUS + * ---------------------------------------------------------*/ + private function HandleOnline(string $deviceID, string $payload): void { - $iid = $this->GetOrCreateDeviceInstance($deviceID); + $ident = $deviceID . '_online'; + $name = $deviceID . ' Online'; - $onlineID = @$this->GetIDForIdentEx("online", $iid); - if (!$onlineID) { - $this->RegisterVariableBoolean("online", "Online"); + $this->EnsureBooleanVariable($ident, $name); + + $value = ($payload === 'true' || $payload === '1'); + $this->SetValue($ident, $value); + + if ($this->ReadPropertyBoolean('Debug')) { + IPS_LogMessage('Shelly_Parser_MQTT', sprintf( + 'Online-Status %s -> %s', + $deviceID, + $value ? 'true' : 'false' + )); } - - $this->SetValue("online", $payload === "true"); } - - /* ------------------------------------------------------------------ - * RPC - * ------------------------------------------------------------------*/ - private function HandleRPC(string $deviceID, string $payload) + /* --------------------------------------------------------- + * RPC-EVENTS + * ---------------------------------------------------------*/ + private function HandleRPC(string $deviceID, string $payload): void { $json = json_decode($payload, true); if (!is_array($json)) { return; } - $src = $json["src"] ?? ""; - if (!str_starts_with($src, "shelly")) { + $src = $json['src'] ?? ''; + if (!is_string($src) || !str_starts_with($src, 'shelly')) { + // kein Shelly-Gerät → ignorieren return; } - // Modell ermitteln + // Typ/Modell aus src extrahieren $type = ShellyParser::ExtractType($src); - $iid = $this->GetOrCreateDeviceInstance($deviceID); + // Typ-Variable + $typeIdent = $deviceID . '_type'; + $this->EnsureStringVariable($typeIdent, $deviceID . ' Typ'); + $this->SetValue($typeIdent, $type); - // Typ speichern - $this->RegisterVariableString("type", "Geräte-Typ"); - $this->SetValue("type", $type); - - // Parameter extrahieren - $mapped = ShellyParser::MapParams($json["params"] ?? []); - - // Variablen anlegen - if (isset($mapped["input"])) { - $this->RegisterVariableBoolean("input", "Input"); - $this->SetValue("input", $mapped["input"]); + // params parsen + $params = $json['params'] ?? []; + if (!is_array($params)) { + return; } - if (isset($mapped["output"])) { - $this->RegisterVariableBoolean("output", "Output"); - $this->EnableAction("output"); - $this->SetValue("output", $mapped["output"]); + $mapped = ShellyParser::MapParams($params); + + // input + if (array_key_exists('input', $mapped)) { + $ident = $deviceID . '_input'; + $this->EnsureBooleanVariable($ident, $deviceID . ' Input'); + $this->SetValue($ident, (bool)$mapped['input']); } - if (isset($mapped["temperature"])) { - $this->RegisterVariableFloat("temperature", "Temperatur"); - $this->SetValue("temperature", $mapped["temperature"]); + // output + if (array_key_exists('output', $mapped)) { + $ident = $deviceID . '_output'; + $this->EnsureBooleanVariable($ident, $deviceID . ' Output'); + $this->EnableAction($ident); + $this->SetValue($ident, (bool)$mapped['output']); } - } - - public function ShellyScanDevices() - { - // Diese Funktion kann später echte Scan-Logik erhalten. - // Jetzt nur Dummy, damit das Modul geladen wird. - IPS_LogMessage("Shelly_Parser_MQTT", "ShellyScanDevices() aufgerufen"); - // Optional: spätere Erweiterung – bekannte Devices ausgeben - $devices = json_decode($this->ReadPropertyString("Devices"), true); - if (is_array($devices)) { - IPS_LogMessage("Shelly_Parser_MQTT", "Aktuell gespeicherte Geräte: " . json_encode($devices)); + // temperature + if (array_key_exists('temperature', $mapped)) { + $ident = $deviceID . '_temperature'; + $this->EnsureFloatVariable($ident, $deviceID . ' Temperatur'); + $this->SetValue($ident, (float)$mapped['temperature']); + } + + if ($this->ReadPropertyBoolean('Debug')) { + IPS_LogMessage('Shelly_Parser_MQTT', sprintf( + 'RPC von %s (Typ %s) → %s', + $deviceID, + $type, + json_encode($mapped) + )); } } - - /* ------------------------------------------------------------------ - * Helper → Child-Instanzen erzeugen - * ------------------------------------------------------------------*/ - private function GetOrCreateDeviceInstance(string $deviceID) + /* --------------------------------------------------------- + * Helper für Variablen + * ---------------------------------------------------------*/ + private function EnsureBooleanVariable(string $ident, string $name): void { - foreach (IPS_GetInstanceListByModuleID("{ED0ED0AC-3843-0888-DF27-FA45435BCEF3}") as $id) { - if (IPS_GetName($id) === $deviceID) { - return $id; - } + $id = @$this->GetIDForIdent($ident); + if ($id === false) { + $this->RegisterVariableBoolean($ident, $name); } + } - // neue Instanz - $iid = IPS_CreateInstance("{ED0ED0AC-3843-0888-DF27-FA45435BCEF3}"); - IPS_SetName($iid, $deviceID); - IPS_SetParent($iid, $this->InstanceID); + private function EnsureFloatVariable(string $ident, string $name): void + { + $id = @$this->GetIDForIdent($ident); + if ($id === false) { + $this->RegisterVariableFloat($ident, $name); + } + } - return $iid; + private function EnsureStringVariable(string $ident, string $name): void + { + $id = @$this->GetIDForIdent($ident); + if ($id === false) { + $this->RegisterVariableString($ident, $name); + } } } -?> -