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
-
-
-
-
-
-
-
-
-
-
-
-
-
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));
- }
-}