diff --git a/Batterie/form.json b/Batterie/form.json index 2eb8f13..b95e24d 100644 --- a/Batterie/form.json +++ b/Batterie/form.json @@ -17,15 +17,34 @@ "suffix": "Sekunden" }, { - "type": "NumberSpinner", + "type":"Select", + "name":"Batterietyp", + "caption":"Batterietyp", + "options":[ + { + "caption":"Goodwe", + "value":1 + }, + { + "caption":"Solaredge", + "value":2 + }, + { + "caption":"Sig Energy", + "value":3 + } + ] + }, + { + "type": "SelectVariable", "name": "MaxBatterieleistung", - "caption": "Maximale Batterieleistung", + "caption": "Maximale Ladeleistung", "suffix": "" }, { - "type": "NumberSpinner", + "type": "SelectVariable", "name": "MaxNachladen", - "caption": "Maximum Nachladen", + "caption": "Maximum Nachladeleistung", "suffix": "" }, { @@ -52,7 +71,7 @@ }, { "caption":"Durch EMS Symcon", - "value":4 + "value":2 } ] }, @@ -67,10 +86,9 @@ "name": "Netzbezug", "caption": "Variable mit dem zu regelnden Netzbezug" }, - { - "type": "SelectVariable", - "name": "Batterieleistung_Effektiv", - "caption": "Effektive, aktuelle Batterieleistung" + { + "type": "Label", + "caption": "Je nach Wr-Type bekommt Laden_Entlade andere Werte.\nGoodwe: Laden=11, Entladen=12,\nSolaredge: Laden=3, Entladen=4,\nSig Energy: Laden=3, Entladen=6, P in kW" } ] } diff --git a/Batterie/module.json b/Batterie/module.json index 468e0f8..13f374d 100644 --- a/Batterie/module.json +++ b/Batterie/module.json @@ -1,5 +1,5 @@ { - "id": "{F6C6A4B2-C5BB-4D94-2629-01D8B0D4CAF5}", + "id": "{166B9E49-882B-ADED-F256-4DD4CC24DF6C}", "name": "Batterie", "type": 3, "vendor": "Belevo AG", diff --git a/Batterie/module.php b/Batterie/module.php index 418aaaf..a4f27e5 100644 --- a/Batterie/module.php +++ b/Batterie/module.php @@ -12,17 +12,16 @@ class Batterie extends IPSModule $this->RegisterPropertyInteger("AufdasNachladen",0); $this->RegisterPropertyInteger("MinimumEntladen",0); $this->RegisterPropertyInteger("Batterieladezustand",0); - $this->RegisterPropertyInteger("Batteriemanagement", 1); + $this->RegisterPropertyInteger("Batteriemanagement", 1); + $this->RegisterPropertyInteger("Batterietyp", 1); $this->RegisterPropertyInteger("MaxNachladen",0); $this->RegisterPropertyInteger("Netzbezug", 0); // Initialisierung mit 0 $this->RegisterPropertyInteger("Interval", 2); // Recheninterval - $this->RegisterPropertyInteger("Batterieleistung_Effektiv", 0); // Recheninterval + // Variabeln für Kommunkation mit Manager - $this->RegisterVariableFloat("Entladeleistung","Entladeleistung", "",0); $this->RegisterVariableInteger("Batteriemanagement_Variabel","Batteriemanagement_Variabel", "",0); - $this->RegisterVariableInteger("Laden3_Entladen4","Laden3_Entladen4", "",3); - $this->RegisterVariableFloat("Ladeleistung","Ladeleistung", "",0); + $this->RegisterVariableInteger("Laden_Entladen","Laden_Entladen", "",3); $this->RegisterVariableInteger("Aktuelle_Leistung", "Aktuelle_Leistung", "", 0); $this->RegisterVariableString("PowerSteps", "PowerSteps"); $this->RegisterVariableBoolean("Idle", "Idle", "", 0); @@ -58,49 +57,143 @@ class Batterie extends IPSModule $batterieManagement = $this->ReadPropertyInteger("Batteriemanagement"); $this->SetValue("Batteriemanagement_Variabel", $batterieManagement); $this->SetTimerInterval("Timer_Do_UserCalc_Battery",$this->ReadPropertyInteger("Interval")*1000); - } + $batterietyp = $this->ReadPropertyInteger("Batterietyp"); - private function GeneratePowerSteps($additionalValue) - { - $maxleistung = $this->ReadPropertyInteger("MaxBatterieleistung"); - $stepSize = 250; // Schrittgröße - $stepSizeSmall = 50; // Kleine Schrittgröße - - // Array direkt als Range erzeugen (schneller als Schleife) - $array_powersteps = range(-$maxleistung, $maxleistung, $stepSize); - - // Nächstgelegenen Wert direkt bestimmen (rundet auf den nächsten Step) - $closestValue = round($additionalValue / $stepSize) * $stepSize; - - // Falls der Wert nicht im Bereich liegt, abbrechen - if (!in_array($closestValue, $array_powersteps)) { - return $array_powersteps; + switch ($batterietyp) { + case 1: // Goodwe + $this->MaintainVariable("Goodwe_EntLadeleistung", "Goodwe_EntLadeleistung", VARIABLETYPE_FLOAT, "", 10, true); + $this->MaintainVariable("Ladeleistung", "Ladeleistung", VARIABLETYPE_FLOAT, "", 11, false); + $this->MaintainVariable("Entladeleistung", "Entladeleistung", VARIABLETYPE_FLOAT, "", 13, false); + $this->MaintainVariable("Laden_Entladen", "Laden_Entladen", VARIABLETYPE_INTEGER, "", 12, true); + break; + + case 2: // Solaredge + $this->MaintainVariable("Goodwe_EntLadeleistung", "Goodwe_EntLadeleistung", VARIABLETYPE_FLOAT, "", 10, false); + $this->MaintainVariable("Ladeleistung", "Ladeleistung", VARIABLETYPE_FLOAT, "", 11, true); + $this->MaintainVariable("Entladeleistung", "Entladeleistung", VARIABLETYPE_FLOAT, "", 13, true); + $this->MaintainVariable("Laden_Entladen", "Laden_Entladen", VARIABLETYPE_INTEGER, "", 12, true); + break; + + case 3: // SiG Energy + $this->MaintainVariable("Goodwe_EntLadeleistung", "Goodwe_EntLadeleistung", VARIABLETYPE_FLOAT, "", 10, false); + $this->MaintainVariable("Ladeleistung", "Ladeleistung", VARIABLETYPE_FLOAT, "", 11, true); + $this->MaintainVariable("Entladeleistung", "Entladeleistung", VARIABLETYPE_FLOAT, "", 13, true); + $this->MaintainVariable("Laden_Entladen", "Laden_Entladen", VARIABLETYPE_INTEGER, "", 12, true); + break; + + default: + // Sicherheit: alles weg + $this->MaintainVariable("Goodwe_EntLadeleistung", "Goodwe_EntLadeleistung", VARIABLETYPE_FLOAT, "", 10, false); + $this->MaintainVariable("Ladeleistung", "Ladeleistung", VARIABLETYPE_FLOAT, "", 11, false); + $this->MaintainVariable("Laden_Entladen", "Laden_Entladen", VARIABLETYPE_INTEGER, "", 12, false); + $this->MaintainVariable("Entladeleistung", "Entladeleistung", VARIABLETYPE_FLOAT, "", 13, false); + break; } - - // Index des gefundenen Werts suchen - $index = array_search($closestValue, $array_powersteps); - - // Zusätzliche Werte berechnen und auf MaxLeistung begrenzen - $newValues = array_filter([ - $closestValue - 4 * $stepSizeSmall, - $closestValue - 3 * $stepSizeSmall, - $closestValue - 2 * $stepSizeSmall, - $closestValue - $stepSizeSmall, - $closestValue, - $closestValue + $stepSizeSmall, - $closestValue + 2 * $stepSizeSmall, - $closestValue + 3 * $stepSizeSmall, - $closestValue + 4 * $stepSizeSmall, - ], function ($value) use ($maxleistung) { - return $value >= -$maxleistung && $value <= $maxleistung; - }); - - // Effizienteres Einfügen der Werte (direkt an der Stelle) - array_splice($array_powersteps, $index, 1, $newValues); - - return $array_powersteps; + + $maxBatVar = $this->ReadPropertyInteger("MaxBatterieleistung"); + $maxNachVar = $this->ReadPropertyInteger("MaxNachladen"); + + if ($maxBatVar > 0) { + $this->RegisterMessage($maxBatVar, VM_UPDATE); + } + if ($maxNachVar > 0) { + $this->RegisterMessage($maxNachVar, VM_UPDATE); + } + + } + + public function MessageSink($TimeStamp, $SenderID, $Message, $Data) +{ + if ($Message !== VM_UPDATE) { + return; + } + + $maxBatVar = $this->ReadPropertyInteger("MaxBatterieleistung"); + $maxNachVar = $this->ReadPropertyInteger("MaxNachladen"); + + if ($SenderID === $maxBatVar || $SenderID === $maxNachVar) { + // PowerSteps sofort neu berechnen (mit aktuellem Peak-Status) + $this->GetCurrentData($this->GetValue("Is_Peak_Shaving")); + } +} + + + +private function GeneratePowerSteps($additionalValue) +{ + + $maxleistung_raw = GetValue($this->ReadPropertyInteger("MaxBatterieleistung")); + $nachladen_raw = GetValue($this->ReadPropertyInteger("MaxNachladen")); + + $stepSize = 250; // Grobe Schrittgröße + $stepSizeSmall = 50; // Feine Schrittgröße + + // Grenzen auf 50er abrunden (floor) + $maxleistung = (int)(floor($maxleistung_raw / $stepSizeSmall) * $stepSizeSmall); + $minleistung = (int)(-floor($nachladen_raw / $stepSizeSmall) * $stepSizeSmall); // negativ! + + // Sicherheitscheck: falls Werte komisch sind + if ($maxleistung < 0) $maxleistung = 0; + if ($minleistung > 0) $minleistung = 0; + + // Grundarray: von min bis max in 250er Schritten + $neg = ($minleistung < 0) ? range($minleistung, 0, $stepSize) : [0]; + $pos = range(0, $maxleistung, $stepSize); + + $array_powersteps = array_values(array_unique(array_merge($neg, $pos))); + sort($array_powersteps, SORT_NUMERIC); + + // Zusätzlichen Wert auf 50er abrunden (floor, nicht round!) + // (wichtig: floor bei negativen Zahlen geht "weiter runter", daher extra Logik) + $closestValue = (int)(floor($additionalValue / $stepSizeSmall) * $stepSizeSmall); + + // Clamp in den Bereich + if ($closestValue < $minleistung) $closestValue = $minleistung; + if ($closestValue > $maxleistung) $closestValue = $maxleistung; + + // Prüfen ob der Wert im Array existiert (bei 250er Raster oft NICHT) + $index = array_search($closestValue, $array_powersteps, true); + + // Wenn nicht vorhanden: an der richtigen Stelle einsortieren + if ($index === false) { + $index = 0; + $count = count($array_powersteps); + while ($index < $count && $array_powersteps[$index] < $closestValue) { + $index++; + } + // $index ist jetzt Einfügeposition + } + + // Feine Werte um closestValue herum (±4 * 50) + $newValues = []; + for ($i = -4; $i <= 4; $i++) { + $v = $closestValue + ($i * $stepSizeSmall); + if ($v >= $minleistung && $v <= $maxleistung) { + $newValues[] = $v; + } + } + + // Duplikate vermeiden (falls schon Werte vorhanden sind) + $newValues = array_values(array_unique($newValues)); + + // Wenn closestValue exakt im Grundarray war: diesen einen ersetzen + // sonst: feinwerte einfach an der Einfügestelle einfügen + if (array_search($closestValue, $array_powersteps, true) !== false) { + $existingIndex = array_search($closestValue, $array_powersteps, true); + array_splice($array_powersteps, $existingIndex, 1, $newValues); + } else { + array_splice($array_powersteps, $index, 0, $newValues); + } + + // Am Ende sortieren + Duplikate killen (sicher ist sicher) + $array_powersteps = array_values(array_unique($array_powersteps)); + sort($array_powersteps, SORT_NUMERIC); + + return $array_powersteps; +} // Ende Array Steps + @@ -132,39 +225,128 @@ public function RequestAction($Ident, $Value) public function SetAktuelle_Leistung(int $power) { + + $batterietyp = $this->ReadPropertyInteger("Batterietyp"); $batterieManagement = $this->ReadPropertyInteger("Batteriemanagement"); - // Wechselrichter steuert das Laden/Entladen der Batterie - if ($batterieManagement == 1) { + + // Goodwe, Solaredge WR Modus + if ($batterieManagement == 1 && ($batterietyp == 1 || $batterietyp == 2)) { $this->SetValue("Entladeleistung", 0); $this->SetValue("Ladeleistung", 0); + $this->SetValue("Batteriemanagement_Variabel", 1); return; + //Sig Energy WR Modus + } elseif ($batterieManagement == 1 && $batterietyp == 3) { + $this->SetValue("Entladeleistung", 0); + $this->SetValue("Ladeleistung", 0); + $this->SetValue("Batteriemanagement_Variabel", 0); + return; + + // Sig Energy Symcon Modus + } elseif ($batterieManagement == 2 && $batterietyp == 3) { + $this->SetValue("Batteriemanagement_Variabel", 1); + + //Solaredge Symcon Modus + }elseif ($batterieManagement == 2 && $batterietyp == 2) { + $this->SetValue("Batteriemanagement_Variabel", 4); } + $batterietyp = $this->ReadPropertyInteger("Batterietyp"); + if ($batterietyp == 1) {//Goodwe + $this->SetValue("Entladeleistung", 0); + $this->SetValue("Ladeleistung", 0); + //-----------------------Gooodwee-------------------------------------// if($this->GetValue("Is_Peak_Shaving")==true){ - if ($power >= 0) { - $this->SetValue("Ladeleistung", $power); - $this->SetValue("Entladeleistung", 0); - $this->SetValue("Laden3_Entladen4", 3); - } else { - $this->SetValue("Entladeleistung", abs($power)); - $this->SetValue("Ladeleistung", 0); - $this->SetValue("Laden3_Entladen4", 4); + + if ($power >= 0) { + $this->SetValue("Goodwe_EntLadeleistung", abs($power)); + $this->SetValue("Laden_Entladen", 11); + } else { + $this->SetValue("Goodwe_EntLadeleistung", abs($power)); + $this->SetValue("Laden_Entladen", 12); + } + + }else{ + + if ($power >= 0) { + $this->SetValue("Goodwe_EntLadeleistung", abs($power)); + $this->SetValue("Laden_Entladen", 11); + } else { + $this->SetValue("Goodwe_EntLadeleistung", abs($power)); + $this->SetValue("Laden_Entladen", 12); + } + } - }else{ - if ($power >= 0) { - $this->SetValue("Ladeleistung", $power); - $this->SetValue("Entladeleistung", 0); - $this->SetValue("Laden3_Entladen4", 3); - } else { - $this->SetValue("Entladeleistung", abs($power)); - $this->SetValue("Ladeleistung", 0); - $this->SetValue("Laden3_Entladen4", 4); - } + }elseif ($batterietyp == 2) {//Solaredge + //-----------------------Solaredge-------------------------------------// + $this->SetValue("Goodwe_EntLadeleistung",0); + if($this->GetValue("Is_Peak_Shaving")==true){ + + if ($power >= 0) { + $this->SetValue("Ladeleistung", $power); + $this->SetValue("Entladeleistung", 0); + $this->SetValue("Laden_Entladen", 3); + } else { + $this->SetValue("Entladeleistung", abs($power)); + $this->SetValue("Ladeleistung", 0); + $this->SetValue("Laden_Entladen", 4); + } + + }else{ + if ($power >= 0) { + $this->SetValue("Ladeleistung", $power); + $this->SetValue("Entladeleistung", 0); + $this->SetValue("Laden_Entladen", 3); + } else { + $this->SetValue("Entladeleistung", abs($power)); + $this->SetValue("Ladeleistung", 0); + $this->SetValue("Laden_Entladen", 4); + } + } + } elseif ($batterietyp == 3) {//Sig Energy + + //-----------------------Sig Energy-------------------------------------// + $this->SetValue("Goodwe_EntLadeleistung",0); + if($this->GetValue("Is_Peak_Shaving")==true){ + + if ($power >= 0) { + $this->SetValue("Ladeleistung", $power/1000); + $this->SetValue("Entladeleistung", 0); + $this->SetValue("Laden_Entladen", 3); + } else { + $this->SetValue("Entladeleistung", abs($power)/1000); + $this->SetValue("Ladeleistung", 0); + $this->SetValue("Laden_Entladen", 6); + } + + }else{ + if ($power >= 0) { + $this->SetValue("Ladeleistung", $power/1000); + $this->SetValue("Entladeleistung", 0); + $this->SetValue("Laden_Entladen", 3); + } else { + $this->SetValue("Entladeleistung", abs($power)/1000); + $this->SetValue("Ladeleistung", 0); + $this->SetValue("Laden_Entladen", 6); + } + } } - + + + + + + + + + + + + + // Prüfe auf Änderung der Leistung im Vergleich zur letzten Einstellung $lastPower = GetValue($this->GetIDForIdent("Aktuelle_Leistung")); if ($power != $lastPower) { @@ -193,7 +375,7 @@ public function RequestAction($Ident, $Value) $array_powersteps = $this->GeneratePowerSteps($this->GetValue("Aktuelle_Leistung")); $aufdasnachladen = $this->ReadPropertyInteger("AufdasNachladen"); $minimumentladen = $this->ReadPropertyInteger("MinimumEntladen"); - $maxleistung = $this->ReadPropertyInteger("MaxBatterieleistung"); + $maxleistung = GetValue($this->ReadPropertyInteger("MaxBatterieleistung")); $dummy_array = []; $batterieladezustand = GetValue($this->ReadPropertyInteger("Batterieladezustand")); $filtered_powersteps_entladen = []; @@ -277,7 +459,7 @@ public function RequestAction($Ident, $Value) }elseif($batterieladezustand<$aufdasnachladen){ - $dummy_array[] = $this->ReadPropertyInteger("MaxNachladen"); + $dummy_array[] = GetValue($this->ReadPropertyInteger("MaxNachladen")); $this->SetValue("PowerSteps", json_encode($dummy_array)); IPS_LogMessage("Batterie", "im 3"); diff --git a/Batterie_Deye/README.md b/Batterie_Deye/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/Batterie_Deye/form.json b/Batterie_Deye/form.json deleted file mode 100644 index 902b923..0000000 --- a/Batterie_Deye/form.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "elements": [ - { - "type": "Label", - "caption": "Konfiguration der Batterie für Peakshaving" - }, - { - "type": "NumberSpinner", - "name": "IdleCounterMax", - "caption": "Zyklen zwischen zwei Leistungsänderungen (Multipliziert sich mit Interval)", - "suffix": "" - }, - { - "type": "NumberSpinner", - "name": "Interval", - "caption": "Intervall Neuberechnung der Werte", - "suffix": "Sekunden" - }, - { - "type": "NumberSpinner", - "name": "MaxBatterieleistung", - "caption": "Maximale Batterieleistung", - "suffix": "" - }, - { - "type": "NumberSpinner", - "name": "MaxNachladen", - "caption": "Maximum Nachladen", - "suffix": "" - }, - { - "type": "NumberSpinner", - "name": "AufdasNachladen", - "caption": "Auf so viel % nachladen", - "suffix": "" - - }, - { - "type": "NumberSpinner", - "name": "MinimumEntladen", - "caption": "Minimal % des Batterieladezustand", - "suffix": "" - }, - { - "type":"Select", - "name":"Batteriemanagement", - "caption":"Batteriemanagement", - "options":[ - { - "caption":"Durch Wechselrichter", - "value":2 - }, - { - "caption":"Durch EMS Symcon", - "value":0 - } - ] - }, - { - "type": "SelectVariable", - "name": "Batteriespannung", - "caption": "Batteriespannung", - "test": true - }, - { - "type": "SelectVariable", - "name": "Batterie_Ladezustand", - "caption": "Batterie Ladeszutand %", - "test": true - }, - { - "type": "SelectVariable", - "name": "Netzbezug", - "caption": "Variable mit dem zu regelnden Netzbezug" - }, - { - "type": "SelectVariable", - "name": "Kotnrolle_TOU_Spannung", - "caption": "Kontrollvariabel Spannung TOU1" - }, - { - "type": "SelectVariable", - "name": "Kotnrolle_Selling_CT", - "caption": "Kontrollvariabel Selling First/Zero Export to CT" - } - ] -} diff --git a/Batterie_Deye/module.json b/Batterie_Deye/module.json deleted file mode 100644 index cc6c8e6..0000000 --- a/Batterie_Deye/module.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "id": "{76529CAE-191C-41BF-5FDE-EF4A4FE25651}", - "name": "Batterie_Deye", - "type": 3, - "vendor": "Belevo AG", - "aliases": [], - "parentRequirements": [], - "childRequirements": [], - "implemented": [], - "prefix": "GEF", - "url": "" -} \ No newline at end of file diff --git a/Batterie_Deye/module.php b/Batterie_Deye/module.php deleted file mode 100644 index 0f3c246..0000000 --- a/Batterie_Deye/module.php +++ /dev/null @@ -1,385 +0,0 @@ -RegisterPropertyInteger("MaxBatterieleistung", 0); - $this->RegisterPropertyInteger("Batteriespannung", 50); - $this->RegisterPropertyInteger("Batterie_Ladezustand", 50); - $this->RegisterPropertyFloat("AufdasNachladen",0); - $this->RegisterPropertyFloat("MinimumEntladen",0); - $this->RegisterPropertyInteger("Batteriemanagement", 1); - $this->RegisterPropertyInteger("Kotnrolle_TOU_Spannung", 0); - $this->RegisterPropertyInteger("Kotnrolle_Selling_CT", 0); - $this->RegisterPropertyInteger("MaxNachladen",0); - $this->RegisterPropertyInteger("Netzbezug", 0); // Initialisierung mit 0 - $this->RegisterPropertyInteger("Interval", 2); // Recheninterval - - // Variabeln für Kommunkation mit Manager - $this->RegisterVariableInteger("Batteriemanagement_Variabel","Batteriemanagement_Variabel", "",0); - $this->RegisterVariableInteger("Ladestrom","Ladestrom", "",0); - $this->RegisterVariableInteger("Entladestrom","Entladestrom", "",0); - $this->RegisterVariableInteger("Batteriespannung_laden_entladen","Batteriespannung_laden_entladen","",0); - - $this->RegisterVariableInteger("Aktuelle_Leistung", "Aktuelle_Leistung", "", 0); - $this->RegisterVariableString("PowerSteps", "PowerSteps"); - $this->RegisterVariableBoolean("Idle", "Idle", "", 0); - $this->RegisterVariableInteger("Sperre_Prio", "Sperre_Prio"); - $this->RegisterVariableInteger("PV_Prio", "PV_Prio"); - $this->RegisterVariableInteger("Power", "Power"); - $this->RegisterVariableBoolean("Is_Peak_Shaving", "Is_Peak_Shaving"); - $this->RegisterVariableInteger("Leistung_Delta", "Leistung_Delta", "", 0); - - $this->RegisterVariableBoolean("Hysterese", "Hysterese","",false); - $this->RegisterVariableBoolean("Schreibkontrolle", "Schreibkontrolle","",false); - - - $this->RegisterVariableFloat("Bezogene_Energie", "Bezogene_Energie", "", 0); - - // Hilfsvariabeln für Idle zustand - $this->RegisterPropertyInteger("IdleCounterMax", 2); - $this->RegisterVariableInteger("IdleCounter", "IdleCounter", "", 0); - $this->SetValue("IdleCounter", 0); - - // Initialisiere Idle - $this->SetValue("Idle", true); - - $this->RegisterTimer("Timer_Do_UserCalc_Battery",$this->ReadPropertyInteger("Interval")*1000,"IPS_RequestAction(" .$this->InstanceID .', "Do_UserCalc", "");'); - - - } - - public function ApplyChanges() - { - parent::ApplyChanges(); - - $batterieManagement = $this->ReadPropertyInteger("Batteriemanagement"); - $this->SetValue("Batteriemanagement_Variabel", $batterieManagement); - $this->SetTimerInterval("Timer_Do_UserCalc_Battery",$this->ReadPropertyInteger("Interval")*1000); - } - - - private function GeneratePowerSteps($additionalValue) - { - $maxleistung = $this->ReadPropertyInteger("MaxBatterieleistung"); - $stepSize = 250; // Schrittgröße - $stepSizeSmall = 50; // Kleine Schrittgröße - - // Array direkt als Range erzeugen (schneller als Schleife) - $array_powersteps = range(-$maxleistung, $maxleistung, $stepSize); - - // Nächstgelegenen Wert direkt bestimmen (rundet auf den nächsten Step) - $closestValue = round($additionalValue / $stepSize) * $stepSize; - - // Falls der Wert nicht im Bereich liegt, abbrechen - if (!in_array($closestValue, $array_powersteps)) { - return $array_powersteps; - } - - // Index des gefundenen Werts suchen - $index = array_search($closestValue, $array_powersteps); - - // Zusätzliche Werte berechnen und auf MaxLeistung begrenzen - $newValues = array_filter([ - $closestValue - 4 * $stepSizeSmall, - $closestValue - 3 * $stepSizeSmall, - $closestValue - 2 * $stepSizeSmall, - $closestValue - $stepSizeSmall, - $closestValue, - $closestValue + $stepSizeSmall, - $closestValue + 2 * $stepSizeSmall, - $closestValue + 3 * $stepSizeSmall, - $closestValue + 4 * $stepSizeSmall, - ], function ($value) use ($maxleistung) { - return $value >= -$maxleistung && $value <= $maxleistung; - }); - - // Effizienteres Einfügen der Werte (direkt an der Stelle) - array_splice($array_powersteps, $index, 1, $newValues); - - return $array_powersteps; - } - - - - -public function RequestAction($Ident, $Value) -{ - switch ($Ident) { - - case "SetAktuelle_Leistung": - $this->SetValue("Power", (int)$Value); - break; - - case "GetCurrentData": - $this->SetValue("Is_Peak_Shaving", (bool)$Value); - break; - - - case "Do_UserCalc": - - $this->SetAktuelle_Leistung($this->GetValue("Power")); - $this->GetCurrentData($this->GetValue("Is_Peak_Shaving")); - break; - - default: - throw new Exception("Invalid Ident"); - } -} - - public function SetAktuelle_Leistung(int $power) - { - $y = (-1)*$power; - - if ($y < 0) { - - // Laden - $uVarID = $this->ReadPropertyInteger("Batteriespannung"); - if ($uVarID > 0 && IPS_VariableExists($uVarID)) { - $U = GetValue($uVarID); - if ($U > 0) { - $lade_strom = abs($y) / $U; - } else { - $lade_strom = 0; - } - } else { - $lade_strom = 0; - } - - $this->SetValue("Ladestrom", $lade_strom); - $this->SetValue("Entladestrom", 0); - $this->SetValue("Batteriespannung_laden_entladen", 100); - - } elseif ($y > 0) { - - // Entladen - $uVarID = $this->ReadPropertyInteger("Batteriespannung"); - if ($uVarID > 0 && IPS_VariableExists($uVarID)) { - $U = GetValue($uVarID); - if ($U > 0) { - $entlade_strom = $y / $U; - } else { - $entlade_strom = 0; - } - } else { - $entlade_strom = 0; - } - - $this->SetValue("Entladestrom", $entlade_strom); - $this->SetValue("Ladestrom", 0); - $this->SetValue("Batteriespannung_laden_entladen", 5); - - } else { - - // Kein Stromfluss (Leistung = 0) - $this->SetValue("Ladestrom", 0); - $this->SetValue("Entladestrom", 0); - } - - - $batterieManagement = $this->ReadPropertyInteger("Batteriemanagement"); - // Wechselrichter steuert das Laden/Entladen der Batterie - if ($batterieManagement == 2) { - $this->SetValue("Ladestrom", 0); - $this->SetValue("Entladestrom", 0); - return; - } - - - // Prüfe auf Änderung der Leistung im Vergleich zur letzten Einstellung - $lastPower = GetValue($this->GetIDForIdent("Aktuelle_Leistung")); - if ($power != $lastPower) { - $this->SetValue("Idle", false); - $this->SetValue( - "IdleCounter", - $this->ReadPropertyInteger("IdleCounterMax") - ); - } - - // Setze die neue aktuelle Leistung - $this->SetValue("Aktuelle_Leistung", $power); - $this->SetValue("Bezogene_Energie", ($this->GetValue("Bezogene_Energie") + ($this->GetValue("Aktuelle_Leistung")*($this->ReadPropertyInteger("Interval")/3600)))); - - // IdleCounter verarbeiten - $this->ProcessIdleCounter(); - - - } - - - public function GetCurrentData(bool $Peak) - { - - - $ct = GetValue($this->ReadPropertyInteger("Kotnrolle_Selling_CT")); - $sell_Ct = $this->GetValue("Batteriemanagement_Variabel"); - - - if ($ct != $sell_Ct) { - $this->SetValue("Schreibkontrolle", false); - } else { - $this->SetValue("Schreibkontrolle", true); - } - - - - //$a = 2.54 * pow($V, 2) - 252 * $V + 6255.4; - $ladestrom_1 = $this->GetValue("Ladestrom"); - $entadestrom_1 = $this->GetValue("Entladedestrom"); - /* - if ($ladestrom_1 > 0 ) { - $V = GetValue($this->ReadPropertyInteger("Batteriespannung")) - 1; - }elseif ($entadestrom_1 > 0) { - $V = GetValue($this->ReadPropertyInteger("Batteriespannung")) + 1; - } else { - - $V = GetValue($this->ReadPropertyInteger("Batteriespannung")); - } - */ - IPS_LogMessage("Batterie", "Currentdata"); - $array_powersteps = $this->GeneratePowerSteps($this->GetValue("Aktuelle_Leistung")); - $aufdasnachladen = $this->ReadPropertyFloat("AufdasNachladen"); - $minimumentladen = $this->ReadPropertyFloat("MinimumEntladen"); - $maxleistung = $this->ReadPropertyInteger("MaxBatterieleistung"); - $dummy_array = []; - - - $batterieladezustand = GetValue($this->ReadPropertyInteger("Batterie_Ladezustand")); - - - - $filtered_powersteps_entladen = []; - if ($this->ReadPropertyInteger("Batteriemanagement") == 2) { - $dummy_array[] = 0; - return $this->SetValue("PowerSteps", json_encode($dummy_array)); - } - - $netzbezug = GetValue($this->ReadPropertyInteger("Netzbezug")); - if (abs($netzbezug) > $maxleistung) { - $netzbezug = $maxleistung * (-1); - } - - if($batterieladezustand>(5+$aufdasnachladen)){ - - $this->SetValue("Hysterese", false); - - }elseif($batterieladezustand<=$aufdasnachladen){ - $this->SetValue("Hysterese", true); - } - - $hyst = $this->GetValue("Hysterese"); - - if($Peak){ - IPS_LogMessage("Batterie", "Im if teil"); - - if($batterieladezustand>$aufdasnachladen && $hyst==false){ - - $dummy_array[] = $netzbezug; - $this->SetValue("PowerSteps", json_encode($dummy_array)); - - }elseif($batterieladezustand>$aufdasnachladen && $hyst==true){ - - - $filtered_powersteps = array_filter($array_powersteps, function ($value) { - return $value <= 0; - }); - $filtered_powersteps_laden = array_values($filtered_powersteps); - $this->SetValue("PowerSteps", json_encode($filtered_powersteps_laden)); - - }elseif($batterieladezustand>$minimumentladen){ - - $this->SetValue("PowerSteps", json_encode($array_powersteps)); - } - else{ - - $filtered_powersteps = array_filter($array_powersteps, function ($value) { - return $value >= 0; - }); - $filtered_powersteps_laden = array_values($filtered_powersteps); - $this->SetValue("PowerSteps", json_encode($filtered_powersteps_laden)); - } - - }else{ - IPS_LogMessage("Batterie", "Im else teil"); - - - if($batterieladezustand>199.9){ - IPS_LogMessage("Batterie", "im 1"); - - $filtered_powersteps = array_filter($array_powersteps, function ($value) { - return $value <= 0; - }); - $filtered_powersteps_laden = array_values($filtered_powersteps); - $this->SetValue("PowerSteps", json_encode($filtered_powersteps_laden)); - - }elseif($batterieladezustand>$aufdasnachladen && $hyst==false){ - - $this->SetValue("PowerSteps", json_encode($array_powersteps)); - IPS_LogMessage("Batterie", "im 2"); - - - }elseif($batterieladezustand>=$aufdasnachladen && $hyst==true){ - - $filtered_powersteps = array_filter($array_powersteps, function ($value) { - return $value >= 0; - }); - $filtered_powersteps_laden = array_values($filtered_powersteps); - $this->SetValue("PowerSteps", json_encode($filtered_powersteps_laden)); - - - }elseif($batterieladezustand<$aufdasnachladen){ - - $dummy_array[] = $this->ReadPropertyInteger("MaxNachladen"); - $this->SetValue("PowerSteps", json_encode($dummy_array)); - IPS_LogMessage("Batterie", "im 3"); - - } - - } - - } - - - - - private function CheckIdle($power) - { - $lastpower = GetValue("Aktuelle_Leistung"); - if ($lastpower != GetValue("Aktuelle_Leistung")) { - $this->SetValue("Idle", false); - $this->SetValue( - "IdleCounter", - $this->ReadPropertyInteger("IdleCounterMax") - ); - } - // IdleCounter auslesen und verarbeiten - $idleCounter = $this->GetValue("IdleCounter"); - if ($idleCounter > 0) { - $this->SetValue("Idle", false); - $this->SetValue("IdleCounter", $idleCounter - 1); - } else { - $this->SetValue("Idle", true); - } - } - - - private function ProcessIdleCounter() - { - // IdleCounter auslesen und verarbeiten - $idleCounter = $this->GetValue("IdleCounter"); - if ($idleCounter > 0) { - $this->SetValue("Idle", false); - $this->SetValue("IdleCounter", $idleCounter - 1); - } else { - $this->SetValue("Idle", true); - } - } - -} -?> diff --git a/PV_Forecast_plotmemory/form.json b/PV_Forecast_plotmemory/form.json deleted file mode 100644 index ff547a2..0000000 --- a/PV_Forecast_plotmemory/form.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "elements": [ - { "type": "ValidationTextBox", "name": "URL", "caption": "Solcast URL" }, - { "type": "SelectVariable", "name": "ActualVariableID", "caption": "Ist-Produktion Variable (Leistung)" }, - { "type": "CheckBox", "name": "ActualIsWatt", "caption": "Istwerte sind in Watt (in kW umrechnen)" }, - - { - "type": "Select", - "name": "RefreshMode", - "caption": "Forecast-Aktualisierung", - "options": [ - { "caption": "Alle X Minuten", "value": "interval" }, - { "caption": "Einmal täglich (Uhrzeit)", "value": "daily" } - ] - }, - { "type": "NumberSpinner", "name": "RefreshMinutes", "caption": "Intervall (Minuten)", "minimum": 1, "maximum": 240 }, - { "type": "ValidationTextBox", "name": "RefreshTime", "caption": "Tägliche Uhrzeit (HH:MM, z.B. 00:01)" }, - - { "type": "CheckBox", "name": "EnableDailySnapshots", "caption": "Tägliche Tages-Plots speichern" }, - { "type": "ValidationTextBox", "name": "SnapshotTime", "caption": "Snapshot-Uhrzeit (HH:MM, z.B. 23:59)" }, - { "type": "NumberSpinner", "name": "KeepDays", "caption": "Aufbewahrung (Tage)", "minimum": 1, "maximum": 365 } - ], - "actions": [ - { - "type": "Button", - "caption": "Forecast jetzt aktualisieren", - "onClick": "IPS_RequestAction($id, \"UpdateForecast\", 0);" - } - ] -} diff --git a/PV_Forecast_plotmemory/module.html b/PV_Forecast_plotmemory/module.html deleted file mode 100644 index be31056..0000000 --- a/PV_Forecast_plotmemory/module.html +++ /dev/null @@ -1,186 +0,0 @@ - - - - - PV_Forecast_plotmemory - - - - - - -
-
Lade…
-
- -
- - - - diff --git a/PV_Forecast_plotmemory/module.json b/PV_Forecast_plotmemory/module.json deleted file mode 100644 index 1b034b7..0000000 --- a/PV_Forecast_plotmemory/module.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "id": "{DF3B17A3-C2F7-0B57-F632-34668D9206E6}", - "name": "PV_Forecast_plotmemory ", - "type": 3, - "vendor": "Belevo AG", - "aliases": [], - "parentRequirements": [], - "childRequirements": [], - "implemented": [], - "prefix": "PVF", - "url": "" -} diff --git a/PV_Forecast_plotmemory/module.php b/PV_Forecast_plotmemory/module.php deleted file mode 100644 index 3f92ed6..0000000 --- a/PV_Forecast_plotmemory/module.php +++ /dev/null @@ -1,589 +0,0 @@ -RegisterPropertyString("URL", ""); - $this->RegisterPropertyInteger("ActualVariableID", 0); - $this->RegisterPropertyBoolean("ActualIsWatt", true); - - // Forecast fetch scheduler - $this->RegisterPropertyString("RefreshMode", "interval"); // interval | daily - $this->RegisterPropertyInteger("RefreshMinutes", 5); - $this->RegisterPropertyString("RefreshTime", "06:00"); // HH:MM - - // Daily snapshot - $this->RegisterPropertyBoolean("EnableDailySnapshots", true); - $this->RegisterPropertyString("SnapshotTime", "23:59"); // HH:MM - $this->RegisterPropertyInteger("KeepDays", 30); - - // Timer - $this->RegisterTimer("UpdateForecastTimer", 0, 'IPS_RequestAction($_IPS["TARGET"], "UpdateForecast", 0);'); - $this->RegisterTimer("SnapshotTimer", 0, 'IPS_RequestAction($_IPS["TARGET"], "SnapshotTick", 0);'); - - // WebHook endpoint - $this->RegisterHook("/hook/solcastcompare_plotmemory"); - } - - public function ApplyChanges() - { - parent::ApplyChanges(); - - // Tile Visualization aktivieren - $this->SetVisualizationType(1); - - // Forecast timer - $mode = $this->ReadPropertyString("RefreshMode"); - if ($mode === "interval") { - $mins = max(1, (int)$this->ReadPropertyInteger("RefreshMinutes")); - $this->SetTimerInterval("UpdateForecastTimer", $mins * 60 * 1000); - } else { - // daily check each minute - $this->SetTimerInterval("UpdateForecastTimer", 60 * 1000); - } - - // Snapshot timer - if ($this->ReadPropertyBoolean("EnableDailySnapshots")) { - $this->SetTimerInterval("SnapshotTimer", 60 * 1000); // every minute - } else { - $this->SetTimerInterval("SnapshotTimer", 0); - } - - $this->RegisterHook("/hook/solcastcompare"); - } - - public function RequestAction($Ident, $Value) - { - switch ($Ident) { - case "UpdateForecast": - $this->HandleForecastSchedule(); - return; - - case "SnapshotTick": - $this->HandleSnapshotSchedule(); - return; - - default: - throw new Exception("Unknown Ident: " . $Ident); - } - } - - // ----------------- Forecast scheduling ----------------- - - private function HandleForecastSchedule(): void - { - $mode = $this->ReadPropertyString("RefreshMode"); - - if ($mode === "interval") { - $this->UpdateForecast(); - return; - } - - // daily - $timeStr = trim($this->ReadPropertyString("RefreshTime")); // HH:MM - $targetSec = $this->ParseHHMMToSeconds($timeStr); - if ($targetSec === null) { - $this->SendDebug("Scheduler", "Ungueltige RefreshTime: " . $timeStr, 0); - return; - } - - $now = time(); - $nowSec = ((int)date("H", $now) * 3600) + ((int)date("i", $now) * 60); - - $lastRun = (int)$this->GetBuffer("LastDailyRunForecast"); - $today0 = strtotime("today"); - - if ($nowSec >= $targetSec && $lastRun < $today0) { - $this->SendDebug("Scheduler", "Taegliches Forecast-Update (" . $timeStr . ")", 0); - $this->UpdateForecast(); - $this->SetBuffer("LastDailyRunForecast", (string)$now); - } - } - - // ----------------- Snapshot scheduling ----------------- - - private function HandleSnapshotSchedule(): void - { - if (!$this->ReadPropertyBoolean("EnableDailySnapshots")) { - return; - } - - $timeStr = trim($this->ReadPropertyString("SnapshotTime")); - $targetSec = $this->ParseHHMMToSeconds($timeStr); - if ($targetSec === null) { - $this->SendDebug("Snapshot", "Ungueltige SnapshotTime: " . $timeStr, 0); - return; - } - - $now = time(); - $nowSec = ((int)date("H", $now) * 3600) + ((int)date("i", $now) * 60); - - $lastRun = (int)$this->GetBuffer("LastDailySnapshotRun"); - $today0 = strtotime("today"); - - if ($nowSec >= $targetSec && $lastRun < $today0) { - $this->SendDebug("Snapshot", "Tages-Snapshot wird erstellt (" . $timeStr . ")", 0); - - // Safety: ensure we have a forecast cached (if not, fetch once) - if ($this->GetBuffer("ForecastRaw") === "") { - $this->UpdateForecast(); - } - - $this->CreateDailySnapshotForDate(new DateTime('today', new DateTimeZone(date_default_timezone_get()))); - $this->CleanupOldSnapshots(); - - $this->SetBuffer("LastDailySnapshotRun", (string)$now); - } - } - - private function ParseHHMMToSeconds(string $hhmm): ?int - { - if (!preg_match('/^(\d{1,2}):(\d{2})$/', $hhmm, $m)) { - return null; - } - $h = (int)$m[1]; - $min = (int)$m[2]; - if ($h < 0 || $h > 23 || $min < 0 || $min > 59) { - return null; - } - return $h * 3600 + $min * 60; - } - - // ----------------- Forecast Fetch ----------------- - - private function UpdateForecast(): void - { - $url = trim($this->ReadPropertyString("URL")); - if ($url === "") { - $this->SendDebug("UpdateForecast", "URL ist leer", 0); - return; - } - - $json = @Sys_GetURLContent($url); - if ($json === false || $json === "") { - $this->SendDebug("UpdateForecast", "Leere Antwort von URL", 0); - return; - } - - $data = json_decode($json, true); - if (!is_array($data)) { - $this->SendDebug("UpdateForecast", "JSON decode fehlgeschlagen", 0); - return; - } - - $this->SetBuffer("ForecastRaw", $json); - $this->SetBuffer("ForecastTS", (string)time()); - $this->SendDebug("UpdateForecast", "Forecast aktualisiert", 0); - } - - // ----------------- Tile Visualization ----------------- - - public function GetVisualizationTile(): string - { - if ($this->GetBuffer("ForecastRaw") === "") { - $this->UpdateForecast(); - } - - $html = file_get_contents(__DIR__ . "/module.html"); - $html = str_replace("{{INSTANCE_ID}}", (string)$this->InstanceID, $html); - return $html; - } - - // ----------------- WebHook: data + download ----------------- - - protected function ProcessHookData() - { - $instance = isset($_GET["instance"]) ? (int)$_GET["instance"] : 0; - $action = isset($_GET["action"]) ? (string)$_GET["action"] : "data"; - - if ($instance !== $this->InstanceID) { - http_response_code(404); - echo "Wrong instance"; - return; - } - - if ($action === "data") { - $this->HandleHookData(); - return; - } - - if ($action === "download") { - $days = isset($_GET["days"]) ? max(1, min(365, (int)$_GET["days"])) : 7; - $this->HandleHookDownloadZip($days); - return; - } - - http_response_code(400); - echo "Unknown action"; - } - - private function HandleHookData(): void - { - $tzLocal = new DateTimeZone(date_default_timezone_get()); - $tzUtc = new DateTimeZone('UTC'); - - // Lokaler Tag - $startLocal = new DateTime('today', $tzLocal); - $endLocal = new DateTime('tomorrow', $tzLocal); - - $startLocalTs = $startLocal->getTimestamp(); - $endLocalTs = $endLocal->getTimestamp(); - - // Lokaler Tagesbereich als UTC-Grenzen (Solcast period_end ist UTC "Z") - $startUtcTs = (clone $startLocal)->setTimezone($tzUtc)->getTimestamp(); - $endUtcTs = (clone $endLocal)->setTimezone($tzUtc)->getTimestamp(); - - $nowTs = time(); - - $forecast = $this->GetForecastSeriesFilteredUtc($startUtcTs, $endUtcTs); - $actual = $this->GetActualSeriesFromArchive($startLocalTs, $endLocalTs, 1800, $nowTs, true); - - $out = [ - "meta" => [ - "forecast_cached_at" => (int)$this->GetBuffer("ForecastTS"), - "bucket_seconds" => 1800, - "actual_is_watt" => (bool)$this->ReadPropertyBoolean("ActualIsWatt"), - "start_local" => $startLocalTs * 1000, - "end_local" => $endLocalTs * 1000, - "now" => $nowTs * 1000 - ], - "series" => [ - "forecast" => $forecast, - "actual" => $actual - ] - ]; - - header("Content-Type: application/json; charset=utf-8"); - echo json_encode($out); - } - - // ----------------- Forecast series (Solcast) ----------------- - - private function GetForecastSeriesFilteredUtc(int $startUtcTs, int $endUtcTs): array - { - $raw = $this->GetBuffer("ForecastRaw"); - if ($raw === "") return []; - - $data = json_decode($raw, true); - if (!is_array($data)) return []; - - // forecast endpoint: forecasts; live endpoint: estimated_actuals - if (isset($data["forecasts"]) && is_array($data["forecasts"])) { - $rows = $data["forecasts"]; - } elseif (isset($data["estimated_actuals"]) && is_array($data["estimated_actuals"])) { - $rows = $data["estimated_actuals"]; - } else { - return []; - } - - $series = []; - foreach ($rows as $row) { - if (!isset($row["period_end"], $row["pv_power_rooftop"])) continue; - - $ts = strtotime($row["period_end"]); // UTC wegen Z - if ($ts === false) continue; - - if ($ts < $startUtcTs || $ts >= $endUtcTs) continue; - - // Solcast: kW - $series[] = [$ts * 1000, (float)$row["pv_power_rooftop"]]; - } - - usort($series, fn($a, $b) => $a[0] <=> $b[0]); - return $series; - } - - // ----------------- Actual series (Archive) ----------------- - // $cutAfterNow=true -> future buckets = null (live view) - // $cutAfterNow=false -> keep as much as possible (snapshot) - private function GetActualSeriesFromArchive(int $startTs, int $endTs, int $bucketSeconds, int $nowTs, bool $cutAfterNow): array - { - $varId = (int)$this->ReadPropertyInteger("ActualVariableID"); - if ($varId <= 0 || !IPS_VariableExists($varId)) return []; - - $archiveId = $this->GetArchiveInstanceID(); - if ($archiveId === 0) return []; - - if (!AC_GetLoggingStatus($archiveId, $varId)) { - $this->SendDebug("Actual", "Variable wird nicht geloggt im Archiv", 0); - return []; - } - - $logged = AC_GetLoggedValues($archiveId, $varId, $startTs, $endTs, 0); - if (!is_array($logged) || count($logged) === 0) { - return $this->BuildNullRaster($startTs, $endTs, $bucketSeconds); - } - - // bucketEnd -> [sum, count] - $buckets = []; - foreach ($logged as $row) { - if (!isset($row["TimeStamp"], $row["Value"])) continue; - - $ts = (int)$row["TimeStamp"]; - $bucketStart = intdiv($ts, $bucketSeconds) * $bucketSeconds; - $bucketEnd = $bucketStart + $bucketSeconds; - - if ($bucketEnd < $startTs || $bucketEnd > $endTs) continue; - - if (!isset($buckets[$bucketEnd])) $buckets[$bucketEnd] = [0.0, 0]; - $buckets[$bucketEnd][0] += (float)$row["Value"]; - $buckets[$bucketEnd][1] += 1; - } - - $series = []; - $isWatt = (bool)$this->ReadPropertyBoolean("ActualIsWatt"); - - for ($t = $startTs + $bucketSeconds; $t <= $endTs; $t += $bucketSeconds) { - - if ($cutAfterNow && $t > $nowTs + $bucketSeconds) { - $series[] = [$t * 1000, null]; - continue; - } - - if (!isset($buckets[$t]) || $buckets[$t][1] <= 0) { - $series[] = [$t * 1000, null]; - continue; - } - - $avg = $buckets[$t][0] / $buckets[$t][1]; - - // W -> kW - if ($isWatt) $avg = $avg / 1000.0; - - $series[] = [$t * 1000, $avg]; - } - - return $series; - } - - private function BuildNullRaster(int $startTs, int $endTs, int $bucketSeconds): array - { - $series = []; - for ($t = $startTs + $bucketSeconds; $t <= $endTs; $t += $bucketSeconds) { - $series[] = [$t * 1000, null]; - } - return $series; - } - - private function GetArchiveInstanceID(): int - { - $list = IPS_GetInstanceListByModuleID(self::ARCHIVE_GUID); - return (is_array($list) && count($list) > 0) ? (int)$list[0] : 0; - } - - // ----------------- Daily snapshot creation ----------------- - - private function GetStorageDir(): string - { - $dir = __DIR__ . "/storage"; - if (!is_dir($dir)) { - @mkdir($dir, 0775, true); - } - return $dir; - } - - private function CreateDailySnapshotForDate(DateTime $dayLocal): void - { - $tzLocal = new DateTimeZone(date_default_timezone_get()); - $tzUtc = new DateTimeZone('UTC'); - - // dayLocal should be "today" in local TZ - $dayLocal->setTimezone($tzLocal); - - $startLocal = (clone $dayLocal); - $startLocal->setTime(0, 0, 0); - - $endLocal = (clone $startLocal); - $endLocal->modify('+1 day'); - - $startLocalTs = $startLocal->getTimestamp(); - $endLocalTs = $endLocal->getTimestamp(); - - $startUtcTs = (clone $startLocal)->setTimezone($tzUtc)->getTimestamp(); - $endUtcTs = (clone $endLocal)->setTimezone($tzUtc)->getTimestamp(); - - $nowTs = time(); // snapshot time (typically 23:59) - - $forecast = $this->GetForecastSeriesFilteredUtc($startUtcTs, $endUtcTs); - $actual = $this->GetActualSeriesFromArchive($startLocalTs, $endLocalTs, 1800, $nowTs, false); - - $dateStr = $startLocal->format('Y-m-d'); - - $payload = [ - "meta" => [ - "date" => $dateStr, - "created_at" => time(), - "forecast_cached_at" => (int)$this->GetBuffer("ForecastTS"), - "bucket_seconds" => 1800, - "actual_is_watt" => (bool)$this->ReadPropertyBoolean("ActualIsWatt"), - "timezone" => $tzLocal->getName() - ], - "series" => [ - "forecast" => $forecast, - "actual" => $actual - ] - ]; - - $dir = $this->GetStorageDir(); - - // Save JSON - $jsonPath = $dir . "/" . $dateStr . ".json"; - @file_put_contents($jsonPath, json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - - // Save HTML plot - $htmlPath = $dir . "/" . $dateStr . ".html"; - @file_put_contents($htmlPath, $this->BuildSnapshotHtml($payload)); - - $this->SendDebug("Snapshot", "Gespeichert: " . basename($jsonPath) . " / " . basename($htmlPath), 0); - } - - private function BuildSnapshotHtml(array $payload): string - { - $date = $payload["meta"]["date"] ?? "unbekannt"; - $forecastJson = json_encode($payload["series"]["forecast"] ?? []); - $actualJson = json_encode($payload["series"]["actual"] ?? []); - - // Hinweis: nutzt Highcharts CDN. (Wenn du offline öffnen willst, sag Bescheid, dann liefern wir highcharts.js lokal mit.) - return ' - - - - PV Snapshot ' . htmlspecialchars($date) . ' - - - - -

