Files
Symcon_Belevo_Energiemanage…/Shelly_Parser_MQTT/module.php
2025-11-24 11:22:40 +01:00

341 lines
10 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
{
private bool $Subscribed = false;
public function Create()
{
parent::Create();
// MQTT-Server
$this->ConnectParent('{C6D2AEB3-6E1F-4B2E-8E69-3A1A00246850}');
}
public function ApplyChanges()
{
parent::ApplyChanges();
$this->ConnectParent('{C6D2AEB3-6E1F-4B2E-8E69-3A1A00246850}');
// Subscribe wird erst beim ersten Paket ausgeführt
$this->Subscribed = false;
}
/* ---------------------------------------------------------
* DEBUG SHORTCUT
* ---------------------------------------------------------*/
private function Log($title, $msg)
{
$this->SendDebug($title, $msg, 0);
IPS_LogMessage("ShellyMQTT/$title", $msg);
}
/* ---------------------------------------------------------
* Sicher Senden (Parent OK?)
* ---------------------------------------------------------*/
private function SafeSend(array $packet)
{
$parent = @IPS_GetInstance($this->InstanceID)['ConnectionID'] ?? 0;
if ($parent === 0 || !IPS_InstanceExists($parent)) {
return;
}
if (IPS_GetInstance($parent)['InstanceStatus'] !== 102) {
return;
}
@$this->SendDataToParent(json_encode($packet));
}
/* ---------------------------------------------------------
* Subscribe once
* ---------------------------------------------------------*/
private function EnsureSubscribe()
{
if ($this->Subscribed) {
return;
}
$this->Subscribed = true;
$this->SafeSend([
'DataID' => '{043EA491-0325-4ADD-8FC2-A30C8EEB4D3F}',
'PacketType' => 8,
'QualityOfService' => 0,
'Retain' => false,
'Topic' => '#',
'Payload' => ''
]);
$this->Log("Subscribe", "Subscribed to #");
}
/* ---------------------------------------------------------
* MQTT Publish
* ---------------------------------------------------------*/
private function Publish(string $topic, string $payload)
{
$this->SafeSend([
'DataID' => '{043EA491-0325-4ADD-8FC2-A30C8EEB4D3F}',
'PacketType' => 3,
'QualityOfService' => 0,
'Retain' => false,
'Topic' => $topic,
'Payload' => $payload
]);
$this->Log("Publish", "$topic$payload");
}
/* ---------------------------------------------------------
* REQUEST ACTION (Schalten Shelly Output)
* ---------------------------------------------------------*/
public function RequestAction($Ident, $Value)
{
$this->Log("RequestAction", "$Ident" . json_encode($Value));
if (!str_contains($Ident, '_output_')) {
throw new Exception("Unknown Ident: $Ident");
}
// Lokale Variable setzen
$varID = $this->FindVariableByIdent($Ident);
if ($varID > 0) {
SetValue($varID, $Value);
}
// deviceID / Index extrahieren
[$deviceID, $suffix] = explode('_output_', $Ident);
$index = intval($suffix);
$topic = "$deviceID/rpc";
$payload = json_encode([
'id' => 1,
'src' => 'ips',
'method' => 'Switch.Set',
'params' => [
'id' => $index,
'on' => (bool)$Value
]
]);
$this->Publish($topic, $payload);
}
/* ---------------------------------------------------------
* RECEIVE DATA
* ---------------------------------------------------------*/
public function ReceiveData($JSONString)
{
$this->EnsureSubscribe();
$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
if (($parts[1] ?? '') === 'online') {
$this->HandleOnline($deviceID, $payload);
return;
}
// Events RPC
if (($parts[1] ?? '') === 'events' && ($parts[2] ?? '') === 'rpc') {
$this->HandleRPC($deviceID, $payload);
return;
}
}
/* ---------------------------------------------------------
* ONLINE
* ---------------------------------------------------------*/
private function HandleOnline(string $deviceID, string $payload)
{
$value = ($payload === 'true' || $payload === '1');
$vid = $this->EnsureBooleanVariable($deviceID, $deviceID . '_online', 'Online');
SetValue($vid, $value);
}
/* ---------------------------------------------------------
* RPC Handler
* ---------------------------------------------------------*/
private function HandleRPC(string $deviceID, string $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);
$tid = $this->EnsureStringVariable($deviceID, $deviceID . '_type', 'Typ');
SetValue($tid, $type);
$params = $json['params'] ?? [];
$mapped = ShellyParser::MapParams($params);
// Outputs
foreach ($mapped['outputs'] as $i => $val) {
$vid = $this->EnsureBooleanVariable($deviceID, "{$deviceID}_output_$i", "Output $i");
SetValue($vid, $val);
}
// Inputs
foreach ($mapped['inputs'] as $i => $val) {
$vid = $this->EnsureBooleanVariable($deviceID, "{$deviceID}_input_$i", "Input $i");
SetValue($vid, $val);
}
// Temperatur
if ($mapped['temperature'] !== null) {
$f = $this->EnsureFloatVariable($deviceID, "{$deviceID}_temperature", "Temperatur");
SetValue($f, $mapped['temperature']);
}
}
/* ---------------------------------------------------------
* VARIABLEN MIT AUTOMATISCHER REPARATUR
* ---------------------------------------------------------*/
private function EnsureBooleanVariable(string $deviceID, string $ident, string $name): int
{
$folder = $this->GetDeviceFolder($deviceID);
$vid = 0;
foreach (IPS_GetChildrenIDs($folder) as $cid) {
$obj = IPS_GetObject($cid);
// Auto-Repair für alte Versionen ohne Ident
if ($obj['ObjectName'] === $name && $obj['ObjectIdent'] === '') {
IPS_SetIdent($cid, $ident);
$vid = $cid;
break;
}
if ($obj['ObjectIdent'] === $ident) {
$vid = $cid;
break;
}
}
if ($vid === 0) {
$vid = IPS_CreateVariable(0);
IPS_SetName($vid, $name);
IPS_SetIdent($vid, $ident);
IPS_SetParent($vid, $folder);
}
// OUTPUT: Switch-Profil + ActionScript
if (str_contains($ident, '_output_')) {
IPS_SetVariableCustomProfile($vid, '~Switch');
// Action Script suchen / erstellen
$scriptIdent = $ident . "_action";
$scriptID = @IPS_GetObjectIDByIdent($scriptIdent, $folder);
if ($scriptID === false) {
$scriptID = IPS_CreateScript(0); // 0 = PHP Script
IPS_SetParent($scriptID, $folder);
IPS_SetIdent($scriptID, $scriptIdent);
IPS_SetName($scriptID, $name . " Action");
$code = <<<EOF
<?php
\$module = IPS_GetParent(\$_IPS['SELF']);
RequestAction(\$module, \$_IPS['VARIABLE'], \$_IPS['VALUE']);
?>
EOF;
IPS_SetScriptContent($scriptID, $code);
}
// Variable zeigt auf dieses Action-Script
IPS_SetVariableCustomAction($vid, $scriptID);
}
return $vid;
}
private function EnsureFloatVariable(string $deviceID, string $ident, string $name): int
{
$folder = $this->GetDeviceFolder($deviceID);
foreach (IPS_GetChildrenIDs($folder) as $cid) {
if (IPS_GetObject($cid)['ObjectIdent'] === $ident) {
return $cid;
}
}
$vid = $this->RegisterVariableFloat($ident, $name);
IPS_SetParent($vid, $folder);
return $vid;
}
private function EnsureStringVariable(string $deviceID, string $ident, string $name): int
{
$folder = $this->GetDeviceFolder($deviceID);
foreach (IPS_GetChildrenIDs($folder) as $cid) {
if (IPS_GetObject($cid)['ObjectIdent'] === $ident) {
return $cid;
}
}
$vid = $this->RegisterVariableString($ident, $name);
IPS_SetParent($vid, $folder);
return $vid;
}
private function GetDeviceFolder(string $deviceID): int
{
$ident = 'folder_' . $deviceID;
foreach (IPS_GetChildrenIDs($this->InstanceID) as $cid) {
if (IPS_GetObject($cid)['ObjectIdent'] === $ident) {
return $cid;
}
}
$fid = IPS_CreateCategory();
IPS_SetName($fid, $deviceID);
IPS_SetIdent($fid, $ident);
IPS_SetParent($fid, $this->InstanceID);
return $fid;
}
private function FindVariableByIdent(string $Ident): int
{
foreach (IPS_GetChildrenIDs($this->InstanceID) as $folder) {
foreach (IPS_GetChildrenIDs($folder) as $cid) {
if (IPS_GetObject($cid)['ObjectIdent'] === $Ident) {
return $cid;
}
}
}
return 0;
}
}
?>