Files
Symcon_Belevo_Energiemanage…/Shelly_Parser_MQTT/module.php
2025-11-21 11:48:30 +01:00

446 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
require_once __DIR__ . '/libs/ShellyParser.php';
class Shelly_Parser_MQTT extends IPSModule
{
public function Create()
{
parent::Create();
// MQTT verbinden (nur Verknüpfung, noch kein Subscribe!)
$this->ConnectParent('{C6D2AEB3-6E1F-4B2E-8E69-3A1A00246850}');
}
public function ApplyChanges()
{
parent::ApplyChanges();
// Parent verbinden
$this->ConnectParent('{C6D2AEB3-6E1F-4B2E-8E69-3A1A00246850}');
// Auf Statusänderungen der eigenen Instanz hören
$this->RegisterMessage($this->InstanceID, IM_CHANGESTATUS);
// Auf Statusänderungen des Parents hören, falls vorhanden
$inst = IPS_GetInstance($this->InstanceID);
$parentID = $inst['ConnectionID'] ?? 0;
if ($parentID > 0 && IPS_InstanceExists($parentID)) {
$this->RegisterMessage($parentID, IM_CHANGESTATUS);
}
// WICHTIG:
// KEIN Subscribe() hier aufrufen!
// Subscribe passiert erst, wenn Instanz/Parent wirklich aktiv ist (MessageSink).
}
/* ---------------------------------------------------------
* MESSAGE SINK REAKTION AUF STATUSÄNDERUNGEN
* ---------------------------------------------------------*/
public function MessageSink($TimeStamp, $SenderID, $Message, $Data)
{
$this->SendDebug('MessageSink', 'Sender=' . $SenderID . ' Message=' . $Message . ' Data=' . print_r($Data, true), 0);
if ($Message === IM_CHANGESTATUS) {
// Eigene Instanz-Statusänderung
if ($SenderID === $this->InstanceID) {
$newStatus = $Data[0] ?? 0;
$this->Log('STATUS', 'Instance status changed to ' . $newStatus);
if ($newStatus === IS_ACTIVE) {
// Jetzt ist das Modul aktiv → Subscribe starten
$this->Log('STATUS', 'Instance is ACTIVE → performing MQTT subscribe');
$this->Subscribe('#');
}
return;
}
// Parent-Statusänderung
$inst = IPS_GetInstance($this->InstanceID);
$parentID = $inst['ConnectionID'] ?? 0;
if (($parentID > 0) && ($SenderID === $parentID)) {
$newStatus = $Data[0] ?? 0;
$this->Log('STATUS', 'Parent status changed to ' . $newStatus);
if ($newStatus === IS_ACTIVE) {
// Parent wieder aktiv → neu subscriben
$this->Log('STATUS', 'Parent is ACTIVE → performing MQTT subscribe');
$this->Subscribe('#');
}
}
}
}
/* ---------------------------------------------------------
* HILFSFUNKTIONEN FÜR PARENT-CHECK & SICHERES SENDEN
* ---------------------------------------------------------*/
private function HasActiveParent(): bool
{
if (!IPS_InstanceExists($this->InstanceID)) {
return false;
}
$inst = IPS_GetInstance($this->InstanceID);
$parentID = $inst['ConnectionID'] ?? 0;
if ($parentID <= 0 || !IPS_InstanceExists($parentID)) {
return false;
}
$parentStatus = IPS_GetInstance($parentID)['InstanceStatus'];
return ($parentStatus === IS_ACTIVE);
}
private function SafeSendToParent(array $packet): void
{
if (!$this->HasActiveParent()) {
$this->Log('MQTT', 'Cannot send to parent parent not active or not available');
return;
}
// Fehler unterdrücken, falls doch mal etwas schiefgeht
@$this->SendDataToParent(json_encode($packet));
}
/* ---------------------------------------------------------
* DEBUG
* ---------------------------------------------------------*/
private function Log($title, $msg)
{
IPS_LogMessage("ShellyMQTT: $title", $msg);
$this->SendDebug($title, $msg, 0);
}
/* ---------------------------------------------------------
* MQTT SUBSCRIBE
* ---------------------------------------------------------*/
private function Subscribe(string $topic): void
{
$packet = [
'PacketType' => 8,
'QualityOfService' => 0,
'Retain' => false,
'Topic' => $topic,
'Payload' => ''
];
$this->SafeSendToParent([
'DataID' => '{043EA491-0325-4ADD-8FC2-A30C8EEB4D3F}'
] + $packet);
$this->Log('Subscribe', $topic);
}
/* ---------------------------------------------------------
* MQTT PUBLISH
* ---------------------------------------------------------*/
private function Publish(string $topic, string $payload): void
{
$packet = [
'PacketType' => 3,
'QualityOfService' => 0,
'Retain' => false,
'Topic' => $topic,
'Payload' => $payload
];
$this->SafeSendToParent([
'DataID' => '{043EA491-0325-4ADD-8FC2-A30C8EEB4D3F}'
] + $packet);
$this->Log('Publish', "$topic$payload");
}
/* ---------------------------------------------------------
* REQUEST ACTION (Schalten von Outputs)
* ---------------------------------------------------------*/
public function RequestAction($Ident, $Value)
{
$this->Log('RequestAction', "$Ident$Value");
if (!str_contains($Ident, '_output_')) {
throw new Exception("Unknown Ident: $Ident");
}
// Lokale Variable updaten
$varID = $this->FindVariableByIdent($Ident);
if ($varID > 0) {
SetValue($varID, $Value);
}
// Gerät + Index extrahieren
[$deviceID, $suffix] = explode('_output_', $Ident, 2);
$index = intval($suffix);
// Shelly RPC Topic
$topic = $deviceID . '/rpc';
// RPC JSON
$payload = json_encode([
'id' => 1,
'src' => 'ips',
'method' => 'Switch.Set',
'params' => [
'id' => $index,
'on' => (bool)$Value
]
]);
$this->Log('MQTT SEND', "$topic : $payload");
// Senden
$this->Publish($topic, $payload);
}
/* ---------------------------------------------------------
* RECEIVE MQTT
* ---------------------------------------------------------*/
public function ReceiveData($JSONString)
{
$this->Log('ReceiveData', $JSONString);
$data = json_decode($JSONString, true);
if (!is_array($data)) {
return;
}
$topic = $data['Topic'] ?? '';
$payload = $data['Payload'] ?? '';
if ($topic === '') {
return;
}
$this->Log('ReceiveTopic', "$topic$payload");
$parts = explode('/', $topic);
$deviceID = $parts[0] ?? '';
if ($deviceID === '') {
return;
}
// Online-Status
if (($parts[1] ?? '') === 'online') {
$this->HandleOnline($deviceID, $payload);
return;
}
// RPC Event
if (($parts[1] ?? '') === 'events' &&
($parts[2] ?? '') === 'rpc') {
$this->HandleRPC($deviceID, $payload);
return;
}
}
/* ---------------------------------------------------------
* ONLINE STATUS
* ---------------------------------------------------------*/
private function HandleOnline(string $deviceID, string $payload): void
{
$value = ($payload === 'true' || $payload === '1');
$this->Log('Online', "$deviceID = " . ($value ? 'true' : 'false'));
$varID = $this->EnsureBooleanVariable($deviceID, $deviceID . '_online', 'Online');
SetValue($varID, $value);
}
/* ---------------------------------------------------------
* RPC-HANDLING
* ---------------------------------------------------------*/
private function HandleRPC(string $deviceID, string $payload): void
{
$this->Log('RPC', "$deviceID : $payload");
$json = json_decode($payload, true);
if (!is_array($json)) {
return;
}
$src = $json['src'] ?? '';
if (!str_starts_with($src, 'shelly')) {
return;
}
// Typ
$type = ShellyParser::ExtractType($src);
$typeID = $this->EnsureStringVariable($deviceID, $deviceID . '_type', 'Typ');
SetValue($typeID, $type);
// Parameter
$params = $json['params'] ?? [];
$mapped = ShellyParser::MapParams($params);
// Outputs
foreach ($mapped['outputs'] as $index => $value) {
$ident = $deviceID . '_output_' . $index;
$varID = $this->EnsureBooleanVariable($deviceID, $ident, 'Output ' . $index);
SetValue($varID, $value);
}
// Inputs
foreach ($mapped['inputs'] as $index => $value) {
$ident = $deviceID . '_input_' . $index;
$varID = $this->EnsureBooleanVariable($deviceID, $ident, 'Input ' . $index);
SetValue($varID, $value);
}
// Temperatur
if ($mapped['temperature'] !== null) {
$tempID = $this->EnsureFloatVariable($deviceID, $deviceID . '_temperature', 'Temperatur');
SetValue($tempID, $mapped['temperature']);
}
}
/* ---------------------------------------------------------
* ACTIONSCRIPT UNSICHTBAR
* ---------------------------------------------------------*/
private function EnsureActionScript(): int
{
$scriptName = '~ShellyMQTT_Action_' . $this->InstanceID;
foreach (IPS_GetChildrenIDs($this->InstanceID) as $cid) {
$obj = IPS_GetObject($cid);
if ($obj['ObjectType'] === OBJECTTYPE_SCRIPT && $obj['ObjectName'] === $scriptName) {
return $cid;
}
}
// neues verstecktes Script
$scriptID = IPS_CreateScript(0);
IPS_SetName($scriptID, $scriptName);
IPS_SetParent($scriptID, $this->InstanceID);
// unsichtbar
IPS_SetHidden($scriptID, true);
IPS_SetPosition($scriptID, -9999);
// Scriptcode
$php = '<?php
$instance = ' . $this->InstanceID . ';
$object = IPS_GetObject($_IPS["VARIABLE"]);
$ident = $object["ObjectIdent"];
$value = $_IPS["VALUE"];
IPS_RequestAction($instance, $ident, $value);
?>';
IPS_SetScriptContent($scriptID, $php);
return $scriptID;
}
/* ---------------------------------------------------------
* VARIABLEN DYNAMISCH
* ---------------------------------------------------------*/
private function EnsureBooleanVariable(string $deviceID, string $ident, string $name): int
{
$folderID = $this->GetDeviceFolder($deviceID);
foreach (IPS_GetChildrenIDs($folderID) as $cid) {
$o = IPS_GetObject($cid);
if ($o['ObjectIdent'] === $ident) {
// Outputs schaltbar machen
if (str_contains($ident, '_output_')) {
$actionScript = $this->EnsureActionScript();
IPS_SetVariableCustomAction($cid, $actionScript);
$var = IPS_GetVariable($cid);
if ($var['VariableProfile'] === '' && $var['VariableCustomProfile'] === '') {
IPS_SetVariableCustomProfile($cid, '~Switch');
}
}
return $cid;
}
}
// Neu erzeugen
$varID = IPS_CreateVariable(0);
IPS_SetIdent($varID, $ident);
IPS_SetName($varID, $name);
IPS_SetParent($varID, $folderID);
if (str_contains($ident, '_output_')) {
$actionScript = $this->EnsureActionScript();
IPS_SetVariableCustomAction($varID, $actionScript);
IPS_SetVariableCustomProfile($varID, '~Switch');
}
return $varID;
}
private function EnsureFloatVariable(string $deviceID, string $ident, string $name): int
{
$folderID = $this->GetDeviceFolder($deviceID);
foreach (IPS_GetChildrenIDs($folderID) as $cid) {
if (IPS_GetObject($cid)['ObjectIdent'] === $ident) {
return $cid;
}
}
$id = $this->RegisterVariableFloat($ident, $name);
IPS_SetParent($id, $folderID);
return $id;
}
private function EnsureStringVariable(string $deviceID, string $ident, string $name): int
{
$folderID = $this->GetDeviceFolder($deviceID);
foreach (IPS_GetChildrenIDs($folderID) as $cid) {
if (IPS_GetObject($cid)['ObjectIdent'] === $ident) {
return $cid;
}
}
$id = $this->RegisterVariableString($ident, $name);
IPS_SetParent($id, $folderID);
return $id;
}
/* ---------------------------------------------------------
* GERÄTE-ORDNER
* ---------------------------------------------------------*/
private function GetDeviceFolder(string $deviceID): int
{
$folderIdent = 'folder_' . $deviceID;
foreach (IPS_GetChildrenIDs($this->InstanceID) as $cid) {
if (IPS_GetObject($cid)['ObjectIdent'] === $folderIdent) {
return $cid;
}
}
$folderID = IPS_CreateCategory();
IPS_SetParent($folderID, $this->InstanceID);
IPS_SetName($folderID, $deviceID);
IPS_SetIdent($folderID, $folderIdent);
return $folderID;
}
/* ---------------------------------------------------------
* VARIABLE FINDEN
* ---------------------------------------------------------*/
private function FindVariableByIdent(string $Ident)
{
foreach (IPS_GetChildrenIDs($this->InstanceID) as $folder) {
foreach (IPS_GetChildrenIDs($folder) as $cid) {
if (IPS_GetObject($cid)['ObjectIdent'] === $Ident) {
return $cid;
}
}
}
return 0;
}
}
?>