PV: Erwartung vs. Tatsächlich (' . htmlspecialchars($date) . ')

-
Gespeichert: ' . date('d.m.Y H:i:s', (int)($payload["meta"]["created_at"] ?? time())) . '
-
- - - -'; - } - - private function CleanupOldSnapshots(): void - { - $keep = max(1, (int)$this->ReadPropertyInteger("KeepDays")); - $dir = $this->GetStorageDir(); - - $cutoff = strtotime("today") - ($keep * 86400); - - foreach (glob($dir . "/*.json") as $file) { - $base = basename($file, ".json"); // YYYY-MM-DD - $ts = strtotime($base); - if ($ts !== false && $ts < $cutoff) { - @unlink($file); - $html = $dir . "/" . $base . ".html"; - if (is_file($html)) @unlink($html); - } - } - } - - // ----------------- ZIP download ----------------- - - private function HandleHookDownloadZip(int $days): void - { - $dir = $this->GetStorageDir(); - - $dates = []; - for ($i = 0; $i < $days; $i++) { - $d = new DateTime('today', new DateTimeZone(date_default_timezone_get())); - $d->modify("-{$i} day"); - $dates[] = $d->format("Y-m-d"); - } - - $zipPath = sys_get_temp_dir() . "/pv_forecast_" . $this->InstanceID . "_" . time() . ".zip"; - $zip = new ZipArchive(); - if ($zip->open($zipPath, ZipArchive::CREATE) !== true) { - http_response_code(500); - echo "Could not create zip"; - return; - } - - $added = 0; - foreach (array_reverse($dates) as $dateStr) { - $jsonFile = $dir . "/" . $dateStr . ".json"; - $htmlFile = $dir . "/" . $dateStr . ".html"; - - if (is_file($htmlFile)) { - $zip->addFile($htmlFile, $dateStr . ".html"); - $added++; - } - if (is_file($jsonFile)) { - $zip->addFile($jsonFile, $dateStr . ".json"); - $added++; - } - } - - $zip->close(); - - if ($added === 0) { - @unlink($zipPath); - http_response_code(404); - echo "No snapshots found"; - return; - } - - header("Content-Type: application/zip"); - header('Content-Disposition: attachment; filename="pv_plots_' . $this->InstanceID . '_' . $days . 'days.zip"'); - header("Content-Length: " . filesize($zipPath)); - - readfile($zipPath); - @unlink($zipPath); - } - - // ----------------- Hook registration ----------------- - - private function RegisterHook(string $Hook) - { - $ids = IPS_GetInstanceListByModuleID(self::WEBHOOK_GUID); - if (count($ids) === 0) return; - - $hookID = (int)$ids[0]; - $hooks = json_decode(IPS_GetProperty($hookID, "Hooks"), true); - if (!is_array($hooks)) $hooks = []; - - foreach ($hooks as $h) { - if (isset($h["Hook"]) && $h["Hook"] === ltrim($Hook, "/")) { - return; - } - if (isset($h["Hook"]) && $h["Hook"] === $Hook) { - return; - } - } - - $hooks[] = ["Hook" => $Hook, "TargetID" => $this->InstanceID]; - IPS_SetProperty($hookID, "Hooks", json_encode($hooks)); - IPS_ApplyChanges($hookID); - } -} diff --git a/PV_Forecast_plotmemory/readme.md b/PV_Forecast_plotmemory/readme.md deleted file mode 100644 index bc8dd0b..0000000 --- a/PV_Forecast_plotmemory/readme.md +++ /dev/null @@ -1,59 +0,0 @@ -# PV_Visu - -Visualisierung des Eigenverbrauchs: Tages-Quoten für PV-Produktion vs. Einspeisung und Verbrauch vs. Netz-Bezug. - -## Inhaltsverzeichnis - -1. [Funktionsumfang](#funktionsumfang) -2. [Voraussetzungen](#voraussetzungen) -3. [Installation](#installation) -4. [Instanz einrichten](#instanz-einrichten) -5. [WebFront](#webfront) -6. [PHP-Befehlsreferenz](#php-befehlsreferenz) - -## Funktionsumfang - -- Anzeige von Tages-Quoten (%) -- Produktion: Eigenverbrauch vs. Einspeisung -- Verbrauch: PV-Anteil vs. Netz-Anteil -- Zwei Balkendiagramme -- Absolute Tages-Summen (kWh) - -## Voraussetzungen - -- IP-Symcon ≥ 7.1 -- Archiv-Modul aktiviert -- Vier kWh-Zähler-Variablen - -## Installation - -1. **Module Store** → Suche nach „PV_Visu“ und installieren -2. **Alternativ**: Unter Module → Repositories folgende URL hinzufügen: - ``` - https://github.com/DeinRepo/PV_Visu.git - ``` - und Modul neu einlesen. - -## Instanz einrichten - -- **Instanz hinzufügen** → Filter: „PV_Visu“ -- Variablen zuweisen: - - | Property | Beschreibung | - | -------------- | -------------------------- | - | VarProduction | PV-Produktionszähler (kWh) | - | VarConsumption | Gesamtverbrauch (kWh) | - | VarFeedIn | Einspeisung (kWh) | - | VarGrid | Netz-Bezug (kWh) | - -## WebFront - -- **Tile-Typ:** PV_Visu -- Balken 1 (Grün): Produktion -- Balken 2 (Orange/Rot): Verbrauch - -## PHP-Befehlsreferenz - -```php -IPS_RequestAction($InstanceID, 'update', true); -``` diff --git a/PV_Visu/form.json b/PV_Visu/form.json deleted file mode 100644 index 1acde8f..0000000 --- a/PV_Visu/form.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "elements": [ - { "type": "SelectVariable", "name": "VarProduction", "caption": "Produktion (kWh)" }, - { "type": "SelectVariable", "name": "VarConsumption", "caption": "Verbrauch (kWh)" }, - { "type": "SelectVariable", "name": "VarFeedIn", "caption": "Einspeisung (kWh)" }, - { "type": "SelectVariable", "name": "VarGrid", "caption": "Bezug Netz (kWh)" } - ], - "actions": [] -} diff --git a/PV_Visu/module.html b/PV_Visu/module.html deleted file mode 100644 index 4d2975c..0000000 --- a/PV_Visu/module.html +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - -
-
-
Produktion (Eigenverbrauch / Einspeisung)
-
-
-
-
-
-
-
-
Verbrauch (PV / Netz)
-
-
-
-
-
-
-
- - - diff --git a/PV_Visu/module.json b/PV_Visu/module.json deleted file mode 100644 index dfbdcba..0000000 --- a/PV_Visu/module.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "id": "{DDE89CBE-4411-5FF4-4931-14204E05CAD0}", - "name": "PV_Visu", - "type": 3, - "vendor": "Belevo AG", - "aliases": [], - "parentRequirements": [], - "childRequirements": [], - "implemented": [], - "prefix": "", - "url": "" -} diff --git a/PV_Visu/module.php b/PV_Visu/module.php deleted file mode 100644 index 475b3be..0000000 --- a/PV_Visu/module.php +++ /dev/null @@ -1,99 +0,0 @@ -RegisterPropertyInteger('VarProduction', 0); - $this->RegisterPropertyInteger('VarConsumption', 0); - $this->RegisterPropertyInteger('VarFeedIn', 0); - $this->RegisterPropertyInteger('VarGrid', 0); - - $this->RegisterVariableString('JSONData', 'Visualisierungsdaten', '', 0); - IPS_SetHidden($this->GetIDForIdent('JSONData'), true); - - $this->SetVisualizationType(1); // HTML SDK Tile - } - - public function ApplyChanges() - { - parent::ApplyChanges(); - - foreach (['VarProduction', 'VarConsumption', 'VarFeedIn', 'VarGrid'] as $prop) { - $vid = $this->ReadPropertyInteger($prop); - if ($vid > 0) { - $this->RegisterMessage($vid, VM_UPDATE); - } - } - - $this->UpdateData(); // Initial - } - - public function MessageSink($TimeStamp, $SenderID, $Message, $Data) - { - if ($Message === VM_UPDATE) { - $this->UpdateData(); - } - } - - public function GetVisualizationTile() - { - $initialData = ''; - $html = file_get_contents(__DIR__ . '/module.html'); - return $html . $initialData; - } - - public function RequestAction($Ident, $Value) - { - if ($Ident === 'update') { - return $this->UpdateData(); // Rückgabe für Visualisierung - } - throw new \Exception("Unknown Ident: $Ident"); - } - - public function UpdateData() - { - $start = strtotime('today 00:00'); - $end = time(); - - $prod = $this->GetDailyTotal($this->ReadPropertyInteger('VarProduction'), $start, $end); - $cons = $this->GetDailyTotal($this->ReadPropertyInteger('VarConsumption'), $start, $end); - $feed = $this->GetDailyTotal($this->ReadPropertyInteger('VarFeedIn'), $start, $end); - $grid = $this->GetDailyTotal($this->ReadPropertyInteger('VarGrid'), $start, $end); - - $prodCons = $prod > 0 ? (($cons - $grid) / $prod) * 100 : 0; - $prodFeed = $prod > 0 ? 100 - $prodCons : 0; - $consPV = $cons > 0 ? min($prod, ($cons - $grid)) / $cons * 100 : 0; - $consGrid = $cons > 0 ? 100 - $consPV : 0; - - $data = [ - 'prodCons' => round($prodCons, 1), - 'prodFeed' => round($prodFeed, 1), - 'consPV' => round($consPV, 1), - 'consGrid' => round($consGrid, 1), - 'value' => [ - 'prod' => round($prod, 2), - 'cons' => round($cons, 2), - 'feed' => round($feed, 2), - 'grid' => round($grid, 2), - ], - ]; - - $json = json_encode($data); - SetValueString($this->GetIDForIdent('JSONData'), $json); - return $data; - } - - private function GetDailyTotal(int $varID, int $start, int $end) - { - if ($varID <= 0) return 0.0; - - $archiveID = @IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0]; - if (!$archiveID) return 0.0; - - $values = @AC_GetAggregatedValues($archiveID, $varID, 1, $start, $end, 1); - return isset($values[0]['Avg']) ? (float)$values[0]['Avg'] : 0.0; - } -} \ No newline at end of file diff --git a/PV_Visu/readme.md b/PV_Visu/readme.md deleted file mode 100644 index bc8dd0b..0000000 --- a/PV_Visu/readme.md +++ /dev/null @@ -1,59 +0,0 @@ -# PV_Visu - -Visualisierung des Eigenverbrauchs: Tages-Quoten für PV-Produktion vs. Einspeisung und Verbrauch vs. Netz-Bezug. - -## Inhaltsverzeichnis - -1. [Funktionsumfang](#funktionsumfang) -2. [Voraussetzungen](#voraussetzungen) -3. [Installation](#installation) -4. [Instanz einrichten](#instanz-einrichten) -5. [WebFront](#webfront) -6. [PHP-Befehlsreferenz](#php-befehlsreferenz) - -## Funktionsumfang - -- Anzeige von Tages-Quoten (%) -- Produktion: Eigenverbrauch vs. Einspeisung -- Verbrauch: PV-Anteil vs. Netz-Anteil -- Zwei Balkendiagramme -- Absolute Tages-Summen (kWh) - -## Voraussetzungen - -- IP-Symcon ≥ 7.1 -- Archiv-Modul aktiviert -- Vier kWh-Zähler-Variablen - -## Installation - -1. **Module Store** → Suche nach „PV_Visu“ und installieren -2. **Alternativ**: Unter Module → Repositories folgende URL hinzufügen: - ``` - https://github.com/DeinRepo/PV_Visu.git - ``` - und Modul neu einlesen. - -## Instanz einrichten - -- **Instanz hinzufügen** → Filter: „PV_Visu“ -- Variablen zuweisen: - - | Property | Beschreibung | - | -------------- | -------------------------- | - | VarProduction | PV-Produktionszähler (kWh) | - | VarConsumption | Gesamtverbrauch (kWh) | - | VarFeedIn | Einspeisung (kWh) | - | VarGrid | Netz-Bezug (kWh) | - -## WebFront - -- **Tile-Typ:** PV_Visu -- Balken 1 (Grün): Produktion -- Balken 2 (Orange/Rot): Verbrauch - -## PHP-Befehlsreferenz - -```php -IPS_RequestAction($InstanceID, 'update', true); -``` diff --git a/SofarWechselrichter/README.md b/SofarWechselrichter/README.md deleted file mode 100644 index 4fa144c..0000000 --- a/SofarWechselrichter/README.md +++ /dev/null @@ -1,105 +0,0 @@ -2. Starte IP-Symcon neu bzw. klicke in der Konsole auf „Module aktualisieren“. - -Anschließend erscheint in der Instanzliste der neue Gerätetyp **Sofar Wechselrichter**. - ---- - -## Konfiguration - -1. **Instanz anlegen** -- Gehe in der IPS-Konsole auf „Instanzen hinzufügen“ → Hersteller: (falls sichtbar) → Modul: **Sofar Wechselrichter**. -- Vergib einen sinnvollen Namen und eine Beschreibung. - -2. **Einstellungen in den Eigenschaften** -- **Logger-Seriennummer**: - Gib hier die „Logger-Nummer“ deines Sofar-Wechselrichters ein (Dezimal). -- **Abfragezyklus (Sekunden)**: - Intervall in Sekunden, in dem zyklisch alle eingetragenen Register abgefragt werden. - Ein Wert von `0` deaktiviert den Timer (keine zyklischen Abfragen). -- **Register-Tabelle**: - Hier definierst du beliebig viele Zeilen, jeweils mit: - 1. **Register-Nummer** (dezimal) - 2. **Bezeichnung** (z. B. „Gesamtproduktion“ oder „Spannung Phase L1“) - 3. **Skalierungs-faktor** (z. B. `0.1`, `1`, `10` usw.) - - Wird beispielsweise für Register `1476` als Bezeichnung „Gesamtproduktion“ mit Skalierungsfaktor `1` eingetragen, - so liest das Modul alle 60 Sekunden (oder den von dir gewählten Zyklus) Register 1476, - multipliziert das rohe UINT16-Ergebnis mit `1` und legt den Wert in einer IPS-Variable an. - -3. **Speichern/Übernehmen** -Klicke auf „Übernehmen“, um die Änderungen zu übernehmen. -Das Modul legt automatisch untergeordnete IPS-Variablen an: - -- **Vorheriger Wert (Register 1160)** - → INT16BE, unverändert (analog zur alten Node-RED-Logik). - Diese Variable heißt intern `Vorheriger Wert` und wird automatisch gepflegt. -- **Alle weiteren Einträge aus der Register-Tabelle** - → Für jede Zeile wird eine Float-Variable mit der von dir angegebenen „Bezeichnung“ angelegt. - Der Variablen-Identifier lautet automatisch `Reg` (z. B. `Reg1476`). - ---- - -## Funktionsweise - -1. **Initialisierung (ApplyChanges)** -- Liest den Abfragezyklus aus den Moduleigenschaften und initialisiert den Timer. -- Legt eine Integer-Variable `Vorheriger Wert` (Reg 1160) an. -- Legt für jede Zeile in der Register-Tabelle eine Float-Variable an (Ident `Reg`). - -2. **Zyklische Abfrage (Timer-Callback)** -- **Register 1160 (INT16BE)** - → Wird als „Vorheriger Wert“ in die Variable geschrieben (signed interpretiert). -- **Alle weiteren Register aus der Tabelle** - → Jedes Register wird per Modbus-ähnlichem TCP-Paketaustausch abgefragt, als UINT16 ausgelesen, - mit dem angegebenen Skalierungsfaktor multipliziert und in der zugehörigen Float-Variable gespeichert. - -3. **Kommunikation** -- TCP-Verbindung zu `192.168.0.0:8899` (feste IP im Code). -- Der Aufruf von `readRegister()` baut ein „Out_Frame“ wie in Node-RED, - rechnet CRC16-Modbus über die letzten 6 Bytes, hängt eine Summen-Checksum + 0x15 an, - sendet das Paket, liest die Antwort, schneidet exakt 2 Daten-Bytes heraus und liefert sie zurück. - ---- - -## Beispiel: Register 1476 („Gesamtproduktion“) - -- **Register-Tabelle** -| Register‐Nummer | Bezeichnung | Skalierungsfaktor | -| --------------: | :------------------ | ----------------: | -| 1476 | Gesamtproduktion | 1 | - -- **Ergebnis** -- Eine Float-Variable mit der Bezeichnung „Gesamtproduktion“ wird angelegt. -- Wenn der PollInterval auf `60` Sekunden steht, liest das Modul alle 60 Sekunden das Register 1476, - skaliert mit 1 und schreibt den numerischen Wert in `Reg1476`. - ---- - -## Fehlersuche - -- Falls die Variable „Vorheriger Wert“ immer denselben Wert liefert oder ein Lesefehler auftritt, prüfe bitte: -1. **Logger-Nummer**: Ist sie korrekt (Dezimal)? -2. **Netzwerk/Firewall**: Kann Symcon die Adresse `192.168.0.100:8899` erreichen? -3. **Debug-Ausgaben**: - – Öffne in der Konsole „Kernel-Log“ → Filter „SofarWechselrichter“. - – Dort werden WARNs und ERRs protokolliert, falls z. B. keine Antwort kommt oder das Datenpaket inkorrekt ist. - -- Falls du andere Datentypen brauchst (z. B. INT16 für Register außerhalb 1160), definiere sie analog als separate Zeile: -– Trage die `Register‐Nummer` ein, gib als Skalierungsfaktor `1` (oder `0.1` etc.) an. -– Der absolute Rohwert wird stets als UINT16 interpretiert (0–65535). -– Solltest du negative INT16 benötigen, kannst du nachträglich einfach die Variable „Vorheriger Wert“ (Reg 1160) - als Beispiel nehmen und in einem Script umrechnen (Werte über 32767 → –32768 + Rest). - ---- - -## Versionshistorie - -- **1.0** -- Erstveröffentlichung: - • Zyklische Abfrage beliebiger Register in einer Matrix konfigurieren - • Automatische Anlage von Variablen für jeden Eintrag - • Spezieller „Vorheriger Wert“ (Register 1160 als INT16) - ---- - -*Ende der Dokumentation.* diff --git a/SofarWechselrichter/form.json b/SofarWechselrichter/form.json deleted file mode 100644 index c7c602b..0000000 --- a/SofarWechselrichter/form.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "elements": [ - { - "type": "Label", - "caption": "Sofar Wechselrichter Konfiguration" - }, - { - "type": "ValidationTextBox", - "name": "IPAddress", - "caption": "Inverter IP-Adresse" - }, - { - "type": "ValidationTextBox", - "name": "LoggerNumber", - "caption": "Logger-Seriennummer" - }, - { - "type": "NumberSpinner", - "name": "PollInterval", - "caption": "Abfragezyklus (Sekunden)", - "minimum": 1, - "suffix": "s" - }, - { - "type": "List", - "name": "Registers", - "caption": "Register-Tabelle", - "add": "Neues Register hinzufügen", - "delete": "Lösche Eintrag", - "columns": [ - { - "caption": "Register-Nummer", - "name": "RegisterNumber", - "width": "200px", - "add": 0, - "edit": { - "type": "NumberSpinner", - "minimum": 0 - } - }, - { - "caption": "Bezeichnung", - "name": "Label", - "width": "300px", - "add": "", - "edit": { - "type": "ValidationTextBox" - } - }, - { - "caption": "Skalierungs-faktor", - "name": "ScalingFactor", - "width": "200px", - "add": 1, - "edit": { - "type": "NumberSpinner", - "digits": 4, - "minimum": -999999, - "maximum": 999999 - } - }, - { - "caption": "Endian", - "name": "Endian", - "width": "80px", - "add": "BE", - "edit": { - "type": "Select", - "options": [ - { "caption": "BE", "value": "BE" }, - { "caption": "LE", "value": "LE" } - ] - } - }, - { - "caption": "Bit-Länge", - "name": "BitLength", - "width": "80px", - "add": "16", - "edit": { - "type": "Select", - "options": [ - { "caption": "16 Bit", "value": "16" }, - { "caption": "32 Bit", "value": "32" }, - { "caption": "64 Bit", "value": "64" } - ] - } - }, - { - "caption": "Signed/Unsigned", - "name": "Signedness", - "width": "100px", - "add": "Unsigned", - "edit": { - "type": "Select", - "options": [ - { "caption": "Unsigned", "value": "Unsigned" }, - { "caption": "Signed", "value": "Signed" } - ] - } - } - ] - } - ], - "actions": [] -} diff --git a/SofarWechselrichter/module.json b/SofarWechselrichter/module.json deleted file mode 100644 index 1d594c2..0000000 --- a/SofarWechselrichter/module.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "id": "{C26E97C8-BA00-0563-6B14-B807C5ACE17F}", - "name": "SofarWechselrichter", - "type": 3, - "vendor": "Belevo AG", - "aliases": [], - "parentRequirements": [], - "childRequirements": [], - "implemented": [], - "prefix": "GEF", - "url": "" -} diff --git a/SofarWechselrichter/module.php b/SofarWechselrichter/module.php deleted file mode 100644 index e289af9..0000000 --- a/SofarWechselrichter/module.php +++ /dev/null @@ -1,379 +0,0 @@ -RegisterPropertyString('IPAddress', ''); - $this->RegisterPropertyString('LoggerNumber', '0'); // als String - $this->RegisterPropertyInteger('PollInterval', 60); - $this->RegisterPropertyString('Registers', '[]'); // JSON-String - - // Timer für zyklische Abfragen (per RequestAction("Query")) - $script = 'IPS_RequestAction(' . $this->InstanceID . ', "Query", "");'; - $this->RegisterTimer('QueryTimer', 0, $script); - } - - public function ApplyChanges() - { - parent::ApplyChanges(); - // Timer-Intervall (ms) - $intervalSec = $this->ReadPropertyInteger('PollInterval'); - $intervalMs = ($intervalSec > 0) ? $intervalSec * 1000 : 0; - $this->SetTimerInterval('QueryTimer', $intervalMs); - - // Aktuelle Registerliste - $registers = json_decode($this->ReadPropertyString('Registers'), true); - if (!is_array($registers)) { - $registers = []; - } - - // 1) Variablen anlegen (neu hinzugekommene Register) - $position = 10; - foreach ($registers as $entry) { - $regNo = (int) $entry['RegisterNumber']; - $label = trim($entry['Label']); - if ($regNo < 0 || $label === '') { - continue; - } - $ident = 'Reg' . $regNo; - if (!$this->VariableExists($ident)) { - $this->RegisterVariableFloat($ident, $label, '', $position); - } - $position += 10; - } - - // 2) Variablen löschen (falls Register entfernt wurden) - $validIdents = []; - foreach ($registers as $entry) { - $validIdents[] = 'Reg' . ((int)$entry['RegisterNumber']); - } - $children = IPS_GetChildrenIDs($this->InstanceID); - foreach ($children as $childID) { - $obj = IPS_GetObject($childID); - if ($obj['ObjectType'] !== 2) { - continue; - } - $ident = $obj['ObjectIdent']; - if (substr($ident, 0, 3) === 'Reg') { - if (!in_array($ident, $validIdents)) { - IPS_DeleteVariable($childID); - } - } - } - } - - public function Destroy() - { - parent::Destroy(); - } - - /** - * Wird aufgerufen durch IPS_RequestAction über den Timer - */ - public function RequestAction($Ident, $Value) - { - switch ($Ident) { - case 'Query': - $this->Query(); - break; - default: - throw new Exception('Ungültiger Ident: ' . $Ident); - } - } - - /** - * Zyklische Abfrage aller definierten Register - */ - private function Query(): void - { - $this->LogMessage('Query invoked', KL_MESSAGE); - - $ip = trim($this->ReadPropertyString('IPAddress')); - $loggerNumberStr = trim($this->ReadPropertyString('LoggerNumber')); - - // 1) Validierung IP - if (!filter_var($ip, FILTER_VALIDATE_IP)) { - $this->LogMessage('Abbruch: Ungültige IP = "' . $ip . '"', KL_WARNING); - return; - } - // 2) Validierung LoggerNumber (Dezimal-String, > 0) - if ($loggerNumberStr === '' || !ctype_digit($loggerNumberStr) || bccomp($loggerNumberStr, '1') < 0) { - $this->LogMessage('Abbruch: Ungültige LoggerNumber = "' . $loggerNumberStr . '"', KL_WARNING); - return; - } - - // 3) Register-Liste einlesen - $registers = json_decode($this->ReadPropertyString('Registers'), true); - if (!is_array($registers) || count($registers) === 0) { - // Keine Register definiert - return; - } - - // 4) Für jedes Register: einzeln auslesen, zusammensetzen, skalieren, speichern - foreach ($registers as $entry) { - $regNo = (int) $entry['RegisterNumber']; - $label = trim((string)$entry['Label']); - $scale = (string) $entry['ScalingFactor']; // kann negativ sein - $endian = strtoupper(trim((string)$entry['Endian'])); - $bitLength = (int) $entry['BitLength']; // 16, 32 oder 64 - $signedness = trim((string)$entry['Signedness']); // "Signed" oder "Unsigned" - $ident = 'Reg' . $regNo; - - if ($regNo < 0 || $label === '' || !in_array($bitLength, [16, 32, 64])) { - continue; - } - - try { - $numRegs = $bitLength / 16; // 1, 2 oder 4 - // Bytes registerweise einzeln abfragen und zusammenfügen: - $dataBytes = ''; - for ($i = 0; $i < $numRegs; $i++) { - $chunk = $this->readSingleRegister($ip, $loggerNumberStr, $regNo + $i); - $dataBytes .= $chunk; - } - // Debug: raw combined response hex - $combinedHex = strtoupper(bin2hex($dataBytes)); - $this->LogMessage("Raw data for Reg {$regNo} ({$bitLength}bit): {$combinedHex}", KL_MESSAGE); - - // Endian-Handling: falls LE, kehre gesamte Byte-Reihenfolge um - if ($endian === 'LE') { - $combinedHex = $this->reverseByteOrder($combinedHex); - $this->LogMessage("After LE reverse: {$combinedHex}", KL_MESSAGE); - } - - // Konvertiere Hex in Dezimal-String - $rawDec = $this->hexToDecimal($combinedHex); - - // Bei "Signed" → Zwei-Komplement-Umrechnung - if ($signedness === 'Signed') { - $half = bcpow('2', (string)($bitLength - 1), 0); // 2^(bitLength-1) - $fullRange = bcpow('2', (string)$bitLength, 0); // 2^bitLength - if (bccomp($rawDec, $half) >= 0) { - $rawDec = bcsub($rawDec, $fullRange, 0); - } - $this->LogMessage("Signed rawDec for Reg {$regNo}: {$rawDec}", KL_MESSAGE); - } - - // Skaliere (bc*-Multiplikation, 4 Nachkommastellen) - $valueStr = bcmul($rawDec, $scale, 4); - SetValueFloat($this->GetIDForIdent($ident), (float)$valueStr); - $this->LogMessage("Final value for Reg {$regNo}: {$valueStr}", KL_MESSAGE); - } catch (Exception $e) { - $this->LogMessage( - "Fehler Lesen Reg {$regNo} ({$bitLength}bit, {$signedness}): " . $e->getMessage(), - KL_WARNING - ); - } - } - } - - /** - * Liest genau ein Register (16 Bit) per Modbus-ähnlichem TCP (2 Bytes zurück). - * - * @param string $ip Inverter-IP - * @param string $serial_nr_str Logger-Seriennummer als Dezimal-String - * @param int $reg Register-Adresse - * @return string 2-Byte-Binär-String - * @throws Exception Bei Kommunikationsfehlern - */ - private function readSingleRegister(string $ip, string $serial_nr_str, int $reg): string - { - // 1) Out_Frame ohne CRC aufbauen - $oFrame = 'a5170010450000'; - - // Dezimal-String → 8-stellige Hex - $hexSN8 = $this->decStringToHex8($serial_nr_str); - $hexSNbytes = [ - substr($hexSN8, 6, 2), - substr($hexSN8, 4, 2), - substr($hexSN8, 2, 2), - substr($hexSN8, 0, 2), - ]; - $oFrame .= implode('', $hexSNbytes); - - // Data-Field (16 Hex-Zeichen konstant) - $oFrame .= '020000000000000000000000000000'; - - // Business-Field: 01 03 + Start-Register + Anzahl Register (1) - $startHex = str_pad(dechex($reg), 4, '0', STR_PAD_LEFT); - $numHex = str_pad(dechex(1), 4, '0', STR_PAD_LEFT); - $oFrame .= '0103' . $startHex . $numHex; - - // 2) CRC16-Modbus (letzte 6 Bytes) - $crcInputHex = substr($oFrame, -12); - $crcInputBin = hex2bin($crcInputHex); - if ($crcInputBin === false) { - throw new Exception("Ungültiges Hex in CRC-Input: {$crcInputHex}"); - } - $crcValue = $this->calculateCRC16Modbus($crcInputBin); - $crcHex = strtoupper(str_pad(dechex($crcValue), 4, '0', STR_PAD_LEFT)); - $crcSwapped = substr($crcHex, 2, 2) . substr($crcHex, 0, 2); - $oFrameWithCRC = $oFrame . strtolower($crcSwapped); - - // 3) Summen-Checksum (alle Bytes ab Index 1) + 0x15 - $l = strlen($oFrameWithCRC) / 2; - $bArr = []; - for ($i = 0; $i < $l; $i++) { - $byteHex = substr($oFrameWithCRC, 2 * $i, 2); - $bArr[$i] = hexdec($byteHex); - } - $crcSum = 0; - for ($i = 1; $i < $l; $i++) { - $crcSum += $bArr[$i]; - $crcSum &= 0xFF; - } - $bArr[$l] = $crcSum; - $bArr[$l+1] = 0x15; - - $frameBin = ''; - foreach ($bArr as $b) { - $frameBin .= chr($b); - } - - // 4) TCP-Verbindung öffnen & Paket senden (Port fest 8899) - $port = 8899; - $fp = @stream_socket_client("tcp://{$ip}:{$port}", $errno, $errstr, 5); - if (!$fp) { - throw new Exception("Verbindung zu {$ip}:{$port} fehlgeschlagen ({$errno}: {$errstr})"); - } - fwrite($fp, $frameBin); - stream_set_timeout($fp, 2); - - // 5) Antwort einlesen - $response = ''; - while (!feof($fp)) { - $chunk = fread($fp, 1024); - if ($chunk === false || $chunk === '') { - break; - } - $response .= $chunk; - } - fclose($fp); - if ($response === '') { - throw new Exception("Keine Antwort vom Inverter erhalten."); - } - - // Debug: log raw response hex - $respHex = strtoupper(bin2hex($response)); - $this->LogMessage("Raw response for single reg {$reg}: {$respHex}", KL_MESSAGE); - - // 6) Slice-Logik: l = 2*1 + 4 = 6, slice(-6, 2) → 2 Bytes - $lModbus = 2 * 1 + 4; // = 6 - $numBytes = 2; - if (strlen($response) < $lModbus) { - throw new Exception("Unerwartet kurze Antwort (< {$lModbus} Bytes)."); - } - $dataBytes = substr($response, -$lModbus, $numBytes); - if (strlen($dataBytes) < $numBytes) { - throw new Exception("Data-Segment enthält weniger als {$numBytes} Bytes."); - } - $dataHex = strtoupper(bin2hex($dataBytes)); - $this->LogMessage("Sliced data for single reg {$reg}: {$dataHex}", KL_MESSAGE); - - return $dataBytes; - } - - /** - * Wandelt einen Dezimal-String in einen 8-stelligen Hex-String um. - * - * @param string $decString - * @return string 8-stellige Hex (uppercase) - */ - private function decStringToHex8(string $decString): string - { - $num = ltrim($decString, '0'); - if ($num === '') { - return '00000000'; - } - $hex = ''; - while (bccomp($num, '0') > 0) { - $mod = bcmod($num, '16'); - $digit = dechex((int)$mod); - $hex = strtoupper($digit) . $hex; - $num = bcdiv($num, '16', 0); - } - return str_pad($hex, 8, '0', STR_PAD_LEFT); - } - - /** - * Kehrt die Byte-Reihenfolge eines Hex-Strings um (2 Hex-Zeichen = 1 Byte). - * - * @param string $hex Hex-Repr. (z.B. "A1B2C3D4") - * @return string Umgekehrte Byte-Reihenfolge (z.B. "D4C3B2A1") - */ - private function reverseByteOrder(string $hex): string - { - $bytes = str_split($hex, 2); - $bytes = array_reverse($bytes); - return implode('', $bytes); - } - - /** - * Konvertiert einen Hex-String in einen Dezimal-String (BCMath). - * - * @param string $hex Uppercase-Hex ohne Präfix (z.B. "00FF10A3") - * @return string Dezimal-String (z.B. "16737763") - */ - private function hexToDecimal(string $hex): string - { - $hex = ltrim($hex, '0'); - if ($hex === '') { - return '0'; - } - $len = strlen($hex); - $dec = '0'; - $power16 = '1'; - for ($i = $len - 1; $i >= 0; $i--) { - $digit = hexdec($hex[$i]); - $term = bcmul((string)$digit, $power16, 0); - $dec = bcadd($dec, $term, 0); - $power16 = bcmul($power16, '16', 0); - } - return $dec; - } - - /** - * Berechnet CRC16-Modbus (Init=0xFFFF, Poly=0xA001) über Binärdaten. - */ - private function calculateCRC16Modbus(string $binaryData): int - { - $crc = 0xFFFF; - $len = strlen($binaryData); - for ($pos = 0; $pos < $len; $pos++) { - $crc ^= ord($binaryData[$pos]); - for ($i = 0; $i < 8; $i++) { - if (($crc & 0x0001) !== 0) { - $crc >>= 1; - $crc ^= 0xA001; - } else { - $crc >>= 1; - } - } - } - return $crc; - } - - /** - * Prüft, ob eine Variable mit Ident existiert. - */ - private function VariableExists(string $ident): bool - { - $vid = @IPS_GetObjectIDByIdent($ident, $this->InstanceID); - return ($vid !== false && IPS_VariableExists($vid)); - } -}