remove CounterTable and CounterClient modules; add CounterServer and update library version

This commit is contained in:
jhartworks
2026-04-15 14:52:48 +02:00
parent ebe6991832
commit 4363fd47a5
9 changed files with 707 additions and 282 deletions
+70
View File
@@ -0,0 +1,70 @@
{
"elements": [
{
"type": "SelectCategory",
"name": "CounterCategoryID",
"caption": "Counter Kategorie"
},
{
"type": "SelectVariable",
"name": "JsonOutputVariableID",
"caption": "JSON Ausgabe Variable",
"validVariableTypes": [3]
},
{
"type": "ExpansionPanel",
"caption": "MQTT (optional)",
"items": [
{
"type": "CheckBox",
"name": "EnableMQTT",
"caption": "MQTT senden"
},
{
"type": "SelectInstance",
"name": "MqttClientID",
"caption": "MQTT Client",
"visible": "EnableMQTT",
"validModules": ["{C6D2AEB3-6E1F-4B2E-8D6E-2F0A6C4C2A7F}"]
},
{
"type": "ValidationTextBox",
"name": "MqttTopic",
"caption": "MQTT Topic",
"visible": "EnableMQTT",
"placeholder": "clients/counters/json"
}
]
},
{
"type": "NumberSpinner",
"name": "UpdateTime",
"caption": "Aktualisierungszeit (Sekunden)",
"digits": 0
}
],
"actions": [
{
"type": "Button",
"caption": "Testlauf (JSON erzeugen)",
"onClick": "SECC_BuildAndStorePayload($id);"
}
],
"status": [
{
"code": 101,
"icon": "active",
"caption": "Aktiv"
},
{
"code": 102,
"icon": "inactive",
"caption": "Keine Kategorie gewählt"
},
{
"code": 103,
"icon": "inactive",
"caption": "Keine JSON Variable gewählt"
}
]
}
+14
View File
@@ -0,0 +1,14 @@
{
"id": "{EF2E22EE-67DE-EF4F-42D0-C83C6E5E37C7}",
"name": "CounterClient",
"type": 3,
"vendor": "J.H. Artworks",
"aliases":
[
],
"url": "https://jh-artworks.com",
"parentRequirements": [],
"childRequirements": [],
"implemented": [],
"prefix": "SECC"
}
+430
View File
@@ -0,0 +1,430 @@
<?php
class CounterClient extends IPSModule
{
public function Create()
{
parent::Create();
$this->RegisterPropertyInteger('CounterCategoryID', 0);
$this->RegisterPropertyInteger('JsonOutputVariableID', 0);
$this->RegisterPropertyInteger('UpdateTime', 60);
$this->RegisterTimer('Update', 0, 'SECC_BuildAndStorePayload($_IPS[\'TARGET\']);');
}
public function ApplyChanges()
{
parent::ApplyChanges();
$updateTime = $this->ReadPropertyInteger('UpdateTime');
if ($updateTime < 1) {
$updateTime = 60;
}
$this->SetTimerInterval('Update', $updateTime * 1000);
}
public function BuildAndStorePayload()
{
$json = $this->BuildPayload();
if ($json === '') {
return;
}
$targetVarId = $this->ReadPropertyInteger('JsonOutputVariableID');
if ($targetVarId > 0 && IPS_VariableExists($targetVarId)) {
SetValueString($targetVarId, $json);
}
IPS_LogMessage('CounterClient', $json);
}
public function BuildPayload()
{
$counterCategoryId = $this->ReadPropertyInteger('CounterCategoryID');
if ($counterCategoryId <= 0 || !IPS_ObjectExists($counterCategoryId)) {
IPS_LogMessage('CounterClient', 'Ungültige CounterCategoryID');
return '';
}
$counterIds = IPS_GetChildrenIDs($counterCategoryId);
$payload = [
'timestamp' => date('Y-m-d H:i:s'),
'counters' => []
];
foreach ($counterIds as $counterObjectId) {
$obj = IPS_GetObject($counterObjectId);
if ($obj['ObjectType'] != 0) {
continue;
}
$counterName = IPS_GetName($counterObjectId);
$counterId = $this->makeSlug($counterName);
$varIds = $this->CollectVariablesRecursive($counterObjectId);
if (count($varIds) === 0) {
continue;
}
$counterData = [
'id' => $counterId,
'name' => $counterName
];
$tempSlots = [];
$pressureSlots = [];
foreach ($varIds as $varId) {
$classified = $this->ClassifyVariable($varId);
if ($classified === null) {
continue;
}
$field = $classified['field'];
$value = $classified['value'];
$unit = $classified['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');
$counterData['type'] = $this->DetectCounterType($counterData);
if (!isset($counterData['unit'])) {
$counterData['unit'] = $this->inferDefaultUnit($counterData);
}
if (count($counterData) > 3) {
$payload['counters'][] = $counterData;
}
}
return json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
private function CollectVariablesRecursive(int $objectId): array
{
$result = [];
$children = IPS_GetChildrenIDs($objectId);
foreach ($children as $childId) {
if (IPS_LinkExists($childId)) {
$link = IPS_GetLink($childId);
$targetId = (int) $link['TargetID'];
if (IPS_VariableExists($targetId)) {
$result[$targetId] = $targetId;
} elseif (IPS_ObjectExists($targetId)) {
foreach ($this->CollectVariablesRecursive($targetId) as $subVarId) {
$result[$subVarId] = $subVarId;
}
}
continue;
}
if (IPS_VariableExists($childId)) {
$result[$childId] = $childId;
continue;
}
if (IPS_ObjectExists($childId)) {
foreach ($this->CollectVariablesRecursive($childId) as $subVarId) {
$result[$subVarId] = $subVarId;
}
}
}
return array_values($result);
}
private function ClassifyVariable(int $varId): ?array
{
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'];
}
if ($profileName === '' || !IPS_VariableProfileExists($profileName)) {
return null;
}
$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);
$varName = mb_strtolower(IPS_GetName($varId));
$unitNorm = $this->normalizeUnit($unit);
if (in_array($unitNorm, ['kwh', 'wh', 'mwh', 'm3', 'l'], true)) {
return [
'field' => 'total_value',
'value' => $value,
'unit' => $suffix !== '' ? trim($suffix) : $unitNorm,
'name' => $varName
];
}
if (in_array($unitNorm, ['kw', 'w', 'mw'], true)) {
return [
'field' => 'power_value',
'value' => $value,
'unit' => $unitNorm,
'name' => $varName
];
}
if (in_array($unitNorm, ['m3/h', 'l/h', 'l/min'], true)) {
return [
'field' => 'flow_value',
'value' => $value,
'unit' => $unitNorm,
'name' => $varName
];
}
if ($unitNorm === 'a') {
return [
'field' => 'current_value',
'value' => $value,
'unit' => $unitNorm,
'name' => $varName
];
}
if ($unitNorm === 'v') {
return [
'field' => 'voltage_value',
'value' => $value,
'unit' => $unitNorm,
'name' => $varName
];
}
if ($unitNorm === 'hz') {
return [
'field' => 'frequency_value',
'value' => $value,
'unit' => $unitNorm,
'name' => $varName
];
}
if (in_array($unitNorm, ['bar', 'mbar', 'pa'], true)) {
if ($this->looksLikeIn($varName)) {
return [
'field' => 'pressure_in_value',
'value' => $value,
'unit' => $unitNorm,
'name' => $varName
];
}
if ($this->looksLikeOut($varName)) {
return [
'field' => 'pressure_out_value',
'value' => $value,
'unit' => $unitNorm,
'name' => $varName
];
}
return [
'field' => 'pressure_auto',
'value' => $value,
'unit' => $unitNorm,
'name' => $varName
];
}
if (in_array($unitNorm, ['°c', 'c', 'k'], true)) {
if ($this->looksLikeIn($varName)) {
return [
'field' => 'temperature_in_value',
'value' => $value,
'unit' => $unitNorm,
'name' => $varName
];
}
if ($this->looksLikeOut($varName)) {
return [
'field' => 'temperature_out_value',
'value' => $value,
'unit' => $unitNorm,
'name' => $varName
];
}
return [
'field' => 'temperature_auto',
'value' => $value,
'unit' => $unitNorm,
'name' => $varName
];
}
if ($unitNorm === 'cos' || $unitNorm === 'cosphi' || $unitNorm === 'pf') {
return [
'field' => 'power_factor',
'value' => $value,
'unit' => $unitNorm,
'name' => $varName
];
}
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 DetectCounterType(array $counterData): string
{
$unit = isset($counterData['unit']) ? $this->normalizeUnit((string) $counterData['unit']) : '';
if (isset($counterData['voltage_value']) || isset($counterData['current_value']) || isset($counterData['frequency_value']) || isset($counterData['power_factor'])) {
return 'electricity';
}
if (isset($counterData['temperature_in_value']) || isset($counterData['temperature_out_value'])) {
return 'heat';
}
if ($unit === 'm3' || $unit === 'l') {
return 'water';
}
if ($unit === 'kwh' || $unit === 'mwh' || $unit === 'wh') {
if (isset($counterData['temperature_in_value']) || isset($counterData['flow_value'])) {
return 'heat';
}
return 'electricity';
}
return 'other';
}
private function inferDefaultUnit(array $counterData): string
{
if (isset($counterData['total_value'])) {
if ($counterData['type'] === 'water') {
return 'm3';
}
return 'kWh';
}
return '';
}
private function looksLikeIn(string $name): bool
{
return strpos($name, 'vorlauf') !== false
|| strpos($name, 'vl') !== false
|| strpos($name, 'flow') !== false
|| strpos($name, 'ein') !== false
|| strpos($name, 'in') !== false;
}
private function looksLikeOut(string $name): bool
{
return strpos($name, 'ruecklauf') !== false
|| strpos($name, 'rücklauf') !== false
|| strpos($name, 'rl') !== false
|| strpos($name, 'return') !== false
|| strpos($name, 'aus') !== false
|| strpos($name, 'out') !== false;
}
private function normalizeUnit(string $unit): string
{
$unit = mb_strtolower(trim($unit));
$unit = str_replace([' ', '°'], ['', '°'], $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;
}
}
?>
+24
View File
@@ -0,0 +1,24 @@
{
"elements": [
{
"type": "SelectInstance",
"name": "Connector",
"caption": "DB-Connector",
"validModules": ["{7B420C9A-F0FF-4C65-925D-6FEE0D8F1A55}"]
},
{
"type": "SelectVariable",
"name": "SourceJSON",
"caption": "JSON-Datenquelle",
"validVariableTypes": [3]
},
{
"type": "NumberSpinner",
"name": "UpdateTime",
"caption": "Aktualisierungszeit (Sekunden)",
"digits": 0
}
],
"actions": [],
"status": []
}
@@ -1,6 +1,6 @@
{ {
"id": "{D0DA85AB-1BAD-C36C-A3FD-1C2660F96F1C}", "id": "{D0DA85AB-1BAD-C36C-A3FD-1C2660F96F1C}",
"name": "CounterTable", "name": "CounterServer",
"type": 3, "type": 3,
"vendor": "J.H. Artworks", "vendor": "J.H. Artworks",
"aliases": "aliases":
@@ -10,5 +10,5 @@
"parentRequirements": [], "parentRequirements": [],
"childRequirements": [], "childRequirements": [],
"implemented": [], "implemented": [],
"prefix": "CT" "prefix": "SECS"
} }
+166
View File
@@ -0,0 +1,166 @@
<?php
class CounterServer extends IPSModule
{
public function Create()
{
parent::Create();
$this->RegisterPropertyInteger('Connector', 0);
$this->RegisterPropertyInteger('SourceJSON', 0);
$this->RegisterPropertyInteger('UpdateTime', 60);
$this->RegisterTimer('Update', 0, 'SECT_checkTable($_IPS[\'TARGET\']);');
}
public function ApplyChanges()
{
parent::ApplyChanges();
$updateTime = $this->ReadPropertyInteger('UpdateTime');
if ($updateTime < 1) {
$updateTime = 60;
}
$this->SetTimerInterval('Update', $updateTime * 1000);
$this->checkTable();
}
public function checkTable()
{
$sqlId = $this->ReadPropertyInteger('Connector');
$jsonVar = $this->ReadPropertyInteger('SourceJSON');
if ($sqlId <= 0 || $jsonVar <= 0) {
IPS_LogMessage('CounterTable', 'Config fehlt.');
return;
}
$jsonString = GetValueString($jsonVar);
if ($jsonString === '') {
return;
}
$payload = json_decode($jsonString, true);
if (!is_array($payload)) {
IPS_LogMessage('CounterTable', 'JSON Fehler');
return;
}
$clientId = (string)$this->InstanceID;
$clientName = IPS_GetName($this->InstanceID);
$timestamp = $payload['timestamp'] ?? date('Y-m-d H:i:s');
$counters = $payload['counters'] ?? [];
foreach ($counters as $c) {
$counterId = $c['id'] ?? '';
if ($counterId === '') continue;
$counterName = $c['name'] ?? $counterId;
$type = $c['type'] ?? 'other';
$unit = $c['unit'] ?? 'kWh';
$projectId = isset($c['project_id']) ? (int)$c['project_id'] : 'NULL';
// ---------- UPSERT ----------
$sql = "
INSERT INTO meter_devices
(project_id, client_id, counter_id, meter_uuid, meter_name, external_id, meter_type, billing_unit, description, is_active)
VALUES
(
" . ($projectId === 'NULL' ? 'NULL' : $projectId) . ",
'" . $this->esc($clientId) . "',
'" . $this->esc($counterId) . "',
'" . $this->esc($clientId . '_' . $counterId) . "',
'" . $this->esc($counterName) . "',
'" . $this->esc($counterId) . "',
'" . $this->esc($type) . "',
'" . $this->esc($unit) . "',
'" . $this->esc('Client: ' . $clientName) . "',
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 client_id = '" . $this->esc($clientId) . "'
AND counter_id = '" . $this->esc($counterId) . "'
LIMIT 1
";
$res = MySQL_ExecuteSimple($sqlId, $sqlGet);
if (!is_array($res) || count($res) === 0) {
IPS_LogMessage('CounterTable', 'Kein meter_id gefunden');
continue;
}
$meterId = (int)$res[0]['id'];
// ---------- 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') . "
)
";
MySQL_ExecuteSimple($sqlId, $sqlInsert);
}
}
private function f($arr, $key)
{
if (!isset($arr[$key]) || $arr[$key] === '' || $arr[$key] === null) {
return 'NULL';
}
if (is_numeric($arr[$key])) {
return str_replace(',', '.', (string)$arr[$key]);
}
return 'NULL';
}
private function esc($v)
{
return str_replace(
['\\', "'", '"'],
['\\\\', "\\'", '\\"'],
$v
);
}
}
?>
-89
View File
@@ -1,89 +0,0 @@
{
"elements":
[
{ "type": "NumberSpinner", "Name": "Price", "caption": "Strompreis", "digits": 2},
{
"type": "List",
"name": "Counters",
"caption": "Zählerliste",
"add": true,
"delete": true,
"sort": {
"column": "Name",
"direction": "ascending"
},
"columns": [
{
"caption": "Name",
"name": "Name",
"width": "100px",
"edit": {
"type": "ValidationTextBox"
},
"add": ""
},
{
"caption": "Gesamtverbrauch",
"name": "Total",
"width": "100px",
"edit": {
"type": "SelectVariable",
"validVariableTypes": [2]
},
"add": 0
},
{
"caption": "Monatsverbrauch",
"name": "Monthly",
"width": "100px",
"edit": {
"type": "SelectVariable",
"validVariableTypes": [2]
},
"add": 0
},
{
"caption": "Wochenverbrauch",
"name": "Weekly",
"width": "100px",
"edit": {
"type": "SelectVariable",
"validVariableTypes": [2]
},
"add": 0
},
{
"caption": "Tagesverbrauch",
"name": "Daily",
"width": "100px",
"edit": {
"type": "SelectVariable",
"validVariableTypes": [2]
},
"add": 0
},
{
"caption": "Limit",
"name": "Limit",
"width": "100px",
"edit": {
"type": "NumberSpinner",
"digits": 2
},
"add": 0.1
}
]
}
],
"actions":
[
],
"status":
[
]
}
-190
View File
@@ -1,190 +0,0 @@
<?
// Klassendefinition
class CounterTable extends IPSModule {
// Überschreibt die interne IPS_Create($id) Funktion
public function Create() {
// Diese Zeile nicht löschen.
parent::Create();
$this->RegisterPropertyFloat("Price",0.23);
$this->RegisterPropertyString("Counters","");
$this->RegisterVariableString("htmlOutput", "Zählerübersicht",["PRESENTATION"=> VARIABLE_PRESENTATION_LEGACY, "PROFILE" => "~HTMLBox"],0);
}
// Überschreibt die intere IPS_ApplyChanges($id) Funktion
public function ApplyChanges() {
// Diese Zeile nicht löschen
parent::ApplyChanges();
$this->checkTable();
}
public function RequestAction($Ident, $Value) {
$varid = $this->GetIDForIdent($Ident);
SetValue($varid, $Value);
}
public function checkTable(){
$price = $this->ReadPropertyFloat("Price");
$arrString = $this->ReadPropertyString("Counters");
$arr = json_decode($arrString);
$dataoutput = "";
/// print_r($arr);
if($arr != NULL){
foreach($arr as $cnt){
$cost = 0.0;
$cost = $cnt->Monthly * $price;
$style ='';
if ($cnt->Monthly > $cnt->Limit){
$style = 'style="background-color:yellow;"';
}
$dataoutput .= '
<tr '.$style.'>
<td style="text-align:left;">
'.$cnt->Name.'
</td>
<td>
'.$cnt->Total.' kWh
</td>
<td>
'.$cnt->Monthly.' kWh
</td>
<td>
'.$cnt->Weekly.' kWh
</td>
<td>
'.$cnt->Daily.' kWh
</td>
<td>
'.$cost.'€
</td>
</tr>';
}
}
$this->createTable($dataoutput);
/*
Array
(
[0] => stdClass Object
(
[Name] => Blub
[Total] => 49294
[Monthly] => 49294
[Weekly] => 49294
[Daily] => 49294
[Limit] => 0.1
)
)
(Code: -32603)
*/
}
public function createTable($data)
{
$htmlprefix =' <html>
<style>
table, td {
border-collapse: separate;
border-spacing: 5px;
}
td {
color:#000000;
font-family:Arial;
font-size:14pt;
width:150px;
text-align:center;
}
th {
color:#000000;
font-family:Arial;
font-size:14pt;
width:150px;
text-align:center;
}
</style>
<body>
<table style="hight:100%;background-color:lightgrey;border:2px solid green;">
<thead>
<tr>
<th style="">
Name:
</th>
<th>
Gesamt:
</th>
<th>
Monat:
</th>
<th>
Woche:
</th>
<th>
Tag:
</th>
<th>
Kosten:
</th>
</tr>
</thead>
';
$htmlsuffix = '
<tr>
</tr>
</table>
';
SetValueString($this->GetIDForIdent("htmlOutput"),$htmlprefix.$data.$htmlsuffix);
}
}
?>
+1 -1
View File
@@ -8,6 +8,6 @@
"date": 1491343200 "date": 1491343200
}, },
"version": "1.0", "version": "1.0",
"build": 9, "build": 0,
"date": 0 "date": 0
} }