328 lines
9.5 KiB
PHP
328 lines
9.5 KiB
PHP
<?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;
|
||
}
|
||
}
|
||
|
||
?>
|