RegisterPropertyInteger("ParseNotifyCategoryID","0"); $this->RegisterPropertyInteger("ParseAlarmCategoryID","0"); $this->RegisterPropertyInteger("ParseAnalogCategoryID","0"); $this->RegisterPropertyInteger("ParseSetpointCategoryID",-1); $this->RegisterPropertyInteger("ParseSetpointCacheCategoryID",-1); $this->RegisterPropertyInteger("MqttCLientID","0"); $this->RegisterPropertyInteger("Projectnumber","230001"); $this->RegisterPropertyString("Projectname",""); $this->RegisterPropertyInteger("ISP","1"); $this->RegisterPropertyInteger("Projectyear","2023"); $this->RegisterPropertyInteger("Updatetime","20"); $this->RegisterPropertyInteger("ForceUpdatetime","30"); $this->RegisterTimer("Update", 0, 'SEMC_SendTopic('.$this->InstanceID.');'); $this->RegisterTimer("ForceUpdate", 0, 'SEMC_ForceSendTopic('.$this->InstanceID.');'); } // Überschreibt die intere IPS_ApplyChanges($id) Funktion public function ApplyChanges() { // Diese Zeile nicht löschen parent::ApplyChanges(); $this->SetTimerInterval("Update", $this->ReadPropertyInteger("Updatetime") * 1000); $this->SetTimerInterval("ForceUpdate", $this->ReadPropertyInteger("ForceUpdatetime") * 1000 * 60); } private function isMqttConnectionReady($instanceId, $logError = true) { $checked = array(); while ($instanceId > 0) { if (isset($checked[$instanceId]) || !IPS_InstanceExists($instanceId)) { return false; } $checked[$instanceId] = true; $instance = IPS_GetInstance($instanceId); if ($instance["InstanceStatus"] != 102) { if ($logError) { IPS_LogMessage("Monitoring Client", "MQTT connection is not ready: " . IPS_GetName($instanceId) . " (Status " . $instance["InstanceStatus"] . ")"); } return false; } $instanceId = isset($instance["ConnectionID"]) ? $instance["ConnectionID"] : 0; } return true; } private function getMqttClientSocketId($instanceId) { $checked = array(); while ($instanceId > 0 && !isset($checked[$instanceId]) && IPS_InstanceExists($instanceId)) { $checked[$instanceId] = true; $instance = IPS_GetInstance($instanceId); if (isset($instance["ModuleInfo"]["ModuleName"]) && $instance["ModuleInfo"]["ModuleName"] == "Client Socket") { return $instanceId; } $instanceId = isset($instance["ConnectionID"]) ? $instance["ConnectionID"] : 0; } return 0; } private function reconnectMqttClientSocket($serverId) { $socketId = $this->getMqttClientSocketId($serverId); if ($socketId === 0) { IPS_LogMessage("Monitoring Client", "No Client Socket found for the configured MQTT Client."); return false; } for ($attempt = 1; $attempt <= 20; $attempt++) { IPS_SetProperty($socketId, "Open", false); IPS_ApplyChanges($socketId); IPS_Sleep(100); IPS_SetProperty($socketId, "Open", true); IPS_ApplyChanges($socketId); IPS_Sleep(500); if ($this->isMqttConnectionReady($serverId, false)) { IPS_LogMessage("Monitoring Client", "MQTT Client Socket reconnected on attempt " . $attempt . "."); return true; } } IPS_LogMessage("Monitoring Client", "MQTT Client Socket could not be reconnected after 20 attempts."); return false; } /** * Die folgenden Funktionen stehen automatisch zur Verfügung, wenn das Modul über die "Module Control" eingefügt wurden. * Die Funktionen werden, mit dem selbst eingerichteten Prefix, in PHP und JSON-RPC wiefolgt zur Verfügung gestellt: * * DWM_SendMessage($id); * */ /* Publish a given payload over a MQTT server under a given topic without having to manually create a MQTT server device */ public function MqttPublish($server_id, $topic, $payload, $retain) { // ensure server instance exists if(!IPS_InstanceExists($server_id)) { return false; } if (!$this->isMqttConnectionReady($server_id)) { return false; } // convert array structure to json string if(is_array($payload)) $payload = json_encode($payload); // determine data type if(is_string($payload)) { $ips_var_type = 3; } else if(is_float($payload)) { $ips_var_type = 2; } else if(is_int($payload)) { $ips_var_type = 1; } else if(is_bool($payload)) { $ips_var_type = 0; } else { // unsupported return false; } $module_id = "{01C00ADD-D04E-452E-B66A-D253278743FE}" /* Module ID of MQTT Server Device */; $ident = "TempMQTTDevice"; // Each client instance owns its temporary device. Do not let different // Monitoring Client instances block each other during a force update. $semaphore = $ident . "_" . $this->InstanceID; // enter semaphore to ensure the temporary device gets used by one thread at a time if(IPS_SemaphoreEnter($semaphore, 5000)) { try { // get temporary MQTT Server Device or create if needed $id = @IPS_GetObjectIDByIdent($ident, $this->InstanceID); if($id === false) { $id = @IPS_CreateInstance($module_id); if($id === false) { return false; } IPS_SetParent($id, $this->InstanceID); IPS_SetIdent($id, $ident); } // ensure the specified server instance is actually compatible if(!IPS_IsInstanceCompatible($id, $server_id)) { return false; } // ensure that the temporary device is actually connected to the correct server instance $inst_config = IPS_GetInstance($id); if($inst_config["ConnectionID"] != $server_id) { IPS_DisconnectInstance($id); if(!@IPS_ConnectInstance($id, $server_id)) { return false; } } // name object to help with debugging IPS_SetName($id, "Temporary MQTT Device for: " . $topic); // configure temporary device $config_arr = array( "Retain" => $retain, "Topic" => $topic, "Type" => $ips_var_type ); $config_str = json_encode($config_arr); IPS_SetConfiguration($id, $config_str); IPS_SetHidden($id,true); IPS_ApplyChanges($id); if (!$this->isMqttConnectionReady($id)) { return false; } // get Value variable and use it to publish the payload $var_id = @IPS_GetObjectIDByIdent("Value", $id); if ($var_id === false) { IPS_LogMessage("Monitoring Client", "MQTT device has no Value variable for topic: " . $topic); return false; } RequestAction($var_id, $payload); return true; } finally { IPS_SemaphoreLeave($semaphore); } } else { // semaphore timeout IPS_LogMessage("Monitoring Client", "MQTT publish timed out waiting for topic: " . $topic); return false; } } // MQTT_Publish public function MqttSync($server_id, $topic, $payload, $retain, $ident, $source) { IPS_LogMessage("Monitoring Client", "MQTT Sync called with Topic: " . $topic . " Payload: " . $payload. " Source: " . $source . " Ident: " . $ident); $cacheCat = $this->ReadPropertyInteger("ParseSetpointCacheCategoryID"); if ($cacheCat < 1){ IPS_LogMessage("Monitoring Client", "No Cache Category defined for Setpoint Sync. Please define a Cache Category to use this feature."); return false; } if (IPS_CategoryExists($cacheCat) == false){ IPS_LogMessage("Monitoring Client", "Cache Category does not exist for Setpoint Sync."); return false; } if (IPS_VariableExists($source) == false){ IPS_LogMessage("Monitoring Client", "Source variable does not exist for Setpoint Sync."); return false; } // ensure server instance exists if(!IPS_InstanceExists($server_id)) { IPS_LogMessage("Monitoring Client", "Server instance does not exist for Setpoint Sync."); return false; } if (!$this->isMqttConnectionReady($server_id)) { return false; } // determine data type if(is_string($payload)) { $ips_var_type = 3; } else if(is_float($payload)) { $ips_var_type = 2; } else if(is_int($payload)) { $ips_var_type = 1; } else if(is_bool($payload)) { $ips_var_type = 0; } else { // unsupported return false; IPS_LogMessage("Monitoring Client", "Unsupported data type for Setpoint Sync."); } IPS_LogMessage("Monitoring Client", "Data type for Setpoint Sync determined as: " . $ips_var_type); $module_id = "{01C00ADD-D04E-452E-B66A-D253278743FE}" /* Module ID of MQTT Server Device */; // get temporary MQTT Server Device or create if needed $id = @IPS_GetObjectIDByIdent($ident, $cacheCat); if($id === false) { $id = @IPS_CreateInstance($module_id); if($id === false) { return false; } IPS_SetParent($id, $cacheCat); IPS_SetIdent($id, $ident); // name object to help with debugging IPS_SetName($id, "CS_" . $ident); // configure temporary device $config_arr = array( "Retain" => $retain, "Topic" => $topic, "Type" => $ips_var_type ); $config_str = json_encode($config_arr); IPS_SetConfiguration($id, $config_str); IPS_ApplyChanges($id); } // ensure the specified server instance is actually compatible if(!IPS_IsInstanceCompatible($id, $server_id)) { return false; IPS_LogMessage("Monitoring Client", "Device is not compatible with server instance for Setpoint Sync."); } $inst_config = IPS_GetInstance($id); if($inst_config["ConnectionID"] != $server_id) { IPS_DisconnectInstance($id); if(!@IPS_ConnectInstance($id, $server_id)) { return false; IPS_LogMessage("Monitoring Client", "Could not connect device to server instance for Setpoint Sync."); } } if (!$this->isMqttConnectionReady($id)) { return false; } IPS_LogMessage("Monitoring Client", "Parentid was :".$id); $id = IPS_GetChildrenIDs($id)[0]; IPS_LogMessage("Monitoring Client", "ID was :".$id); $valSelf = GetValue($id); $valSource = GetValue($source); IPS_LogMessage("Monitoring Client", "Self value: " . $valSelf . " Source value: " . $valSource); $varInfoSelf = IPS_GetVariable($id); $changedtimeSelf = $varInfoSelf["VariableChanged"]; $varInfoSource = IPS_GetVariable($source); $changedtimeSource = $varInfoSource["VariableChanged"]; if($valSelf != $valSource) { //self is newer than source, so update source with self value if ($changedtimeSelf > $changedtimeSource) { RequestAction($source, $valSelf); IPS_LogMessage("Monitoring Client", "Source variable updated with self value for Setpoint Sync. Source: " . $source . " Value: " . $valSelf); } //source is newer than self, so update self with source value if ($changedtimeSource > $changedtimeSelf) { RequestAction($id, $valSource); IPS_LogMessage("Monitoring Client", "Self variable updated with source value for Setpoint Sync. Self: " . $id . " Value: " . $valSource); } } return true; } // MQTT_Sync private function resolveLinkTarget($objectId) { $visited = array(); while ($objectId > 0 && IPS_LinkExists($objectId)) { if (isset($visited[$objectId])) { return 0; } $visited[$objectId] = true; $objectId = IPS_GetLink($objectId)["TargetID"]; } return $objectId; } /* * Return every variable directly represented by a monitoring category. * The parent name is deliberately kept from the visible object/link in the * configured tree, so existing MQTT topic names remain unchanged. */ private function getCategoryVariables($categoryId) { $variables = array(); // Unconfigured client categories use -1. IPS_CategoryExists() rejects // negative IDs before it can return false. if ($categoryId < 1 || !IPS_CategoryExists($categoryId)) { return $variables; } foreach (IPS_GetChildrenIDs($categoryId) as $entryId) { $entryName = IPS_GetName($entryId); $targetId = $this->resolveLinkTarget($entryId); if ($targetId <= 0 || !IPS_ObjectExists($targetId)) { continue; } // A variable or a link to a variable is located directly in the category. if (IPS_VariableExists($targetId)) { $variables[] = array( "id" => $targetId, "parent" => $entryName, "name" => IPS_GetName($targetId), "direct" => true ); continue; } // An object (or a link to an object) may contain one or several values. foreach (IPS_GetChildrenIDs($targetId) as $childId) { $variableId = $this->resolveLinkTarget($childId); if ($variableId <= 0 || !IPS_VariableExists($variableId)) { continue; } $variables[] = array( "id" => $variableId, "parent" => $entryName, "name" => IPS_GetName($variableId), "direct" => false ); } } return $variables; } private function getTopicForVariable($top, $variable, $force) { // The old force path used the bare variable name for values directly in // the category. Keep that legacy topic spelling intact. if ($force && $variable["direct"]) { return $top . $variable["name"]; } return $top . $variable["parent"] . "_" . $variable["name"]; } public function SendTopic() { $mqttId = $this->ReadPropertyInteger("MqttCLientID"); $projectyear = $this->ReadPropertyInteger("Projectyear"); $projectnumber = $this->ReadPropertyInteger("Projectnumber"); $projectname = $this->ReadPropertyString("Projectname"); $ispnumber = $this->ReadPropertyInteger("ISP"); $updatetime = $this->ReadPropertyInteger("Updatetime"); $catIds = array( array("id" => $this->ReadPropertyInteger("ParseNotifyCategoryID"), "top" => "Projekte" . $projectyear . "/P" . $projectnumber . "/ISP" . $ispnumber . "/Notify/", "setpoint" => false), array("id" => $this->ReadPropertyInteger("ParseAlarmCategoryID"), "top" => "Projekte" . $projectyear . "/P" . $projectnumber . "/ISP" . $ispnumber . "/Alarm/", "setpoint" => false), array("id" => $this->ReadPropertyInteger("ParseAnalogCategoryID"), "top" => "Projekte" . $projectyear . "/P" . $projectnumber . "/ISP" . $ispnumber . "/Analog/", "setpoint" => false), array("id" => $this->ReadPropertyInteger("ParseSetpointCategoryID"), "top" => "Projekte" . $projectyear . "/P" . $projectnumber . "/ISP" . $ispnumber . "/Setpoint/", "setpoint" => true) ); foreach ($catIds as $catId) { foreach ($this->getCategoryVariables($catId["id"]) as $variable) { $varInfo = IPS_GetVariable($variable["id"]); $changedtime = $varInfo["VariableChanged"]; $topic = $this->getTopicForVariable($catId["top"], $variable, false); $payload = round(GetValue($variable["id"]), 2); if ($catId["setpoint"]) { $syncname = $variable["parent"] . "_" . $variable["name"]; IPS_LogMessage("Monitoring Client", "Sync Setpoint: " . $topic . " mit Payload: " . $payload . " und Source: " . $variable["id"] . " und Ident: " . $syncname); $this->MqttSync($mqttId, $topic, $payload, false, $syncname, $variable["id"]); } elseif ($changedtime > time() - $updatetime) { $this->MqttPublish($mqttId, $topic, $payload, false); } } } $topic = "Projekte" . $projectyear . "/" . $projectnumber . "/ISP" . $ispnumber . "/Name"; $this->MqttPublish($mqttId, $topic, $projectname, false); } public function ForceSendTopic() { $mqttId = $this->ReadPropertyInteger("MqttCLientID"); $projectyear = $this->ReadPropertyInteger("Projectyear"); $projectnumber = $this->ReadPropertyInteger("Projectnumber"); $projectname = $this->ReadPropertyString("Projectname"); $ispnumber = $this->ReadPropertyInteger("ISP"); // A manual force update starts with a clean MQTT socket connection. // Normal timer-driven sends do not interrupt the connection. if (!$this->reconnectMqttClientSocket($mqttId)) { return; } $catIds = array( array("id" => $this->ReadPropertyInteger("ParseNotifyCategoryID"), "top" => "Projekte" . $projectyear . "/P" . $projectnumber . "/ISP" . $ispnumber . "/Notify/"), array("id" => $this->ReadPropertyInteger("ParseAlarmCategoryID"), "top" => "Projekte" . $projectyear . "/P" . $projectnumber . "/ISP" . $ispnumber . "/Alarm/"), array("id" => $this->ReadPropertyInteger("ParseAnalogCategoryID"), "top" => "Projekte" . $projectyear . "/P" . $projectnumber . "/ISP" . $ispnumber . "/Analog/"), array("id" => $this->ReadPropertyInteger("ParseSetpointCategoryID"), "top" => "Projekte" . $projectyear . "/P" . $projectnumber . "/ISP" . $ispnumber . "/Setpoint/") ); foreach ($catIds as $catId) { foreach ($this->getCategoryVariables($catId["id"]) as $variable) { $topic = $this->getTopicForVariable($catId["top"], $variable, true); $payload = round(GetValue($variable["id"]), 2); if (!$this->MqttPublish($mqttId, $topic, $payload, false)) { return; } } } $topic = "Projekte" . $projectyear . "/" . $projectnumber . "/ISP" . $ispnumber . "/Name"; $this->MqttPublish($mqttId, $topic, $projectname, false); } } ?>