From 3c3ad7efc1b27a728e8b2631fae5d7e65f36c6f7 Mon Sep 17 00:00:00 2001 From: jhartworks Date: Thu, 16 Apr 2026 15:49:14 +0200 Subject: [PATCH] fix: refactor CounterServer to CounterClient and update property registrations for improved functionality --- CounterServer/module.php | 701 ++++++++++++++++++++++++++++++++------- 1 file changed, 582 insertions(+), 119 deletions(-) diff --git a/CounterServer/module.php b/CounterServer/module.php index a5ef0ef..1197cfb 100644 --- a/CounterServer/module.php +++ b/CounterServer/module.php @@ -1,17 +1,21 @@ RegisterPropertyInteger('Connector', 0); - $this->RegisterPropertyInteger('SourceJSON', 0); + $this->RegisterPropertyInteger('CounterCategoryID', 0); + $this->RegisterPropertyInteger('JsonOutputVariableID', 0); $this->RegisterPropertyInteger('UpdateTime', 60); - $this->RegisterPropertyString('Projectname', 60); - $this->RegisterPropertyInteger('ProjectIdPortal', -1); - $this->RegisterTimer('Update', 0, 'SECS_checkTable('.$this->InstanceID.');'); + $this->RegisterPropertyBoolean('EnableMQTT', false); + $this->RegisterPropertyInteger('MqttClientID', 0); + $this->RegisterPropertyString('Projectname', ''); + $this->RegisterPropertyInteger('Projectyear', 2026); + $this->RegisterPropertyInteger('Projectnumber', 0); + + $this->RegisterTimer('Update', 0, 'SECC_BuildAndStorePayload(' . $this->InstanceID . ');'); } public function ApplyChanges() @@ -24,148 +28,607 @@ class CounterServer extends IPSModule } $this->SetTimerInterval('Update', $updateTime * 1000); - $this->checkTable(); } - public function checkTable() + public function BuildAndStorePayload() { - $sqlId = $this->ReadPropertyInteger('Connector'); - $jsonVar = $this->ReadPropertyInteger('SourceJSON'); - - if ($sqlId <= 0 || $jsonVar <= 0) { - IPS_LogMessage('CounterTable', 'Config fehlt.'); + $json = $this->BuildPayload(); + if ($json === '') { return; } - $jsonString = GetValueString($jsonVar); - if ($jsonString === '') { - return; + $targetVarId = $this->ReadPropertyInteger('JsonOutputVariableID'); + if ($targetVarId > 0 && IPS_VariableExists($targetVarId)) { + SetValueString($targetVarId, $json); } - $payload = json_decode($jsonString, true); - if (!is_array($payload)) { - IPS_LogMessage('CounterTable', 'JSON Fehler'); - return; + IPS_LogMessage('CounterClient', $json); + + if ($this->ReadPropertyBoolean('EnableMQTT')) { + $mqttClientId = $this->ReadPropertyInteger('MqttClientID'); + $mqttTopic = 'Projekte' . $this->ReadPropertyInteger('Projectyear') . '/P' . $this->ReadPropertyInteger('Projectnumber') . '/Counters'; + + if ($mqttClientId > 0 && IPS_InstanceExists($mqttClientId) && $mqttTopic !== '') { + $this->MqttPublish($mqttClientId, $mqttTopic, $json, true); + } else { + IPS_LogMessage('CounterClient', 'MQTT ist aktiviert, aber ungültige MQTT Client ID oder Topic.'); + } + } + } + + public function BuildPayload() + { + $counterCategoryId = $this->ReadPropertyInteger('CounterCategoryID'); + if ($counterCategoryId <= 0 || !IPS_ObjectExists($counterCategoryId)) { + IPS_LogMessage('CounterClient', 'Ungültige CounterCategoryID'); + return ''; } - $clientId = (string)$this->InstanceID; - $clientName = IPS_GetName($this->InstanceID); - $projectName = $this->ReadPropertyString('Projectname'); - $timestamp = $payload['timestamp'] ?? date('Y-m-d H:i:s'); - $counters = $payload['counters'] ?? []; - $projectId = $this->ReadPropertyInteger('ProjectIdPortal'); + $counterIds = IPS_GetChildrenIDs($counterCategoryId); - if ($projectId !== -1) { - foreach ($counters as $c) { + $payload = [ + 'timestamp' => date('Y-m-d H:i:s'), + 'counters' => [] + ]; - $counterId = $c['id'] ?? ''; - if ($counterId === '') continue; - - $counterName = $c['name'] ?? $counterId; - $type = $c['type'] ?? 'other'; - $unit = $c['unit'] ?? 'kWh'; - - - // ---------- UPSERT ---------- - $sql = " - INSERT INTO meter_devices - (project_id, counter_id, meter_uuid, external_id, external_name, meter_type, billing_unit, is_active) - VALUES - ( - '" . $this->esc($projectId) . "', - '" . $this->esc($counterId) . "', - '" . hash('sha256', $projectId.$clientId . '_' . $counterId) . "', - '" . $this->esc($clientId) . "', - '" . $this->esc($projectName.'_'. $counterId) . "', - '" . $this->esc($type) . "', - '" . $this->esc($unit) . "', - 1 - ) - ON DUPLICATE KEY UPDATE - meter_name = VALUES(meter_name), - meter_type = VALUES(meter_type), - billing_unit = VALUES(billing_unit), - is_active = 1 - "; - - - MySQL_ExecuteSimple($sqlId, $sql); - - - // ---------- GET meter_id ---------- - $sqlGet = " - SELECT id FROM meter_devices - WHERE meter_uuid = '" . hash('sha256', $projectId.$clientId . '_' . $counterId) . "' - LIMIT 1 - "; - - $res = MySQL_ExecuteSimple($sqlId, $sqlGet); - - if (!is_array($res) || count($res) === 0) { - IPS_LogMessage('CounterTable', 'Kein meter_id gefunden'); + foreach ($counterIds as $counterObjectId) { + if (!IPS_ObjectExists($counterObjectId)) { continue; } - $meterId = $res[0]->id; + $obj = IPS_GetObject($counterObjectId); + if ((int)$obj['ObjectType'] !== 0) { + continue; + } - // ---------- INSERT measurement ---------- - $sqlInsert = " - INSERT INTO meter_measurements - ( - meter_id, measured_at, - total_value, consumption_value, - power_value, flow_value, - current_value, voltage_value, - temperature_in_value, temperature_out_value, - pressure_in_value, pressure_out_value, - frequency_value, power_factor - ) - VALUES - ( - " . $meterId . ", - '" . $this->esc($timestamp) . "', - " . $this->f($c, 'total_value') . ", - " . $this->f($c, 'consumption_value') . ", - " . $this->f($c, 'power_value') . ", - " . $this->f($c, 'flow_value') . ", - " . $this->f($c, 'current_value') . ", - " . $this->f($c, 'voltage_value') . ", - " . $this->f($c, 'temperature_in_value') . ", - " . $this->f($c, 'temperature_out_value') . ", - " . $this->f($c, 'pressure_in_value') . ", - " . $this->f($c, 'pressure_out_value') . ", - " . $this->f($c, 'frequency_value') . ", - " . $this->f($c, 'power_factor') . " - ) - "; + $counterName = IPS_GetName($counterObjectId); + $counterId = $this->makeSlug($counterName); - MySQL_ExecuteSimple($sqlId, $sqlInsert); + $entries = $this->CollectVariablesRecursive($counterObjectId, [], []); + IPS_LogMessage('CounterClient', 'Counter "' . $counterName . '" -> gefundene Variablen: ' . count($entries)); + + if (count($entries) === 0) { + continue; + } + + $counterData = [ + 'id' => $counterId, + 'name' => $counterName + ]; + + $tempSlots = []; + $pressureSlots = []; + $recognizedCount = 0; + + foreach ($entries as $entry) { + $classified = $this->ClassifyEntry($entry); + if ($classified === null) { + IPS_LogMessage('CounterClient', 'Ignoriert: ' . $entry['context']); + continue; + } + + $recognizedCount++; + + $field = $classified['field']; + $value = $classified['value']; + $unit = $classified['unit']; + + IPS_LogMessage('CounterClient', 'Erkannt: ' . $entry['context'] . ' => ' . $field . ' = ' . $value . ' [' . $unit . ']'); + + if ($field === 'temperature_auto') { + $tempSlots[] = $classified; + continue; + } + + if ($field === 'pressure_auto') { + $pressureSlots[] = $classified; + continue; + } + + $counterData[$field] = $value; + + if ($field === 'total_value' && !isset($counterData['unit']) && $unit !== '') { + $counterData['unit'] = $unit; + } + } + + $this->assignInOutValues($counterData, $tempSlots, 'temperature_in_value', 'temperature_out_value'); + $this->assignInOutValues($counterData, $pressureSlots, 'pressure_in_value', 'pressure_out_value'); + + $contextString = mb_strtolower($counterName); + foreach ($entries as $entry) { + $contextString .= ' ' . $entry['context']; + } + + $counterData['type'] = $this->DetectCounterType($counterData, $contextString); + + if (!isset($counterData['unit'])) { + $counterData['unit'] = $this->inferDefaultUnit($counterData); + } + + if ($recognizedCount > 0) { + $payload['counters'][] = $counterData; + } else { + IPS_LogMessage('CounterClient', 'Counter ohne erkannte Messwerte: ' . $counterName); + } } - } else { - IPS_LogMessage('CounterTable', 'Keine Project-ID für Portal angegeben.'); + + return json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + + private function CollectVariablesRecursive(int $objectId, array $path, array $visited): array + { + $result = []; + + if (isset($visited[$objectId])) { + return $result; + } + $visited[$objectId] = true; + + if (!IPS_ObjectExists($objectId)) { + return $result; + } + + if (IPS_LinkExists($objectId)) { + $link = IPS_GetLink($objectId); + $targetId = (int)$link['TargetID']; + $linkName = IPS_GetName($objectId); + + if ($targetId > 0 && IPS_ObjectExists($targetId)) { + $newPath = $path; + if ($linkName !== '') { + $newPath[] = $linkName; + } + + return $this->CollectVariablesRecursive($targetId, $newPath, $visited); + } + + return $result; + } + + if (IPS_VariableExists($objectId)) { + $result[] = [ + 'varId' => $objectId, + 'context' => $this->buildContextString($path, IPS_GetName($objectId)) + ]; + return $result; + } + + $obj = IPS_GetObject($objectId); + $objName = IPS_GetName($objectId); + + $newPath = $path; + if ($objName !== '') { + $newPath[] = $objName; + } + + $children = IPS_GetChildrenIDs($objectId); + foreach ($children as $childId) { + $sub = $this->CollectVariablesRecursive($childId, $newPath, $visited); + foreach ($sub as $entry) { + $result[] = $entry; + } + } + + return $result; + } + + private function ClassifyEntry(array $entry): ?array + { + $varId = (int)$entry['varId']; + + if (!IPS_VariableExists($varId)) { + return null; + } + + $var = IPS_GetVariable($varId); + $type = (int)$var['VariableType']; + + if ($type !== 1 && $type !== 2) { + return null; + } + + $profileName = ''; + if ($var['VariableCustomProfile'] !== '') { + $profileName = $var['VariableCustomProfile']; + } elseif ($var['VariableProfile'] !== '') { + $profileName = $var['VariableProfile']; + } + + $unit = ''; + if ($profileName !== '' && IPS_VariableProfileExists($profileName)) { + $profile = IPS_GetVariableProfile($profileName); + $suffix = trim((string)$profile['Suffix']); + $prefix = trim((string)$profile['Prefix']); + $unit = trim($prefix . ' ' . $suffix); + } + + if ($unit === '') { + return null; + } + + $value = GetValue($varId); + if (!is_numeric($value)) { + return null; + } + + $value = round((float)$value, 3); + $context = mb_strtolower($entry['context']); + $unitNorm = $this->normalizeUnit($unit); + + if ($this->isTotalUnit($unitNorm, $context)) { + return [ + 'field' => 'total_value', + 'value' => $value, + 'unit' => $this->formatOutputUnit($unitNorm), + 'name' => $context + ]; + } + + if ($this->isPowerUnit($unitNorm, $context)) { + return [ + 'field' => 'power_value', + 'value' => $value, + 'unit' => $unitNorm, + 'name' => $context + ]; + } + + if ($this->isFlowUnit($unitNorm, $context)) { + return [ + 'field' => 'flow_value', + 'value' => $value, + 'unit' => $unitNorm, + 'name' => $context + ]; + } + + if ($unitNorm === 'a') { + return [ + 'field' => 'current_value', + 'value' => $value, + 'unit' => $unitNorm, + 'name' => $context + ]; + } + + if ($unitNorm === 'v') { + return [ + 'field' => 'voltage_value', + 'value' => $value, + 'unit' => $unitNorm, + 'name' => $context + ]; + } + + if ($unitNorm === 'hz') { + return [ + 'field' => 'frequency_value', + 'value' => $value, + 'unit' => $unitNorm, + 'name' => $context + ]; + } + + if (in_array($unitNorm, ['bar', 'mbar', 'pa'], true)) { + if ($this->looksLikeIn($context)) { + return [ + 'field' => 'pressure_in_value', + 'value' => $value, + 'unit' => $unitNorm, + 'name' => $context + ]; + } + + if ($this->looksLikeOut($context)) { + return [ + 'field' => 'pressure_out_value', + 'value' => $value, + 'unit' => $unitNorm, + 'name' => $context + ]; + } + + return [ + 'field' => 'pressure_auto', + 'value' => $value, + 'unit' => $unitNorm, + 'name' => $context + ]; + } + + if (in_array($unitNorm, ['°c', 'c', 'k'], true)) { + if ($this->looksLikeIn($context)) { + return [ + 'field' => 'temperature_in_value', + 'value' => $value, + 'unit' => $unitNorm, + 'name' => $context + ]; + } + + if ($this->looksLikeOut($context)) { + return [ + 'field' => 'temperature_out_value', + 'value' => $value, + 'unit' => $unitNorm, + 'name' => $context + ]; + } + + return [ + 'field' => 'temperature_auto', + 'value' => $value, + 'unit' => $unitNorm, + 'name' => $context + ]; + } + + if ($unitNorm === 'cos' || $unitNorm === 'cosphi' || $unitNorm === 'pf') { + return [ + 'field' => 'power_factor', + 'value' => $value, + 'unit' => $unitNorm, + 'name' => $context + ]; + } + + return null; + } + + private function assignInOutValues(array &$counterData, array $items, string $fieldIn, string $fieldOut): void + { + foreach ($items as $item) { + if ($this->looksLikeIn($item['name']) && !isset($counterData[$fieldIn])) { + $counterData[$fieldIn] = $item['value']; + continue; + } + + if ($this->looksLikeOut($item['name']) && !isset($counterData[$fieldOut])) { + $counterData[$fieldOut] = $item['value']; + continue; + } + } + + foreach ($items as $item) { + if (!isset($counterData[$fieldIn])) { + $counterData[$fieldIn] = $item['value']; + continue; + } + + if (!isset($counterData[$fieldOut])) { + $counterData[$fieldOut] = $item['value']; + continue; + } } } - private function f($arr, $key) + private function DetectCounterType(array $counterData, string $context): string { - if (!isset($arr[$key]) || $arr[$key] === '' || $arr[$key] === null) { - return 'NULL'; + $unit = isset($counterData['unit']) ? $this->normalizeUnit((string)$counterData['unit']) : ''; + $ctx = mb_strtolower($context); + + if (isset($counterData['voltage_value']) || isset($counterData['current_value']) || isset($counterData['frequency_value']) || isset($counterData['power_factor'])) { + return 'electricity'; } - if (is_numeric($arr[$key])) { - return str_replace(',', '.', (string)$arr[$key]); + if (isset($counterData['temperature_in_value']) || isset($counterData['temperature_out_value'])) { + if (strpos($ctx, 'kalt') !== false || strpos($ctx, 'cool') !== false || strpos($ctx, 'kaelte') !== false || strpos($ctx, 'kälte') !== false) { + return 'cooling'; + } + return 'heat'; } - return 'NULL'; + if ($unit === 'm3' || $unit === 'l') { + if (strpos($ctx, 'gas') !== false) { + return 'gas'; + } + return 'water'; + } + + if ($unit === 'kwh' || $unit === 'kw/h' || $unit === 'mwh' || $unit === 'wh') { + if (isset($counterData['flow_value']) || strpos($ctx, 'waerme') !== false || strpos($ctx, 'wärme') !== false || strpos($ctx, 'warmwasser') !== false || strpos($ctx, 'heizung') !== false) { + return 'heat'; + } + return 'electricity'; + } + + if (strpos($ctx, 'gas') !== false) { + return 'gas'; + } + + return 'other'; } - private function esc($v) + private function inferDefaultUnit(array $counterData): string { - return str_replace( - ['\\', "'", '"'], - ['\\\\', "\\'", '\\"'], - $v - ); + if (isset($counterData['total_value'])) { + if ($counterData['type'] === 'water' || $counterData['type'] === 'gas') { + return 'm³'; + } + return 'kWh'; + } + + return ''; + } + + private function isTotalUnit(string $unitNorm, string $context): bool + { + if (in_array($unitNorm, ['kwh', 'wh', 'mwh', 'm3', 'l', 'kw/h', 'w/h', 'mw/h'], true)) { + return true; + } + + if (strpos($context, 'energy') !== false && in_array($unitNorm, ['kw', 'w', 'mw'], true)) { + return true; + } + + if (strpos($context, 'volume') !== false && in_array($unitNorm, ['m3', 'l'], true)) { + return true; + } + + return false; + } + + private function isPowerUnit(string $unitNorm, string $context): bool + { + if (in_array($unitNorm, ['kw', 'w', 'mw'], true)) { + if (strpos($context, 'energy') !== false) { + return false; + } + return true; + } + + return false; + } + + private function isFlowUnit(string $unitNorm, string $context): bool + { + if (in_array($unitNorm, ['m3/h', 'l/h', 'l/min'], true)) { + return true; + } + + if (strpos($context, 'flow') !== false && in_array($unitNorm, ['m3', 'l'], true)) { + return true; + } + + return false; + } + + private function formatOutputUnit(string $unitNorm): string + { + if ($unitNorm === 'm3') { + return 'm³'; + } + if ($unitNorm === 'kwh') { + return 'kWh'; + } + if ($unitNorm === 'kw/h') { + return 'kW/h'; + } + return $unitNorm; + } + + private function looksLikeIn(string $name): bool + { + $name = mb_strtolower($name); + + return strpos($name, 'vorlauf') !== false + || strpos($name, 'vl') !== false + || strpos($name, 'flow') !== false + || strpos($name, 'ein') !== false + || strpos($name, 'iv') !== false + || strpos($name, 'inlet') !== false; + } + + private function looksLikeOut(string $name): bool + { + $name = mb_strtolower($name); + + return strpos($name, 'ruecklauf') !== false + || strpos($name, 'rücklauf') !== false + || strpos($name, 'rl') !== false + || strpos($name, 'return') !== false + || strpos($name, 'aus') !== false + || strpos($name, 'ri') !== false + || strpos($name, 'outlet') !== false; + } + + private function normalizeUnit(string $unit): string + { + $unit = mb_strtolower(trim($unit)); + $unit = str_replace(["\xc2\xa0", ' '], '', $unit); + $unit = str_replace(['m³', '㎥'], 'm3', $unit); + return $unit; + } + + private function makeSlug(string $text): string + { + $text = mb_strtolower($text); + $text = str_replace(['ä', 'ö', 'ü', 'ß'], ['ae', 'oe', 'ue', 'ss'], $text); + $text = preg_replace('/[^a-z0-9]+/u', '_', $text); + $text = trim($text, '_'); + + if ($text === '') { + $text = 'counter_' . time(); + } + + return $text; + } + + private function buildContextString(array $pathParts, string $varName): string + { + $parts = $pathParts; + if ($varName !== '') { + $parts[] = $varName; + } + + $parts = array_values(array_filter($parts, function ($v) { + return trim((string)$v) !== ''; + })); + + return implode(' ', $parts); + } + + public function MqttPublish($server_id, $topic, $payload, $retain) + { + if (!IPS_InstanceExists($server_id)) { + return false; + } + + $ips_var_type = 3; + $module_id = '{01C00ADD-D04E-452E-B66A-D253278743FE}'; + $ident = 'TempMQTTDevice_' . $this->InstanceID; + + if (!IPS_SemaphoreEnter($ident, 100)) { + return false; + } + + try { + $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); + } + + if (!IPS_IsInstanceCompatible($id, $server_id)) { + return false; + } + + $inst_config = IPS_GetInstance($id); + if ((int)$inst_config['ConnectionID'] !== (int)$server_id) { + IPS_DisconnectInstance($id); + if (!@IPS_ConnectInstance($id, $server_id)) { + return false; + } + } + + IPS_SetName($id, 'Temporary MQTT Device for: ' . $topic); + + $config_arr = [ + 'Retain' => $retain, + 'Topic' => $topic, + 'Type' => $ips_var_type + ]; + + IPS_SetConfiguration($id, json_encode($config_arr)); + IPS_SetHidden($id, true); + IPS_ApplyChanges($id); + + $var_id = @IPS_GetObjectIDByIdent('Value', $id); + if ($var_id === false) { + return false; + } + + RequestAction($var_id, $payload); + } finally { + IPS_SemaphoreLeave($ident); + } + + return true; } } ?> \ No newline at end of file