no message

This commit is contained in:
2025-11-14 08:47:13 +01:00
parent 8e5e40912d
commit 88e10aaf08
4 changed files with 212 additions and 184 deletions

View File

@@ -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": []

View File

@@ -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;
}
}

View File

@@ -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"
}

View File

@@ -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: "<DeviceID>_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;
}
// <GeräteID>/online
if ((isset($parts[1])) && ($parts[1] === 'online')) {
$this->HandleOnline($deviceID, $payload);
return;
}
// RPC
if ((($parts[1] ?? "") === "events") && (($parts[2] ?? "") === "rpc")) {
// <GeräteID>/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']);
}
// 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)
));
}
}
public function ShellyScanDevices()
/* ---------------------------------------------------------
* Helper für Variablen
* ---------------------------------------------------------*/
private function EnsureBooleanVariable(string $ident, string $name): void
{
// 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));
$id = @$this->GetIDForIdent($ident);
if ($id === false) {
$this->RegisterVariableBoolean($ident, $name);
}
}
/* ------------------------------------------------------------------
* Helper → Child-Instanzen erzeugen
* ------------------------------------------------------------------*/
private function GetOrCreateDeviceInstance(string $deviceID)
private function EnsureFloatVariable(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->RegisterVariableFloat($ident, $name);
}
}
// neue Instanz
$iid = IPS_CreateInstance("{ED0ED0AC-3843-0888-DF27-FA45435BCEF3}");
IPS_SetName($iid, $deviceID);
IPS_SetParent($iid, $this->InstanceID);
return $iid;
private function EnsureStringVariable(string $ident, string $name): void
{
$id = @$this->GetIDForIdent($ident);
if ($id === false) {
$this->RegisterVariableString($ident, $name);
}
}
}
?>