341 lines
10 KiB
PHP
341 lines
10 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-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;
|
||
}
|
||
}
|
||
|
||
?>
|