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

328 lines
9.5 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 verbinden
$this->ConnectParent('{C6D2AEB3-6E1F-4B2E-8E69-3A1A00246850}');
}
public function ApplyChanges()
{
parent::ApplyChanges();
// Parent verbinden
$this->ConnectParent('{C6D2AEB3-6E1F-4B2E-8E69-3A1A00246850}');
// Niemals im ApplyChanges Publish/Subscribe senden!
$this->Subscribed = false;
}
/* ---------------------------------------------------------
* SICHER SENDEN
* ---------------------------------------------------------*/
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) { // IS_ACTIVE
return;
}
@ $this->SendDataToParent(json_encode($packet));
}
/* ---------------------------------------------------------
* DEBUG
* ---------------------------------------------------------*/
private function Log($title, $msg)
{
IPS_LogMessage("ShellyMQTT: $title", $msg);
$this->SendDebug($title, $msg, 0);
}
/* ---------------------------------------------------------
* SUBSCRIBE nur wenn der Parent sicher aktiv ist!
* ---------------------------------------------------------*/
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", "#");
}
/* ---------------------------------------------------------
* MQTT SEND
* ---------------------------------------------------------*/
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
* ---------------------------------------------------------*/
public function RequestAction($Ident, $Value)
{
$this->Log('RequestAction', "$Ident" . var_export($Value, true));
// Nur Outputs sind schaltbar
if (!str_contains($Ident, '_output_')) {
throw new Exception("Unknown Ident: $Ident");
}
// Lokale Variable aktualisieren
$varID = $this->FindVariableByIdent($Ident);
if ($varID > 0) {
SetValue($varID, $Value);
}
// deviceID und 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->Log('ReceiveData', $JSONString);
// Erst beim ersten Datenpaket subscriben
$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;
}
// 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');
$varID = $this->EnsureBooleanVariable($deviceID, $deviceID . '_online', 'Online');
SetValue($varID, $value);
}
/* ---------------------------------------------------------
* RPC
* ---------------------------------------------------------*/
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);
$typeID = $this->EnsureStringVariable($deviceID, $deviceID . '_type', 'Typ');
SetValue($typeID, $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) {
$tid = $this->EnsureFloatVariable($deviceID, $deviceID . '_temperature', 'Temperatur');
SetValue($tid, $mapped['temperature']);
}
}
/* ---------------------------------------------------------
* VARIABLEN ETC.
* ---------------------------------------------------------*/
private function EnsureBooleanVariable(string $deviceID, string $ident, string $name): int
{
$folder = $this->GetDeviceFolder($deviceID);
$vid = 0;
// Gibt es die Variable schon?
foreach (IPS_GetChildrenIDs($folder) as $cid) {
$obj = IPS_GetObject($cid);
if ($obj['ObjectIdent'] === $ident) {
$vid = $cid;
break;
}
}
// Neu anlegen, falls nicht vorhanden
if ($vid === 0) {
$vid = IPS_CreateVariable(0); // 0 = Boolean
IPS_SetName($vid, $name);
IPS_SetIdent($vid, $ident);
IPS_SetParent($vid, $folder);
}
// ---- HIER: nur für Outputs Action + Profil setzen ----
if (str_contains($ident, '_output_')) {
// Schaltprofil
IPS_SetVariableCustomProfile($vid, '~Switch');
// Aktion geht direkt an dieses Modul (RequestAction)
IPS_SetVariableCustomAction($vid, $this->InstanceID);
}
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)
{
foreach (IPS_GetChildrenIDs($this->InstanceID) as $folder) {
foreach (IPS_GetChildrenIDs($folder) as $cid) {
if (IPS_GetObject($cid)['ObjectIdent'] === $Ident) {
return $cid;
}
}
}
return 0;
}
}
?>