Compare commits
247 Commits
cc5dad6d9a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2178860a51 | ||
|
|
cfcf6ba43c | ||
| 11d72eca10 | |||
| 99aba58f34 | |||
| 584d69b471 | |||
| 7850533911 | |||
| eb90029ad6 | |||
| 724f3b0d2a | |||
|
|
a57a76972d | ||
|
|
91e4709612 | ||
|
|
944817950a | ||
| 88b6c60890 | |||
|
|
6757682399 | ||
|
|
f11255b071 | ||
|
|
1e23489346 | ||
|
|
4f7ecf1aae | ||
| daff7a81e5 | |||
| f35c43bacd | |||
| 912e3ad3df | |||
| 851dc7d70e | |||
| ccb068dd5d | |||
| 57a4594e38 | |||
| 6094694809 | |||
| c9426c1bfc | |||
| 17fe281fa3 | |||
| 1ee6b409aa | |||
| 8dd82f1abd | |||
| 015b1fc441 | |||
| e21f760e13 | |||
| 5aa44ad8a2 | |||
|
|
8f051aced5 | ||
|
|
e6cd727fc1 | ||
|
|
494984734b | ||
|
|
47beb18f0b | ||
|
|
41c53c7a8f | ||
|
|
f887dba1e8 | ||
|
|
7db0667bd0 | ||
|
|
e22ebf328a | ||
|
|
e12c192e2d | ||
|
|
f89ff7f200 | ||
|
|
7314b81cc6 | ||
|
|
c6245d870e | ||
|
|
20bafaf68f | ||
| 936a7ab46e | |||
| 1d099588d2 | |||
| a902604003 | |||
| 1e7efaef6e | |||
| d6a5d2c1f2 | |||
| 875c40e179 | |||
| 4a3fb06b21 | |||
| 92e4393b12 | |||
|
|
b8783ebba4 | ||
|
|
658b9626b4 | ||
|
|
b5973a5878 | ||
|
|
716538c758 | ||
|
|
11be77f8dd | ||
|
|
afc72486b9 | ||
|
|
76464b66cf | ||
|
|
a5ea4c75ce | ||
|
|
8c5a9c5e61 | ||
|
|
9bbbad260a | ||
|
|
09962a8cd0 | ||
|
|
ab9eec4249 | ||
|
|
c5ec334421 | ||
|
|
1e0b314f2a | ||
|
|
979fa1a44f | ||
|
|
b779f4ce42 | ||
|
|
2847607983 | ||
|
|
cf126855ba | ||
|
|
c6a8c32a65 | ||
|
|
78668405bc | ||
|
|
cdc7d762d8 | ||
|
|
6e9835d852 | ||
|
|
9e6ffb3c01 | ||
|
|
4648c5e404 | ||
|
|
0b9d53fff9 | ||
|
|
9f22ae0aa7 | ||
|
|
ef49957d7f | ||
|
|
8148d62484 | ||
|
|
2eab6b60ec | ||
|
|
a314141093 | ||
|
|
fc411e0d5f | ||
|
|
d23b317093 | ||
|
|
ff9fc91aae | ||
|
|
5b57f2637a | ||
|
|
010f6c5785 | ||
|
|
6cffe2715f | ||
|
|
b79e0d1430 | ||
|
|
adf6b573de | ||
|
|
20228fbcf1 | ||
|
|
9319e3a2f8 | ||
|
|
391184a26e | ||
|
|
4afd1cf6fa | ||
|
|
f235142561 | ||
|
|
e06333be0b | ||
|
|
31d950f6d2 | ||
|
|
3390330a82 | ||
|
|
225e254366 | ||
|
|
d94837f597 | ||
|
|
e859ead314 | ||
|
|
ba9c92c4ac | ||
|
|
b0929f77a9 | ||
|
|
2c86a7c181 | ||
|
|
18cbf73cd0 | ||
|
|
83bf7c5907 | ||
|
|
5df7c0c305 | ||
|
|
f80ebc8367 | ||
|
|
519e845eeb | ||
|
|
230e8bd890 | ||
|
|
a61ca6cff4 | ||
|
|
eed1e585d6 | ||
|
|
0e9ad10626 | ||
|
|
9ed5471d6a | ||
|
|
234482bd8d | ||
|
|
1a07130db2 | ||
|
|
ba2cdaede8 | ||
|
|
4535be0f2c | ||
|
|
41b4a85409 | ||
|
|
1207792810 | ||
|
|
63912b3606 | ||
|
|
3be124e81f | ||
|
|
422ea69b4f | ||
|
|
0a6ce80266 | ||
|
|
b83d9459eb | ||
|
|
a25f440097 | ||
|
|
b241cc3f77 | ||
|
|
cfb196f346 | ||
|
|
83c74ad05d | ||
|
|
27536d3d16 | ||
|
|
33667256ec | ||
|
|
5db059fc51 | ||
|
|
2a605a6138 | ||
|
|
bf79f30e76 | ||
|
|
ce57b11224 | ||
|
|
4711fb0208 | ||
|
|
2f68634145 | ||
|
|
f21acfe6a6 | ||
|
|
110120f398 | ||
|
|
ba8e3af69b | ||
|
|
b1d8dbeeaf | ||
|
|
9bb2b90ce0 | ||
|
|
aa01389bf3 | ||
|
|
a86c8dba66 | ||
|
|
7ac31ef2a2 | ||
|
|
bbb8ee8bfe | ||
|
|
692bf28e0c | ||
|
|
322c4acbb5 | ||
|
|
d345d26b1e | ||
|
|
f5d6ad2a72 | ||
|
|
3f86c5583a | ||
|
|
bb16399d7d | ||
|
|
05f4b8b32d | ||
|
|
09f1c52f87 | ||
|
|
6cb64b47fa | ||
|
|
13ff15a0d9 | ||
|
|
1699ad4e7c | ||
|
|
ff747d1f04 | ||
|
|
3a18f52492 | ||
|
|
80e35c372e | ||
|
|
d1f0b876aa | ||
|
|
feb7301fc1 | ||
|
|
f4e87772e1 | ||
|
|
d2bae877a4 | ||
|
|
90701ec350 | ||
|
|
b7ef02eb86 | ||
|
|
ba99c8f2e3 | ||
|
|
fdc23168b3 | ||
|
|
4335c37d9f | ||
|
|
13f458e10c | ||
|
|
1627f81b9e | ||
|
|
03e02558d5 | ||
|
|
94778e2209 | ||
|
|
e99c5c0902 | ||
|
|
c8d5bb3ab4 | ||
|
|
499eddccb9 | ||
|
|
26ec00c63e | ||
|
|
aafd768e94 | ||
|
|
78be02d507 | ||
|
|
78299cee43 | ||
|
|
e31ad76616 | ||
|
|
2c01458065 | ||
|
|
3651502d6e | ||
|
|
f0de973f26 | ||
|
|
394b797e84 | ||
|
|
9f18caf7d0 | ||
|
|
883b3815be | ||
|
|
a36c575ad7 | ||
|
|
a6a758b332 | ||
|
|
cc3cb5fd38 | ||
|
|
ca74e3d946 | ||
|
|
7f5a6bcade | ||
|
|
65d2be480f | ||
|
|
38f450b00e | ||
|
|
26fd4a4159 | ||
|
|
25630326c7 | ||
|
|
b6a1625433 | ||
|
|
195209a6da | ||
|
|
06a8327619 | ||
|
|
5f127b9ede | ||
|
|
284ed6ff3b | ||
|
|
b28d9b94ce | ||
|
|
da24c80b6c | ||
|
|
2c37115071 | ||
|
|
e7a95c36da | ||
|
|
58421f7fbd | ||
|
|
800d56907c | ||
|
|
2ca3b5b83d | ||
|
|
b55e8c67af | ||
| 9f9207f17f | |||
| 40b995d5e3 | |||
| 31a9f0afe7 | |||
| 14ff6781c2 | |||
| a514a45a3b | |||
| 9b1008d7f5 | |||
| 459c115a7d | |||
| 1b20b2e637 | |||
| 422ea912b0 | |||
| 4e6f5f6fb7 | |||
| 6650164acc | |||
| b68db49f72 | |||
| 558fd9aa4e | |||
| b4de095a20 | |||
| cd9209f800 | |||
| 47bf006229 | |||
| 7a4af1a67f | |||
| f3e7d81fb5 | |||
| 7daa8b7671 | |||
| 7680feda8d | |||
| b46b09ba29 | |||
| 3098547209 | |||
| 3180ac7922 | |||
| 9d628b7a3f | |||
| 17ca7c4c63 | |||
| 3a0eb6113e | |||
| 62e397db10 | |||
| 376dcc121e | |||
| 391f5716e1 | |||
| c0cc49261b | |||
| 37f5c3bede | |||
| f5ddc77d36 | |||
| 920bd27e9e | |||
| 51e038400b | |||
| 353b8afb3d | |||
| 1d4914dbf1 | |||
| 4213aa4804 | |||
| 74a38694cb | |||
| 43bf2194cb |
@@ -4,121 +4,148 @@
|
||||
"type": "Label",
|
||||
"caption": "🧾 Abrechnungseinstellungen"
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"type": "List",
|
||||
"name": "Users",
|
||||
"caption": "Benutzerliste",
|
||||
"add": true,
|
||||
"delete": true,
|
||||
"sortable": true,
|
||||
"columns": [
|
||||
{ "caption": "ID", "name": "id", "width": "10%", "add": "", "edit": { "type": "ValidationTextBox" } },
|
||||
{ "caption": "Name", "name": "name", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } },
|
||||
{ "caption": "Adresse", "name": "address", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } },
|
||||
{ "caption": "Ort", "name": "city", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } }
|
||||
]
|
||||
|
||||
"type": "SelectMedia",
|
||||
"name": "LogoMediaID",
|
||||
"caption": "Logo auswählen"
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"type": "List",
|
||||
"name": "PowerMeters",
|
||||
"caption": "Stromzählerliste",
|
||||
"add": true,
|
||||
"delete": true,
|
||||
"sortable": true,
|
||||
"columns": [
|
||||
{ "caption": "ID", "name": "id", "width": "10%", "add": "", "edit": { "type": "ValidationTextBox" } },
|
||||
{ "caption": "Name", "name": "name", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } },
|
||||
{ "caption": "Var. Verbrauch", "name": "var_consumption", "width": "20%", "add": 0, "edit": { "type": "SelectVariable" } },
|
||||
{ "caption": "Var. Rückspeisung", "name": "var_feed", "width": "20%", "add": 0, "edit": { "type": "SelectVariable" } },
|
||||
{ "caption": "Benutzer-ID", "name": "user_id", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } }
|
||||
]
|
||||
"type": "ValidationTextBox",
|
||||
"name": "PropertyText",
|
||||
"caption": "Liegenschaft"
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"type": "List",
|
||||
"name": "WaterMeters",
|
||||
"caption": "Verbrauchszählerliste",
|
||||
"add": true,
|
||||
"delete": true,
|
||||
"sortable": true,
|
||||
"columns": [
|
||||
{ "caption": "ID", "name": "id", "width": "10%", "add": "", "edit": { "type": "ValidationTextBox" } },
|
||||
{ "caption": "Name", "name": "name", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } },
|
||||
{ "caption": "Var. Verbrauch", "name": "var_consumption", "width": "20%", "add": 0, "edit": { "type": "SelectVariable" } },
|
||||
{ "caption": "Benutzer-ID", "name": "user_id", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } },
|
||||
{
|
||||
"caption": "Zählertyp",
|
||||
"name": "meter_type",
|
||||
"width": "20%",
|
||||
"add": "Warmwasser",
|
||||
"edit": {
|
||||
"type": "Select",
|
||||
"options": [
|
||||
{ "caption": "Warmwasser", "value": "Warmwasser" },
|
||||
{ "caption": "Kaltwasser", "value": "Kaltwasser" },
|
||||
{ "caption": "Wärme", "value": "Wärme" }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
"type": "ValidationTextBox",
|
||||
"name": "FooterText",
|
||||
"caption": "Fusszeile"
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"caption": "⚖️ Mehrwertsteuer (MWST)"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "EnableVAT",
|
||||
"caption": "Mehrwertsteuer berechnen und ausweisen"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "VATPercentage",
|
||||
"caption": "MWST-Satz in %",
|
||||
"digits": 2,
|
||||
"minimum": 0,
|
||||
"maximum": 100
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"caption": "🏦 QR-Einzahlungsschein Daten"
|
||||
},
|
||||
{
|
||||
"type": "ValidationTextBox",
|
||||
"name": "CreditorName",
|
||||
"caption": "Rechnungssteller Name"
|
||||
},
|
||||
{
|
||||
"type": "ValidationTextBox",
|
||||
"name": "CreditorAddress",
|
||||
"caption": "Rechnungssteller Strasse/Nr."
|
||||
},
|
||||
{
|
||||
"type": "ValidationTextBox",
|
||||
"name": "CreditorCity",
|
||||
"caption": "Rechnungssteller PLZ/Ort"
|
||||
},
|
||||
{
|
||||
"type": "ValidationTextBox",
|
||||
"name": "BankIBAN",
|
||||
"caption": "Bank IBAN"
|
||||
},
|
||||
{
|
||||
"type": "List",
|
||||
"name": "Users",
|
||||
"caption": "Benutzerliste",
|
||||
"add": true,
|
||||
"delete": true,
|
||||
"sortable": true,
|
||||
"columns": [
|
||||
{ "caption": "ID", "name": "id", "width": "10%", "add": "", "edit": { "type": "ValidationTextBox" } },
|
||||
{ "caption": "Name", "name": "name", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } },
|
||||
{ "caption": "Adresse", "name": "address", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } },
|
||||
{ "caption": "Ort", "name": "city", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } }
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"type": "List",
|
||||
"name": "Tariffs",
|
||||
"caption": "Tarifübersicht",
|
||||
"add": true,
|
||||
"delete": true,
|
||||
"sortable": true,
|
||||
"columns": [
|
||||
{
|
||||
"caption": "Startdatum",
|
||||
"name": "start",
|
||||
"width": "20%",
|
||||
"add": "",
|
||||
"edit": { "type": "SelectDate" }
|
||||
},
|
||||
{
|
||||
"caption": "Enddatum",
|
||||
"name": "end",
|
||||
"width": "20%",
|
||||
"add": "",
|
||||
"edit": { "type": "SelectDate" }
|
||||
},
|
||||
{
|
||||
"caption": "Tarif (Rp/Einheit)",
|
||||
"name": "price",
|
||||
"width": "20%",
|
||||
"add": 0,
|
||||
"edit": { "type": "NumberSpinner", "digits": 3, "minimum": 0 }
|
||||
},
|
||||
{
|
||||
"caption": "Einheit",
|
||||
"name": "unit_type",
|
||||
"width": "20%",
|
||||
"add": "Netztarif",
|
||||
"edit": {
|
||||
"type": "Select",
|
||||
"options": [
|
||||
{ "caption": "Netztarif", "value": "Netztarif" },
|
||||
{ "caption": "Einspeisetarif", "value": "Einspeisetarif" },
|
||||
{ "caption": "Solartarif", "value": "Solartarif" },
|
||||
{ "caption": "Warmwasser", "value": "Warmwasser" },
|
||||
{ "caption": "Kaltwasser", "value": "Kaltwasser" },
|
||||
{ "caption": "Wärme", "value": "Wärme" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
"type": "List",
|
||||
"name": "PowerMeters",
|
||||
"caption": "Stromzählerliste",
|
||||
"add": true,
|
||||
"delete": true,
|
||||
"sortable": true,
|
||||
"columns": [
|
||||
{ "caption": "ID", "name": "id", "width": "10%", "add": "", "edit": { "type": "ValidationTextBox" } },
|
||||
{ "caption": "Name", "name": "name", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } },
|
||||
{ "caption": "Var. Verbrauch", "name": "var_consumption", "width": "20%", "add": 0, "edit": { "type": "SelectVariable" } },
|
||||
{ "caption": "Var. Rückspeisung", "name": "var_feed", "width": "20%", "add": 0, "edit": { "type": "SelectVariable" } },
|
||||
{ "caption": "Benutzer-ID", "name": "user_id", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "List",
|
||||
"name": "WaterMeters",
|
||||
"caption": "Verbrauchszählerliste",
|
||||
"add": true,
|
||||
"delete": true,
|
||||
"sortable": true,
|
||||
"columns": [
|
||||
{ "caption": "ID", "name": "id", "width": "10%", "add": "", "edit": { "type": "ValidationTextBox" } },
|
||||
{ "caption": "Name", "name": "name", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } },
|
||||
{ "caption": "Var. Verbrauch", "name": "var_consumption", "width": "20%", "add": 0, "edit": { "type": "SelectVariable" } },
|
||||
{ "caption": "Benutzer-ID", "name": "user_id", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } },
|
||||
{
|
||||
"caption": "Zählertyp",
|
||||
"name": "meter_type",
|
||||
"width": "20%",
|
||||
"add": "Warmwasser",
|
||||
"edit": {
|
||||
"type": "Select",
|
||||
"options": [
|
||||
{ "caption": "Warmwasser", "value": "Warmwasser" },
|
||||
{ "caption": "Kaltwasser", "value": "Kaltwasser" },
|
||||
{ "caption": "Wärme", "value": "Wärme" }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "List",
|
||||
"name": "Tariffs",
|
||||
"caption": "Tarifübersicht",
|
||||
"add": true,
|
||||
"delete": true,
|
||||
"sortable": true,
|
||||
"columns": [
|
||||
{ "caption": "Startdatum", "name": "start", "width": "20%", "add": "", "edit": { "type": "SelectDate" } },
|
||||
{ "caption": "Enddatum", "name": "end", "width": "20%", "add": "", "edit": { "type": "SelectDate" } },
|
||||
{ "caption": "Tarif (Rp/Einheit)", "name": "price", "width": "20%", "add": 0, "edit": { "type": "NumberSpinner", "digits": 3, "minimum": 0 } },
|
||||
{
|
||||
"caption": "Einheit",
|
||||
"name": "unit_type",
|
||||
"width": "20%",
|
||||
"add": "Netztarif",
|
||||
"edit": {
|
||||
"type": "Select",
|
||||
"options": [
|
||||
{ "caption": "Netztarif", "value": "Netztarif" },
|
||||
{ "caption": "Einspeisetarif", "value": "Einspeisetarif" },
|
||||
{ "caption": "Solartarif", "value": "Solartarif" },
|
||||
{ "caption": "Warmwasser", "value": "Warmwasser" },
|
||||
{ "caption": "Kaltwasser", "value": "Kaltwasser" },
|
||||
{ "caption": "Wärme", "value": "Wärme" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
67
Bat_EV_SDL _V2/README.md
Normal file
67
Bat_EV_SDL _V2/README.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Manager_1
|
||||
Beschreibung des Moduls.
|
||||
|
||||
### Inhaltsverzeichnis
|
||||
|
||||
1. [Funktionsumfang](#1-funktionsumfang)
|
||||
2. [Voraussetzungen](#2-voraussetzungen)
|
||||
3. [Software-Installation](#3-software-installation)
|
||||
4. [Einrichten der Instanzen in IP-Symcon](#4-einrichten-der-instanzen-in-ip-symcon)
|
||||
5. [Statusvariablen und Profile](#5-statusvariablen-und-profile)
|
||||
6. [WebFront](#6-webfront)
|
||||
7. [PHP-Befehlsreferenz](#7-php-befehlsreferenz)
|
||||
|
||||
### 1. Funktionsumfang
|
||||
|
||||
*
|
||||
|
||||
### 2. Voraussetzungen
|
||||
|
||||
- IP-Symcon ab Version 7.1
|
||||
|
||||
### 3. Software-Installation
|
||||
|
||||
* Über den Module Store das 'Manager_1'-Modul installieren.
|
||||
* Alternativ über das Module Control folgende URL hinzufügen
|
||||
|
||||
### 4. Einrichten der Instanzen in IP-Symcon
|
||||
|
||||
Unter 'Instanz hinzufügen' kann das 'Manager_1'-Modul mithilfe des Schnellfilters gefunden werden.
|
||||
- Weitere Informationen zum Hinzufügen von Instanzen in der [Dokumentation der Instanzen](https://www.symcon.de/service/dokumentation/konzepte/instanzen/#Instanz_hinzufügen)
|
||||
|
||||
__Konfigurationsseite__:
|
||||
|
||||
Name | Beschreibung
|
||||
-------- | ------------------
|
||||
|
|
||||
|
|
||||
|
||||
### 5. Statusvariablen und Profile
|
||||
|
||||
Die Statusvariablen/Kategorien werden automatisch angelegt. Das Löschen einzelner kann zu Fehlfunktionen führen.
|
||||
|
||||
#### Statusvariablen
|
||||
|
||||
Name | Typ | Beschreibung
|
||||
------ | ------- | ------------
|
||||
| |
|
||||
| |
|
||||
|
||||
#### Profile
|
||||
|
||||
Name | Typ
|
||||
------ | -------
|
||||
|
|
||||
|
|
||||
|
||||
### 6. WebFront
|
||||
|
||||
Die Funktionalität, die das Modul im WebFront bietet.
|
||||
|
||||
### 7. PHP-Befehlsreferenz
|
||||
|
||||
`boolean GEF_BeispielFunktion(integer $InstanzID);`
|
||||
Erklärung der Funktion.
|
||||
|
||||
Beispiel:
|
||||
`GEF_BeispielFunktion(12345);`
|
||||
112
Bat_EV_SDL _V2/form.json
Normal file
112
Bat_EV_SDL _V2/form.json
Normal file
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"elements": [
|
||||
{
|
||||
"type": "Label",
|
||||
"caption": "Aufgepasst: Bei Goodwe nur Ladenvariabel auswählen und entladen Dummy Variabel.\nGoodwe braucht nur eine Leistungssoll Variabel. Entlade NICHT auf gleiche Variabel setzen wie Laden\nGoodwe: Laden=11, Entladen=12,\nSolaredge: Laden=3, Entladen=4\nDefault: Laden=1, Entladen=2"
|
||||
},
|
||||
{
|
||||
"type":"List",
|
||||
"name":"Batteries",
|
||||
"caption":"Batterien mit Typ, Leistung und kapazität",
|
||||
"add":true,
|
||||
"delete":true,
|
||||
"columns":[
|
||||
{
|
||||
"caption":"Typ",
|
||||
"name":"typ",
|
||||
"width":"100px",
|
||||
"add":"Stufe",
|
||||
"edit":{
|
||||
"type":"ValidationTextBox"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption":"Leistung in W",
|
||||
"name":"powerbat",
|
||||
"width":"150px",
|
||||
"add":5000,
|
||||
"edit":{
|
||||
"type":"NumberSpinner"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption":"Kapazität in kWh",
|
||||
"name":"capazity",
|
||||
"width":"200px",
|
||||
"add":60,
|
||||
"edit":{
|
||||
"type":"NumberSpinner"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption":"SoC",
|
||||
"name":"soc",
|
||||
"width":"200px",
|
||||
"add":0,
|
||||
"edit":{
|
||||
"type":"SelectVariable"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption":"Batterieleistung Laden",
|
||||
"name":"powerbat_laden",
|
||||
"width":"200px",
|
||||
"add":0,
|
||||
"edit":{
|
||||
"type":"SelectVariable"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption":"Batterieleistung Entladen",
|
||||
"name":"powerbat_entladen",
|
||||
"width":"200px",
|
||||
"add":0,
|
||||
"edit":{
|
||||
"type":"SelectVariable"
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"caption": "Register Laden/Entladen Modus",
|
||||
"name": "register_ladenentladen_modus",
|
||||
"width": "260px",
|
||||
"add": 0,
|
||||
"edit": {
|
||||
"type": "SelectVariable"
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"caption": "Aktuelle Batterieleistung",
|
||||
"name": "register_bat_power",
|
||||
"width": "260px",
|
||||
"add": 0,
|
||||
"edit": {
|
||||
"type": "SelectVariable"
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "SDL_Leistung_Laden",
|
||||
"caption": "SDL_Leistung_Laden",
|
||||
"suffix": "W"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "SDL_Leistung_Entladen",
|
||||
"caption": "SDL_Leistung_Entladen",
|
||||
"suffix": "W"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "UpdateInterval",
|
||||
"caption": "Neuberechnung alle",
|
||||
"suffix": "Sekunden",
|
||||
"minimum": 0,
|
||||
"maximum": 86400
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "{A7FEA6E1-EBAA-1567-C14B-9A63B09C9EDC}",
|
||||
"name": "Batterie_test",
|
||||
"id": "{843EB84A-5180-6B47-A72E-9CDBE59DD9D3}",
|
||||
"name": "Bat_EV_SDL _V2",
|
||||
"type": 3,
|
||||
"vendor": "Belevo AG",
|
||||
"aliases": [],
|
||||
808
Bat_EV_SDL _V2/module.php
Normal file
808
Bat_EV_SDL _V2/module.php
Normal file
@@ -0,0 +1,808 @@
|
||||
<?php
|
||||
|
||||
class Bat_EV_SDL_V2 extends IPSModule
|
||||
{
|
||||
private const HOURS = 0.5; // 30 Minuten
|
||||
|
||||
public function Create()
|
||||
{
|
||||
parent::Create();
|
||||
|
||||
// Properties
|
||||
$this->RegisterPropertyString("Batteries", "[]");
|
||||
//$this->RegisterPropertyInteger("$upKWh + $underKWh)", 0);
|
||||
$this->RegisterPropertyInteger("SDL_Leistung_Laden", 0);
|
||||
$this->RegisterPropertyInteger("SDL_Leistung_Entladen", 0); // W
|
||||
$this->RegisterPropertyInteger("UpdateInterval", 5); // Minuten
|
||||
|
||||
// Status
|
||||
$this->RegisterVariableBoolean("State", "Aktiv", "~Switch", 1);
|
||||
$this->EnableAction("State");
|
||||
|
||||
// Prozentwerte
|
||||
$this->RegisterVariableFloat("SDL_Pos", "SDL Energie verfügbar (%)", "", 10);
|
||||
$this->RegisterVariableFloat("SoC_EV", "EV Energie verfügbar (%)", "", 11);
|
||||
|
||||
// Variablen
|
||||
$this->RegisterVariableFloat("Nennleistung_Soll_EV", "Nennleistung Soll EV", "", 2);
|
||||
$this->RegisterVariableFloat("Nennleistung_Soll_SDL", "Nennleistung Soll SDL", "", 3);
|
||||
$this->RegisterVariableFloat("Aktuelle_Leistung_EV", "Aktuelle Leistung EV", "", 5);
|
||||
$this->RegisterVariableFloat("Aktuelle_Leistung_SDL", "Aktuelle Leistung SDL", "", 4);
|
||||
$this->RegisterVariableFloat("P_SDL_laden", "P SDL laden max (W)", "", 21);
|
||||
$this->RegisterVariableFloat("P_SDL_entladen", "P SDL entladen max (W)", "", 22);
|
||||
$this->RegisterVariableFloat("P_EV_laden", "P EV laden max (W)", "", 31);
|
||||
$this->RegisterVariableFloat("P_EV_entladen", "P EV entladen max (W)", "", 32);
|
||||
|
||||
// Debug
|
||||
$this->RegisterVariableString("CalcJSON", "Berechnung (JSON)", "", 99);
|
||||
|
||||
// Timer: wichtig -> Prefix muss passen
|
||||
$this->RegisterTimer("UpdateTimer", 0, 'GEF_Update($_IPS["TARGET"]);');
|
||||
}
|
||||
|
||||
public function ApplyChanges()
|
||||
{
|
||||
parent::ApplyChanges();
|
||||
|
||||
$intervalSec = (int)$this->ReadPropertyInteger("UpdateInterval");
|
||||
$this->SetTimerInterval("UpdateTimer", ($intervalSec > 0) ? $intervalSec * 1000 : 0);
|
||||
|
||||
// Cache neu bauen (force)
|
||||
$this->BuildBatteryCache(true);
|
||||
|
||||
$this->Update();
|
||||
}
|
||||
|
||||
public function RequestAction($Ident, $Value)
|
||||
{
|
||||
if ($Ident === "State") {
|
||||
SetValue($this->GetIDForIdent("State"), (bool)$Value);
|
||||
if ((bool)$Value) {
|
||||
$this->Update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Exception("Invalid Ident: " . $Ident);
|
||||
}
|
||||
|
||||
public function Update()
|
||||
{
|
||||
|
||||
|
||||
// Reentranzschutz – verhindert parallele Update()-Läufe
|
||||
$semKey = 'BatEVSDL_Update_' . $this->InstanceID;
|
||||
|
||||
if (!IPS_SemaphoreEnter($semKey, 5000)) {
|
||||
// Überspringen, wenn bereits ein Lauf aktiv ist
|
||||
$this->SendDebug("Update", "SKIP (Semaphore locked)", 0);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
try {
|
||||
|
||||
if (!GetValue($this->GetIDForIdent("State"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache nur neu bauen, wenn nötig
|
||||
$this->BuildBatteryCache(false);
|
||||
|
||||
$cache = json_decode($this->GetBufferSafe("BatCacheJSON"), true);
|
||||
if (!is_array($cache) || empty($cache["bats"])) {
|
||||
$this->WriteAllZero("cache empty/invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
$calc = [
|
||||
"inputs" => $cache["inputs"] ?? [],
|
||||
"batteries" => [],
|
||||
"total" => []
|
||||
];
|
||||
|
||||
// Summen
|
||||
$sdlDisKW_ges = 0.0;
|
||||
$evDisKW_ges = 0.0;
|
||||
$sdlChKW_ges = 0.0;
|
||||
$evChKW_ges = 0.0;
|
||||
|
||||
$real_kWh_ev_ges = 0.0;
|
||||
$real_kWh_sdl_ges = 0.0;
|
||||
|
||||
$SDL_kWh_ges = 0.0;
|
||||
$EV_kWh_ges = 0.0;
|
||||
$totalCapKWh = 0.0;
|
||||
|
||||
foreach ($cache["bats"] as $i => $c) {
|
||||
|
||||
$capKWh = (float)($c["capKWh"] ?? 0.0);
|
||||
if ($capKWh <= 0.0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// dynamisch: SoC lesen
|
||||
$socVarId = (int)($c["socVarId"] ?? 0);
|
||||
$socPct = $this->ReadSocPercent($socVarId);
|
||||
|
||||
$real_kWh = $capKWh / 100.0 * $socPct;
|
||||
|
||||
// vorkalkuliert:
|
||||
$typ = (string)($c["typ"] ?? ("Bat " . ($i + 1)));
|
||||
$underKWh = (float)($c["underKWh"] ?? 0.0);
|
||||
$upKWh = (float)($c["upKWh"] ?? 0.0);
|
||||
$SDL_kWh = (float)($c["SDL_kWh_total"] ?? 0.0);
|
||||
$EV_kWh = (float)($c["EV_kWh_total"] ?? 0.0);
|
||||
|
||||
$sdlShareKW_laden = (float)($c["sdlShareKW_laden"] ?? 0.0);
|
||||
$sdlShareKW_entladen = (float)($c["sdlShareKW_entladen"] ?? 0.0);
|
||||
$evShareKW_laden = (float)($c["evShareKW_laden"] ?? 0.0);
|
||||
$evShareKW_entladen = (float)($c["evShareKW_entladen"] ?? 0.0);
|
||||
|
||||
|
||||
|
||||
// Defaults
|
||||
$EV_SOC = 0.0;
|
||||
$SDL_SOC = 0.0;
|
||||
|
||||
$sdlDisKW = 0.0; $evDisKW = 0.0;
|
||||
$sdlChKW = 0.0; $evChKW = 0.0;
|
||||
|
||||
$real_kWh_ev = 0.0;
|
||||
$real_kWh_sdl = 0.0;
|
||||
|
||||
// --- Deine 3 Fälle ---
|
||||
if ($underKWh <= $real_kWh && $upKWh >= $real_kWh) {
|
||||
|
||||
|
||||
$denSDL = ($capKWh - $upKWh + $underKWh);
|
||||
if ($denSDL > 0.0) {
|
||||
$SDL_SOC = 100.0 * $underKWh / $denSDL;
|
||||
} else {
|
||||
$SDL_SOC = 0.0;
|
||||
}
|
||||
$SDL_SOC = is_finite($SDL_SOC) ? max(0.0, min(100.0, $SDL_SOC)) : 0.0;
|
||||
|
||||
|
||||
if ($EV_kWh > 0.0) {
|
||||
$EV_SOC = 100.0 * ($real_kWh - $underKWh) / $EV_kWh;
|
||||
} else {
|
||||
$EV_SOC = 0.0; // definierter Fallback
|
||||
}
|
||||
$EV_SOC = is_finite($EV_SOC) ? max(0.0, min(100.0, $EV_SOC)) : 0.0;
|
||||
|
||||
|
||||
|
||||
|
||||
$sdlDisKW = $sdlShareKW_entladen;
|
||||
$evDisKW = $evShareKW_entladen;
|
||||
$sdlChKW = $sdlShareKW_laden;
|
||||
$evChKW = $evShareKW_laden;
|
||||
|
||||
$real_kWh_ev = $real_kWh - $underKWh;
|
||||
$real_kWh_sdl = $underKWh;
|
||||
|
||||
} elseif ($upKWh < $real_kWh) {
|
||||
|
||||
$EV_SOC = 100.0;
|
||||
|
||||
$den1 = ($capKWh - $real_kWh + $underKWh);
|
||||
$den2 = (2.0 * $underKWh);
|
||||
|
||||
if ($den1 > 0.0 && $den2 > 0.0) {
|
||||
$SDL_SOC = min(100.0, ($den1 / $den2) * 100.0);
|
||||
} else {
|
||||
$SDL_SOC = 0.0;
|
||||
}
|
||||
$SDL_SOC = is_finite($SDL_SOC) ? max(0.0, min(100.0, $SDL_SOC)) : 0.0;
|
||||
|
||||
$sdlDisKW = $sdlShareKW_entladen;
|
||||
$evDisKW = $evShareKW_entladen;
|
||||
$sdlChKW = $sdlShareKW_laden;
|
||||
$evChKW = 0.0;
|
||||
|
||||
$real_kWh_ev = $capKWh - ($capKWh - $upKWh + $underKWh);
|
||||
$real_kWh_sdl = ($capKWh - $upKWh + $underKWh) - ($capKWh - $real_kWh);
|
||||
|
||||
} elseif ($underKWh > $real_kWh) {
|
||||
|
||||
$EV_SOC = 0.0;
|
||||
|
||||
$den = $upKWh + $underKWh;
|
||||
$SDL_SOC = ($den > 0.0) ? ($real_kWh * 100.0 / $den) : 0.0;
|
||||
$SDL_SOC = is_finite($SDL_SOC) ? max(0.0, min(100.0, $SDL_SOC)) : 0.0;
|
||||
|
||||
$sdlDisKW = $sdlShareKW_entladen;
|
||||
$evDisKW = 0.0;
|
||||
$sdlChKW = $sdlShareKW_laden;
|
||||
$evChKW = $evShareKW_laden;
|
||||
|
||||
$real_kWh_ev = 0.0;
|
||||
$real_kWh_sdl = $real_kWh;
|
||||
}
|
||||
|
||||
// Null/Full
|
||||
if ($real_kWh <= 0.0) {
|
||||
$sdlDisKW = 0.0;
|
||||
$real_kWh_ev = 0.0;
|
||||
$real_kWh_sdl = 0.0;
|
||||
} elseif ($real_kWh >= $capKWh) {
|
||||
$sdlChKW = 0.0;
|
||||
//$real_kWh_ev = $capKWh - ($upKWh + $underKWh);
|
||||
//$real_kWh_sdl = $upKWh + $underKWh;
|
||||
|
||||
$real_kWh_ev = $EV_kWh;
|
||||
$real_kWh_sdl = $SDL_kWh;
|
||||
}
|
||||
|
||||
$real_kWh_ev = max(0.0, $real_kWh_ev);
|
||||
$real_kWh_sdl = max(0.0, $real_kWh_sdl);
|
||||
|
||||
// Summen
|
||||
$totalCapKWh += $capKWh;
|
||||
|
||||
$sdlDisKW_ges += $sdlDisKW;
|
||||
$evDisKW_ges += $evDisKW;
|
||||
$sdlChKW_ges += $sdlChKW;
|
||||
$evChKW_ges += $evChKW;
|
||||
|
||||
$real_kWh_ev_ges += $real_kWh_ev;
|
||||
$real_kWh_sdl_ges += $real_kWh_sdl;
|
||||
|
||||
$SDL_kWh_ges += $SDL_kWh;
|
||||
$EV_kWh_ges += $EV_kWh;
|
||||
|
||||
$calc["batteries"][] = [
|
||||
"idx" => $c["idx"] ?? $i,
|
||||
"typ" => $typ,
|
||||
"SoC_varId" => $socVarId,
|
||||
"SoC_pct" => round($socPct, 3),
|
||||
"Effektive kWh" => round($real_kWh, 3),
|
||||
|
||||
"EV_SOC" => round($EV_SOC, 3),
|
||||
"SDL_SOC" => round($SDL_SOC, 3),
|
||||
|
||||
"EV_kWh" => round($real_kWh_ev, 3),
|
||||
"SDL_kWh" => round($real_kWh_sdl, 3),
|
||||
|
||||
"under_grenze_kWh" => round($underKWh, 3),
|
||||
"up_grenze_kWh" => round($upKWh, 3),
|
||||
|
||||
"SDL_Charge_kW" => round($sdlChKW, 3),
|
||||
"SDL_Discharge_kW" => round($sdlDisKW, 3),
|
||||
"EV_Charge_kW" => round($evChKW, 3),
|
||||
"EV_Discharge_kW" => round($evDisKW, 3),
|
||||
];
|
||||
}
|
||||
|
||||
$evPosPct = ($EV_kWh_ges > 0.0) ? ($real_kWh_ev_ges / $EV_kWh_ges * 100.0) : 0.0;
|
||||
$sdlPosPct = ($SDL_kWh_ges > 0.0) ? ($real_kWh_sdl_ges / $SDL_kWh_ges * 100.0) : 0.0;
|
||||
|
||||
$this->SetIdentValue("SDL_Pos", round($sdlPosPct, 3));
|
||||
$this->SetIdentValue("SoC_EV", round($evPosPct, 3));
|
||||
|
||||
$this->SetIdentValue("P_SDL_laden", round($sdlChKW_ges * 1000.0, 0));
|
||||
$this->SetIdentValue("P_SDL_entladen", round($sdlDisKW_ges * 1000.0, 0));
|
||||
$this->SetIdentValue("P_EV_laden", round($evChKW_ges * 1000.0, 0));
|
||||
$this->SetIdentValue("P_EV_entladen", round($evDisKW_ges * 1000.0, 0));
|
||||
|
||||
$calc["total"] = [
|
||||
"SDL_SoC_pct" => round($sdlPosPct, 3),
|
||||
"EV_SoC_pct" => round($evPosPct, 3),
|
||||
|
||||
"SDL_kWh_total" => round($SDL_kWh_ges, 3),
|
||||
"EV_kWh_total" => round($EV_kWh_ges, 3),
|
||||
|
||||
"SDL_Charge_kW" => round($sdlChKW_ges, 3),
|
||||
"SDL_Discharge_kW" => round($sdlDisKW_ges, 3),
|
||||
"EV_Charge_kW" => round($evChKW_ges, 3),
|
||||
"EV_Discharge_kW" => round($evDisKW_ges, 3),
|
||||
|
||||
"totalCap_kWh" => round($totalCapKWh, 3)
|
||||
];
|
||||
|
||||
$this->SetIdentValue("CalcJSON", json_encode($calc, JSON_PRETTY_PRINT));
|
||||
$this->ApplySetpoints();
|
||||
|
||||
|
||||
} catch (Throwable $e) {
|
||||
$this->SendDebug("Update ERROR", $e->getMessage() . " @ " . $e->getFile() . ":" . $e->getLine(), 0);
|
||||
$this->WriteAllZero("Exception: " . $e->getMessage());
|
||||
}
|
||||
finally {
|
||||
IPS_SemaphoreLeave($semKey); // ✅ Tür immer wieder aufschließen
|
||||
}
|
||||
}
|
||||
|
||||
private function BuildBatteryCache(bool $force): void
|
||||
{
|
||||
$batteriesRaw = $this->ReadPropertyString("Batteries");
|
||||
//$sdlTotalW = max(0, (int)$this->ReadPropertyInteger("SDL_Leistung"));
|
||||
$sdlTotalW_laden = max(0, (int)$this->ReadPropertyInteger("SDL_Leistung_Laden"));
|
||||
$sdlTotalW_entladen = max(0, (int)$this->ReadPropertyInteger("SDL_Leistung_Entladen"));
|
||||
$hours = self::HOURS;
|
||||
|
||||
$hash = md5(json_encode([
|
||||
"Batteries" => $batteriesRaw,
|
||||
"SDL_W_Laden" => $sdlTotalW_laden,
|
||||
"SDL_W_Entladen" => $sdlTotalW_entladen,
|
||||
"hours" => $hours
|
||||
]));
|
||||
|
||||
$oldHash = $this->GetBufferSafe("BatCacheHash");
|
||||
if (!$force && $oldHash === $hash) {
|
||||
return;
|
||||
}
|
||||
|
||||
$batteries = json_decode($batteriesRaw, true);
|
||||
if (!is_array($batteries)) $batteries = [];
|
||||
|
||||
$sumBatPowerW = 0.0;
|
||||
foreach ($batteries as $b) {
|
||||
$p = (float)($b["powerbat"] ?? 0);
|
||||
if ($p > 0) $sumBatPowerW += $p;
|
||||
}
|
||||
|
||||
$cache = [
|
||||
"inputs" => [
|
||||
"SDL_Leistung_W_laden" => $sdlTotalW_laden,
|
||||
"SDL_Leistung_W_entladen" => $sdlTotalW_entladen,
|
||||
"SumBatPower_W" => round($sumBatPowerW, 0),
|
||||
"hours" => $hours
|
||||
],
|
||||
"bats" => []
|
||||
];
|
||||
|
||||
if ($sumBatPowerW <= 0.0) {
|
||||
$this->SetBuffer("BatCacheHash", $hash);
|
||||
$this->SetBuffer("BatCacheJSON", json_encode($cache));
|
||||
$this->SendDebug("Cache", "sumBatPowerW=0 -> empty cache", 0);
|
||||
return;
|
||||
}
|
||||
|
||||
$sumBatPowerkW = $sumBatPowerW / 1000.0;
|
||||
//$sdlTotalkW = $sdlTotalW / 1000.0;
|
||||
|
||||
$sdlTotalkW_laden = $sdlTotalW_laden / 1000.0;
|
||||
$sdlTotalkW_entladen = $sdlTotalW_entladen / 1000.0;
|
||||
|
||||
foreach ($batteries as $idx => $b) {
|
||||
|
||||
$pBatW = max(0.0, (float)($b["powerbat"] ?? 0));
|
||||
$pBatkW = $pBatW / 1000.0;
|
||||
|
||||
$capKWh = max(0.0, (float)($b["capazity"] ?? 0));
|
||||
if ($capKWh <= 0.0) continue;
|
||||
|
||||
$socVarId = (int)($b["soc"] ?? 0);
|
||||
$typ = (string)($b["typ"] ?? ("Bat " . ($idx + 1)));
|
||||
|
||||
|
||||
// Mit laden und entladen unterschiedlich
|
||||
//----------------------------------------------------
|
||||
$sdlShareKW_laden = ($sumBatPowerkW > 0.0) ? ($sdlTotalkW_laden / $sumBatPowerkW * $pBatkW) : 0.0;
|
||||
$evShareKW_laden = $pBatkW - $sdlShareKW_laden;
|
||||
$upKWh = $capKWh - $sdlShareKW_laden * 0.5;
|
||||
|
||||
$sdlShareKW_entladen = ($sumBatPowerkW > 0.0) ? ($sdlTotalkW_entladen / $sumBatPowerkW * $pBatkW) : 0.0;
|
||||
$evShareKW_entladen = $pBatkW - $sdlShareKW_entladen;
|
||||
$underKWh = $sdlShareKW_entladen * 0.5;
|
||||
|
||||
$SDL_kWh = $underKWh + ($capKWh -$upKWh);
|
||||
$EV_kWh = $capKWh - $SDL_kWh;
|
||||
//----------------------------------------------------
|
||||
|
||||
$cache["bats"][] = [
|
||||
"idx" => $idx,
|
||||
"typ" => $typ,
|
||||
|
||||
"socVarId" => $socVarId,
|
||||
"capKWh" => $capKWh,
|
||||
|
||||
"pBatW" => $pBatW,
|
||||
"sdlShareKW_laden" => $sdlShareKW_laden,
|
||||
"sdlShareKW_entladen" => $sdlShareKW_entladen,
|
||||
"evShareKW_laden" => $evShareKW_laden,
|
||||
"evShareKW_entladen" => $evShareKW_entladen,
|
||||
|
||||
"underKWh" => $underKWh,
|
||||
"upKWh" => $upKWh,
|
||||
|
||||
"SDL_kWh_total" => $SDL_kWh,
|
||||
"EV_kWh_total" => $EV_kWh
|
||||
];
|
||||
}
|
||||
|
||||
$this->SetBuffer("BatCacheHash", $hash);
|
||||
$this->SetBuffer("BatCacheJSON", json_encode($cache));
|
||||
|
||||
$this->SendDebug("Cache", "Battery cache rebuilt (" . count($cache["bats"]) . " bats)", 0);
|
||||
}
|
||||
|
||||
private function GetBufferSafe(string $name): string
|
||||
{
|
||||
$v = $this->GetBuffer($name);
|
||||
return is_string($v) ? $v : "";
|
||||
}
|
||||
|
||||
private function WriteAllZero(string $reason): void
|
||||
{
|
||||
$this->SetIdentValue("SDL_Pos", 0.0);
|
||||
$this->SetIdentValue("SoC_EV", 0.0);
|
||||
|
||||
$this->SetIdentValue("P_SDL_laden", 0.0);
|
||||
$this->SetIdentValue("P_SDL_entladen", 0.0);
|
||||
|
||||
$this->SetIdentValue("P_EV_laden", 0.0);
|
||||
$this->SetIdentValue("P_EV_entladen", 0.0);
|
||||
|
||||
$this->SetIdentValue("CalcJSON", json_encode(["error" => $reason], JSON_PRETTY_PRINT));
|
||||
}
|
||||
|
||||
private function SetIdentValue(string $ident, $value): void
|
||||
{
|
||||
$id = @$this->GetIDForIdent($ident);
|
||||
if ($id <= 0) {
|
||||
$this->SendDebug(__FUNCTION__, "Ident nicht gefunden: $ident", 0);
|
||||
return;
|
||||
}
|
||||
SetValue($id, $value);
|
||||
}
|
||||
|
||||
private function ReadSocPercent(int $varId): float
|
||||
{
|
||||
// Falls jemand statt Variable direkt 0..100 einträgt
|
||||
if ($varId >= 0 && $varId <= 100 && !IPS_VariableExists($varId)) {
|
||||
return (float)$varId;
|
||||
}
|
||||
|
||||
if ($varId <= 0 || !IPS_VariableExists($varId)) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$v = GetValue($varId);
|
||||
if (!is_numeric($v)) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$soc = (float)$v;
|
||||
if ($soc < 0.0) $soc = 0.0;
|
||||
if ($soc > 100.0) $soc = 100.0;
|
||||
|
||||
return $soc;
|
||||
}
|
||||
|
||||
public function ApplySetpoints(): void
|
||||
{
|
||||
$pEvW = (float) GetValue($this->GetIDForIdent("Nennleistung_Soll_EV"));
|
||||
$pSdlW = (float) GetValue($this->GetIDForIdent("Nennleistung_Soll_SDL"));
|
||||
|
||||
//Diverenz zwischen EV und SDl berechnen
|
||||
|
||||
|
||||
// Methode für priorisierung
|
||||
$distribution = $this->CalculateBatteryDistribution($pEvW,$pSdlW);
|
||||
|
||||
|
||||
$this->WriteBatteryPowerSetpoints($distribution);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function CalculateBatteryDistribution(float $pEvW, float $pSdlW): array
|
||||
{
|
||||
$calcJsonId = $this->GetIDForIdent("CalcJSON");
|
||||
// Fallback, falls Variable leer ist oder nicht existiert
|
||||
if (!IPS_VariableExists($calcJsonId)) {
|
||||
return [];
|
||||
}
|
||||
$rawJson = (string)GetValue($calcJsonId);
|
||||
if (empty($rawJson)) {
|
||||
return [];
|
||||
}
|
||||
$calc = json_decode($rawJson, true);
|
||||
$batteries = $calc['batteries'] ?? [];
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Hilfsfunktion: Verteilungslogik (Closure)
|
||||
// ---------------------------------------------------------
|
||||
$distributePower = function(float $targetPower, string $mode) use ($batteries): array {
|
||||
// Initialisierung des Ergebnis-Arrays (Key = idx, Value = zugewiesene Watt)
|
||||
$result = [];
|
||||
foreach ($batteries as $bat) {
|
||||
$result[$bat['idx']] = 0.0;
|
||||
}
|
||||
|
||||
if (abs($targetPower) < 0.01) {
|
||||
return $result; // Nichts zu tun
|
||||
}
|
||||
|
||||
$isCharge = ($targetPower > 0);
|
||||
$absPower = abs($targetPower);
|
||||
// Relevante Keys basierend auf Modus (EV oder SDL) und Richtung (Laden/Entladen)
|
||||
$socKey = ($mode === 'EV') ? 'EV_SOC' : 'SDL_SOC';
|
||||
// Achtung: JSON Limits sind in kW, wir rechnen in Watt -> * 1000
|
||||
$limitKey = $isCharge ? $mode . '_Charge_kW' : $mode . '_Discharge_kW';
|
||||
|
||||
// 1. Batterien vorbereiten und gruppieren nach gerundetem SoC
|
||||
$groups = [];
|
||||
foreach ($batteries as $bat) {
|
||||
$soc = (int)round($bat[$socKey]); // Auf ganze Zahl runden
|
||||
$maxW = ((float)$bat[$limitKey]) * 1000.0; // kW in Watt umrechnen
|
||||
$groups[$soc][] = [
|
||||
'idx' => $bat['idx'],
|
||||
'maxW' => $maxW
|
||||
];
|
||||
}
|
||||
|
||||
// 2. Sortieren der Gruppen
|
||||
// Laden: Wenig SoC zuerst (ASC) -> leere füllen
|
||||
// Entladen: Viel SoC zuerst (DESC) -> volle leeren
|
||||
if ($isCharge) {
|
||||
ksort($groups);
|
||||
} else {
|
||||
krsort($groups);
|
||||
}
|
||||
|
||||
// 3. Verteilung
|
||||
$remainingNeeded = $absPower;
|
||||
|
||||
foreach ($groups as $soc => $groupBatteries) {
|
||||
if ($remainingNeeded <= 0.01) break;
|
||||
|
||||
// Gesamte verfügbare Leistung in dieser SoC-Gruppe ermitteln
|
||||
$groupTotalCapacity = 0.0;
|
||||
foreach ($groupBatteries as $gb) {
|
||||
$groupTotalCapacity += $gb['maxW'];
|
||||
}
|
||||
|
||||
// Wie viel können wir dieser Gruppe zuteilen?
|
||||
// Entweder alles was die Gruppe kann, oder den Restbedarf
|
||||
$powerForThisGroup = min($remainingNeeded, $groupTotalCapacity);
|
||||
|
||||
// Proportionale Aufteilung innerhalb der Gruppe
|
||||
// Falls Gruppe Kapazität 0 hat (Defekt/Voll), verhindern wir DivByZero
|
||||
if ($groupTotalCapacity > 0) {
|
||||
$ratio = $powerForThisGroup / $groupTotalCapacity;
|
||||
foreach ($groupBatteries as $gb) {
|
||||
$assigned = $gb['maxW'] * $ratio;
|
||||
$result[$gb['idx']] = $assigned;
|
||||
}
|
||||
}
|
||||
|
||||
$remainingNeeded -= $powerForThisGroup;
|
||||
}
|
||||
|
||||
// Wenn wir entladen, müssen die Werte negativ sein
|
||||
if (!$isCharge) {
|
||||
foreach ($result as $idx => $val) {
|
||||
$result[$idx] = -$val;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Hauptablauf
|
||||
// ---------------------------------------------------------
|
||||
|
||||
// 1. Berechnung für EV und SDL getrennt durchführen
|
||||
$evDistribution = $distributePower($pEvW, 'EV');
|
||||
$sdlDistribution = $distributePower($pSdlW, 'SDL');
|
||||
|
||||
// 2. Ergebnisse zusammenführen und Output formatieren
|
||||
$finalOutput = [];
|
||||
|
||||
foreach ($batteries as $bat) {
|
||||
$idx = $bat['idx'];
|
||||
// Summe der beiden Anforderungen (kann sich gegenseitig aufheben)
|
||||
$valEv = $evDistribution[$idx] ?? 0.0;
|
||||
$valSdl = $sdlDistribution[$idx] ?? 0.0;
|
||||
$totalW = $valEv + $valSdl;
|
||||
|
||||
// Aufteilen in Charge / Discharge für das Return-Format
|
||||
$chargeW = 0.0;
|
||||
$dischargeW = 0.0;
|
||||
|
||||
if ($totalW > 0) {
|
||||
$chargeW = abs($totalW);
|
||||
} else {
|
||||
$dischargeW = abs($totalW);
|
||||
}
|
||||
|
||||
// JSON Objekt erstellen
|
||||
$finalOutput[] = [
|
||||
"idx" => $idx,
|
||||
"typ" => (string)$bat['typ'], // Typ als String beibehalten
|
||||
"chargeW" => round($chargeW, 0), // Optional: runden für sauberes JSON
|
||||
"dischargeW" => round($dischargeW, 0)
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
$batteriesRaw = json_decode($this->ReadPropertyString("Batteries"));
|
||||
|
||||
$totalPower_ist = 0;
|
||||
|
||||
foreach ($batteriesRaw as $bat) {
|
||||
|
||||
$totalPower_ist += GetValue($bat->register_bat_power);
|
||||
|
||||
}
|
||||
|
||||
|
||||
$sumReq = (abs($pEvW) + abs($pSdlW));
|
||||
$sumReqRel = ($pEvW + $pSdlW);
|
||||
|
||||
if($sumReq==0){
|
||||
|
||||
$this->SetValue("Aktuelle_Leistung_EV", $totalPower_ist / 2);
|
||||
$this->SetValue("Aktuelle_Leistung_SDL", $totalPower_ist / 2);
|
||||
|
||||
}else{
|
||||
|
||||
if($pEvW>=0){
|
||||
|
||||
$this->SetValue("Aktuelle_Leistung_EV",((1+($totalPower_ist-$sumReqRel) / $sumReq)) * $pEvW);
|
||||
|
||||
}else{
|
||||
$this->SetValue("Aktuelle_Leistung_EV",((1-($totalPower_ist-$sumReqRel) / $sumReq)) * $pEvW);
|
||||
|
||||
}
|
||||
|
||||
if($pSdlW>=0){
|
||||
$this->SetValue("Aktuelle_Leistung_SDL",((1+($totalPower_ist-$sumReqRel) / $sumReq)) * $pSdlW);
|
||||
|
||||
}else{
|
||||
$this->SetValue("Aktuelle_Leistung_SDL",((1-($totalPower_ist-$sumReqRel) / $sumReq)) * $pSdlW);
|
||||
}
|
||||
|
||||
}
|
||||
/*
|
||||
$sumReq = (float)($pEvW + $pSdlW);
|
||||
|
||||
if (!is_finite($sumReq) || abs($sumReq) < 0.01) {
|
||||
|
||||
$this->SetValue("Aktuelle_Leistung_EV", $totalPower_ist / 2);
|
||||
$this->SetValue("Aktuelle_Leistung_SDL", $totalPower_ist / 2);
|
||||
|
||||
} else {
|
||||
|
||||
$this->SetValue("Aktuelle_Leistung_EV",($totalPower_ist / $sumReq) * $pEvW);
|
||||
|
||||
$this->SetValue("Aktuelle_Leistung_SDL",($totalPower_ist / $sumReq) * $pSdlW);
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
|
||||
return $finalOutput;
|
||||
}
|
||||
|
||||
private function WriteBatteryPowerSetpoints(array $distribution): void
|
||||
{
|
||||
IPS_LogMessage(
|
||||
__FUNCTION__,
|
||||
"distribution=" . json_encode($distribution, JSON_PRETTY_PRINT)
|
||||
);
|
||||
|
||||
$batteriesCfg = json_decode($this->ReadPropertyString("Batteries"), true);
|
||||
if (!is_array($batteriesCfg) || empty($batteriesCfg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($distribution as $d) {
|
||||
|
||||
$idx = (int)($d["idx"] ?? -1);
|
||||
if ($idx < 0 || !isset($batteriesCfg[$idx])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cfg = $batteriesCfg[$idx];
|
||||
|
||||
$typ = (string)($d["typ"] ?? ($cfg["typ"] ?? ("Bat " . ($idx + 1))));
|
||||
|
||||
// ✅ immer positiv
|
||||
$chargeW = max(0.0, (float)($d["chargeW"] ?? 0.0));
|
||||
$dischargeW = max(0.0, (float)($d["dischargeW"] ?? 0.0));
|
||||
|
||||
// nie gleichzeitig
|
||||
if ($chargeW > 0.0 && $dischargeW > 0.0) {
|
||||
$this->SendDebug("WriteBatteryPowerSetpoints", "WARN: both >0 for $typ (idx=$idx) -> set 0", 0);
|
||||
$chargeW = 0.0;
|
||||
$dischargeW = 0.0;
|
||||
}
|
||||
|
||||
$this->WriteByVendorRegistersSingleMode($typ, $cfg, $chargeW, $dischargeW);
|
||||
|
||||
$this->SendDebug("Setpoints", "$typ (idx=$idx) charge={$chargeW}W discharge={$dischargeW}W", 0);
|
||||
}
|
||||
}
|
||||
|
||||
private function WriteByVendorRegistersSingleMode(string $typ, array $cfg, float $chargeW, float $dischargeW): void
|
||||
{
|
||||
$t = mb_strtolower($typ);
|
||||
|
||||
// Leistungs-Variablen
|
||||
$varPowerCharge = (int)($cfg["powerbat_laden"] ?? 0);
|
||||
$varPowerDisch = (int)($cfg["powerbat_entladen"] ?? 0);
|
||||
|
||||
// ✅ EIN Modus-Register
|
||||
$varMode = (int)($cfg["register_ladenentladen_modus"] ?? 0);
|
||||
|
||||
// sichere Writer
|
||||
$setInt = function(int $varId, int $value): void {
|
||||
if ($varId > 0 && IPS_VariableExists($varId)) {
|
||||
RequestAction($varId, $value);
|
||||
}
|
||||
};
|
||||
$setW = function(int $varId, float $w): void {
|
||||
if ($varId > 0 && IPS_VariableExists($varId)) {
|
||||
RequestAction($varId, (int)round(max(0.0, $w), 0)); // ✅ niemals negativ
|
||||
}
|
||||
};
|
||||
|
||||
// Moduscodes je Typ
|
||||
$modeCharge = 1; $modeDisch = 2; // Default
|
||||
if (strpos($t, "goodwe") !== false) {
|
||||
$modeCharge = 11; $modeDisch = 12;
|
||||
} elseif (strpos($t, "solaredge") !== false) {
|
||||
$modeCharge = 3; $modeDisch = 4;
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Laden
|
||||
// =========================
|
||||
if ($chargeW > 0.0) {
|
||||
|
||||
// Modus setzen (ein Register)
|
||||
$setInt($varMode, $modeCharge);
|
||||
|
||||
// GoodWe: nur powerbat_laden
|
||||
if (strpos($t, "goodwe") !== false) {
|
||||
$setW($varPowerCharge, (int)$chargeW);
|
||||
$setW($varPowerDisch, (int)0);
|
||||
return;
|
||||
}
|
||||
|
||||
// SolarEdge + Default: zwei Leistungsregister
|
||||
$setW($varPowerCharge, $chargeW);
|
||||
$setW($varPowerDisch, 0.0);
|
||||
return;
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Entladen
|
||||
// =========================
|
||||
if ($dischargeW > 0.0) {
|
||||
|
||||
// Modus setzen (ein Register)
|
||||
$setInt($varMode, $modeDisch);
|
||||
|
||||
// GoodWe: Entladen nutzt trotzdem powerbat_laden (immer positiv)
|
||||
if (strpos($t, "goodwe") !== false) {
|
||||
$setW($varPowerCharge, (int)$dischargeW);
|
||||
$setW($varPowerDisch, (int)0);
|
||||
return;
|
||||
}
|
||||
|
||||
// SolarEdge + Default: Entladen in powerbat_entladen
|
||||
$setW($varPowerDisch, $dischargeW);
|
||||
$setW($varPowerCharge, 0.0);
|
||||
return;
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Stop / Neutral
|
||||
// =========================
|
||||
$setW($varPowerCharge, 0.0);
|
||||
$setW($varPowerDisch, 0.0);
|
||||
// optional: Modus nicht anfassen oder auf 0 setzen:
|
||||
// $setInt($varMode, 0);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
?>
|
||||
@@ -36,15 +36,15 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"type": "SelectVariable",
|
||||
"name": "MaxBatterieleistung",
|
||||
"caption": "Maximale Batterieleistung",
|
||||
"caption": "Maximale Ladeleistung",
|
||||
"suffix": ""
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"type": "SelectVariable",
|
||||
"name": "MaxNachladen",
|
||||
"caption": "Maximum Nachladen",
|
||||
"caption": "Maximum Nachladeleistung",
|
||||
"suffix": ""
|
||||
},
|
||||
{
|
||||
|
||||
@@ -91,52 +91,109 @@ class Batterie extends IPSModule
|
||||
break;
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
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;
|
||||
$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 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;
|
||||
// $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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -193,35 +250,6 @@ public function RequestAction($Ident, $Value)
|
||||
}elseif ($batterieManagement == 2 && $batterietyp == 2) {
|
||||
$this->SetValue("Batteriemanagement_Variabel", 4);
|
||||
}
|
||||
/*
|
||||
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);
|
||||
}
|
||||
|
||||
}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);
|
||||
}
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
$batterietyp = $this->ReadPropertyInteger("Batterietyp");
|
||||
@@ -307,16 +335,7 @@ public function RequestAction($Ident, $Value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Prüfe auf Änderung der Leistung im Vergleich zur letzten Einstellung
|
||||
@@ -347,7 +366,8 @@ 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"));
|
||||
$maxentladeleistung = GetValue($this->ReadPropertyInteger("MaxNachladen"));
|
||||
$dummy_array = [];
|
||||
$batterieladezustand = GetValue($this->ReadPropertyInteger("Batterieladezustand"));
|
||||
$filtered_powersteps_entladen = [];
|
||||
@@ -357,8 +377,8 @@ public function RequestAction($Ident, $Value)
|
||||
}
|
||||
|
||||
$netzbezug = GetValue($this->ReadPropertyInteger("Netzbezug"));
|
||||
if (abs($netzbezug) > $maxleistung) {
|
||||
$netzbezug = $maxleistung * (-1);
|
||||
if (abs($netzbezug) > $maxentladeleistung) {
|
||||
$netzbezug = $maxentladeleistung * (-1);
|
||||
}
|
||||
|
||||
if($batterieladezustand>(5+$aufdasnachladen)){
|
||||
@@ -431,7 +451,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");
|
||||
|
||||
|
||||
@@ -1,94 +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":"Select",
|
||||
"name":"Batterietyp",
|
||||
"caption":"Batterietyp",
|
||||
"options":[
|
||||
{
|
||||
"caption":"Goodwe",
|
||||
"value":1
|
||||
},
|
||||
{
|
||||
"caption":"Solaredge",
|
||||
"value":2
|
||||
},
|
||||
{
|
||||
"caption":"Sig Energy",
|
||||
"value":3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "MaxBatterieleistung",
|
||||
"caption": "Maximale Batterieleistung",
|
||||
"suffix": ""
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "MaxNachladen",
|
||||
"caption": "Maximum Nachladen",
|
||||
"suffix": ""
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "AufdasNachladen",
|
||||
"caption": "Auf so viel Prozent nachladen",
|
||||
"suffix": ""
|
||||
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "MinimumEntladen",
|
||||
"caption": "Minimum des Batterieladezustand",
|
||||
"suffix": ""
|
||||
},
|
||||
{
|
||||
"type":"Select",
|
||||
"name":"Batteriemanagement",
|
||||
"caption":"Batteriemanagement",
|
||||
"options":[
|
||||
{
|
||||
"caption":"Durch Wechselrichter",
|
||||
"value":1
|
||||
},
|
||||
{
|
||||
"caption":"Durch EMS Symcon",
|
||||
"value":2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "Batterieladezustand",
|
||||
"caption": "Batterieladezustand",
|
||||
"test": true
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "Netzbezug",
|
||||
"caption": "Variable mit dem zu regelnden Netzbezug"
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"caption": "Goodwe: Laden=11, Entladen=12,\nSolaredge: Laden=3, Entladen=4,\nSig Energy: Laden=3, Entladen=6, P in kW"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,483 +0,0 @@
|
||||
<?php
|
||||
|
||||
class Batterie_test extends IPSModule
|
||||
{
|
||||
public function Create()
|
||||
{
|
||||
parent::Create();
|
||||
|
||||
// Batterie spezifische Eigenschaften
|
||||
$this->RegisterPropertyInteger("MaxBatterieleistung", 0);
|
||||
$this->RegisterPropertyInteger("Batteriespannung", 50);
|
||||
$this->RegisterPropertyInteger("AufdasNachladen",0);
|
||||
$this->RegisterPropertyInteger("MinimumEntladen",0);
|
||||
$this->RegisterPropertyInteger("Batterieladezustand",0);
|
||||
$this->RegisterPropertyInteger("Batteriemanagement", 1);
|
||||
$this->RegisterPropertyInteger("Batterietyp", 1);
|
||||
$this->RegisterPropertyInteger("MaxNachladen",0);
|
||||
$this->RegisterPropertyInteger("Netzbezug", 0); // Initialisierung mit 0
|
||||
$this->RegisterPropertyInteger("Interval", 2); // 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->RegisterVariableInteger("Laden_Entladen","Laden_Entladen", "",3);
|
||||
//$this->RegisterVariableFloat("Ladeleistung","Ladeleistung", "",0);
|
||||
//$this->RegisterVariableFloat("Goodwe_EntLadeleistung","Goodwe_EntLadeleistung", "",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->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);
|
||||
|
||||
$batterietyp = $this->ReadPropertyInteger("Batterietyp");
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
|
||||
$batterietyp = $this->ReadPropertyInteger("Batterietyp");
|
||||
$batterieManagement = $this->ReadPropertyInteger("Batteriemanagement");
|
||||
|
||||
// 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);
|
||||
}
|
||||
/*
|
||||
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);
|
||||
}
|
||||
|
||||
}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);
|
||||
}
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
$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("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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}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) {
|
||||
$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)
|
||||
{
|
||||
IPS_LogMessage("Batterie", "Currentdata");
|
||||
|
||||
$array_powersteps = $this->GeneratePowerSteps($this->GetValue("Aktuelle_Leistung"));
|
||||
$aufdasnachladen = $this->ReadPropertyInteger("AufdasNachladen");
|
||||
$minimumentladen = $this->ReadPropertyInteger("MinimumEntladen");
|
||||
$maxleistung = $this->ReadPropertyInteger("MaxBatterieleistung");
|
||||
$dummy_array = [];
|
||||
$batterieladezustand = GetValue($this->ReadPropertyInteger("Batterieladezustand"));
|
||||
$filtered_powersteps_entladen = [];
|
||||
if ($this->ReadPropertyInteger("Batteriemanagement") == 1) {
|
||||
$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>99){
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
||||
67
Energy_Pie/README.md
Normal file
67
Energy_Pie/README.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Manager_1
|
||||
Beschreibung des Moduls.
|
||||
|
||||
### Inhaltsverzeichnis
|
||||
|
||||
1. [Funktionsumfang](#1-funktionsumfang)
|
||||
2. [Voraussetzungen](#2-voraussetzungen)
|
||||
3. [Software-Installation](#3-software-installation)
|
||||
4. [Einrichten der Instanzen in IP-Symcon](#4-einrichten-der-instanzen-in-ip-symcon)
|
||||
5. [Statusvariablen und Profile](#5-statusvariablen-und-profile)
|
||||
6. [WebFront](#6-webfront)
|
||||
7. [PHP-Befehlsreferenz](#7-php-befehlsreferenz)
|
||||
|
||||
### 1. Funktionsumfang
|
||||
|
||||
*
|
||||
|
||||
### 2. Voraussetzungen
|
||||
|
||||
- IP-Symcon ab Version 7.1
|
||||
|
||||
### 3. Software-Installation
|
||||
|
||||
* Über den Module Store das 'Manager_1'-Modul installieren.
|
||||
* Alternativ über das Module Control folgende URL hinzufügen
|
||||
|
||||
### 4. Einrichten der Instanzen in IP-Symcon
|
||||
|
||||
Unter 'Instanz hinzufügen' kann das 'Manager_1'-Modul mithilfe des Schnellfilters gefunden werden.
|
||||
- Weitere Informationen zum Hinzufügen von Instanzen in der [Dokumentation der Instanzen](https://www.symcon.de/service/dokumentation/konzepte/instanzen/#Instanz_hinzufügen)
|
||||
|
||||
__Konfigurationsseite__:
|
||||
|
||||
Name | Beschreibung
|
||||
-------- | ------------------
|
||||
|
|
||||
|
|
||||
|
||||
### 5. Statusvariablen und Profile
|
||||
|
||||
Die Statusvariablen/Kategorien werden automatisch angelegt. Das Löschen einzelner kann zu Fehlfunktionen führen.
|
||||
|
||||
#### Statusvariablen
|
||||
|
||||
Name | Typ | Beschreibung
|
||||
------ | ------- | ------------
|
||||
| |
|
||||
| |
|
||||
|
||||
#### Profile
|
||||
|
||||
Name | Typ
|
||||
------ | -------
|
||||
|
|
||||
|
|
||||
|
||||
### 6. WebFront
|
||||
|
||||
Die Funktionalität, die das Modul im WebFront bietet.
|
||||
|
||||
### 7. PHP-Befehlsreferenz
|
||||
|
||||
`boolean GEF_BeispielFunktion(integer $InstanzID);`
|
||||
Erklärung der Funktion.
|
||||
|
||||
Beispiel:
|
||||
`GEF_BeispielFunktion(12345);`
|
||||
@@ -4,6 +4,5 @@
|
||||
{ "type": "SelectVariable", "name": "VarConsumption", "caption": "Verbrauch (kWh)" },
|
||||
{ "type": "SelectVariable", "name": "VarFeedIn", "caption": "Einspeisung (kWh)" },
|
||||
{ "type": "SelectVariable", "name": "VarGrid", "caption": "Bezug Netz (kWh)" }
|
||||
],
|
||||
"actions": []
|
||||
}
|
||||
]
|
||||
}
|
||||
219
Energy_Pie/module.html
Normal file
219
Energy_Pie/module.html
Normal file
@@ -0,0 +1,219 @@
|
||||
<div id="wrap" style="padding:12px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;">
|
||||
|
||||
<!-- Controls -->
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;">
|
||||
<label style="display:flex;gap:6px;align-items:center;">
|
||||
<span>Zeitraum</span>
|
||||
<select id="range">
|
||||
<option value="day">Tag</option>
|
||||
<option value="week">Woche</option>
|
||||
<option value="month">Monat</option>
|
||||
<option value="year">Jahr</option>
|
||||
<option value="total">Gesamt</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label style="display:flex;gap:6px;align-items:center;">
|
||||
<span>Datum</span>
|
||||
<input id="date" type="date" />
|
||||
</label>
|
||||
|
||||
<button id="prev" type="button">◀</button>
|
||||
<button id="today" type="button">Heute</button>
|
||||
<button id="next" type="button">▶</button>
|
||||
</div>
|
||||
|
||||
<div id="period" style="margin-top:10px;font-size:15px;font-weight:600;"></div>
|
||||
<div id="hint" style="margin-top:6px;font-size:15px;font-weight:700;"></div>
|
||||
|
||||
<div id="grid"
|
||||
style="margin-top:14px;
|
||||
display:grid;
|
||||
grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
gap:14px;">
|
||||
</div>
|
||||
|
||||
<div id="err" style="margin-top:8px;color:#ffb4b4;font-size:13px;"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@media (max-width:720px){
|
||||
#grid{grid-template-columns:1fr}
|
||||
}
|
||||
|
||||
#wrap{
|
||||
position:relative;
|
||||
overflow:hidden;
|
||||
min-height:100vh;
|
||||
background:#121216;
|
||||
}
|
||||
|
||||
#wrap::before{
|
||||
content:"";
|
||||
position:absolute;
|
||||
inset:-35%;
|
||||
background:
|
||||
radial-gradient(520px 420px at 18% 18%, rgba(99,179,255,.42), transparent 60%),
|
||||
radial-gradient(560px 460px at 82% 28%, rgba(168,85,247,.38), transparent 62%),
|
||||
radial-gradient(620px 520px at 55% 85%, rgba(99,179,255,.26), transparent 64%);
|
||||
filter:blur(18px);
|
||||
pointer-events:none;
|
||||
}
|
||||
|
||||
#wrap>*{position:relative;z-index:1}
|
||||
|
||||
#hint .kv{margin-right:14px;white-space:nowrap}
|
||||
#hint .kv b{font-weight:900}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
|
||||
const elRange = document.getElementById('range');
|
||||
const elDate = document.getElementById('date');
|
||||
const elPrev = document.getElementById('prev');
|
||||
const elToday = document.getElementById('today');
|
||||
const elNext = document.getElementById('next');
|
||||
const elPeriod = document.getElementById('period');
|
||||
const elHint = document.getElementById('hint');
|
||||
const elGrid = document.getElementById('grid');
|
||||
const elErr = document.getElementById('err');
|
||||
|
||||
function showErr(e){
|
||||
elErr.textContent = e?.message || e;
|
||||
}
|
||||
|
||||
function ra(){
|
||||
return window.requestAction || window.parent?.requestAction || null;
|
||||
}
|
||||
|
||||
function send(id,val){
|
||||
const f = ra();
|
||||
if(f) try{f(id,val)}catch(e){showErr(e)}
|
||||
}
|
||||
|
||||
function fmt(d){
|
||||
const p=n=>String(n).padStart(2,'0');
|
||||
return `${p(d.getDate())}.${p(d.getMonth()+1)}.${d.getFullYear()}`;
|
||||
}
|
||||
|
||||
function monthName(m){
|
||||
return ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','Dez'][m];
|
||||
}
|
||||
|
||||
function isoWeek(d){
|
||||
d=new Date(Date.UTC(d.getFullYear(),d.getMonth(),d.getDate()));
|
||||
d.setUTCDate(d.getUTCDate()+4-(d.getUTCDay()||7));
|
||||
const y=d.getUTCFullYear();
|
||||
const s=new Date(Date.UTC(y,0,1));
|
||||
return Math.ceil((((d-s)/86400000)+1)/7)+" "+y;
|
||||
}
|
||||
|
||||
function render(data){
|
||||
if(!data) return;
|
||||
|
||||
elRange.value = data.range || 'day';
|
||||
elDate.value = data.date || '';
|
||||
|
||||
elDate.disabled = (data.range === 'total');
|
||||
|
||||
// Zeitraum-Anzeige
|
||||
if (data.range === 'total') {
|
||||
// ✅ Wunsch: bei Gesamt nix anzeigen
|
||||
elPeriod.textContent = '';
|
||||
} else if (data.tStart && data.tEnd) {
|
||||
const s = new Date(data.tStart * 1000);
|
||||
const e = new Date(data.tEnd * 1000 - 1000);
|
||||
|
||||
if (data.range === 'day')
|
||||
elPeriod.textContent = `Zeitraum: ${fmt(s)}`;
|
||||
else if (data.range === 'week')
|
||||
elPeriod.textContent = `Zeitraum: Woche ${isoWeek(s)}`;
|
||||
else if (data.range === 'month')
|
||||
elPeriod.textContent = `Zeitraum: ${monthName(s.getMonth())} ${s.getFullYear()}`;
|
||||
else if (data.range === 'year')
|
||||
elPeriod.textContent = `Zeitraum: ${s.getFullYear()}`;
|
||||
else
|
||||
elPeriod.textContent = ''; // fallback: lieber leer als falsch
|
||||
} else {
|
||||
elPeriod.textContent = '';
|
||||
}
|
||||
|
||||
// Keine Daten
|
||||
if(data.hasData === false){
|
||||
elHint.innerHTML = `<b>Letzter Zeitpunkt</b> <span style="opacity:.7">(Keine Werte für diesen Zeitraum)</span>`;
|
||||
elGrid.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Werte
|
||||
const v = data.values || {};
|
||||
elHint.innerHTML = `
|
||||
<span class="kv"><b>Produktion:</b> ${v.Produktion?.toFixed(2) || 0} kWh</span>
|
||||
<span class="kv"><b>Verbrauch:</b> ${v.Hausverbrauch?.toFixed(2) || 0} kWh</span>
|
||||
<span class="kv"><b>Netzbezug:</b> ${v.Netz?.toFixed(2) || 0} kWh</span>
|
||||
<span class="kv"><b>Einspeisung:</b> ${v.Einspeisung?.toFixed(2) || (0)} kWh</span>
|
||||
`;
|
||||
|
||||
const donut = (t, p, c) => {
|
||||
const r = 56;
|
||||
const C = 2 * Math.PI * r;
|
||||
const dash = (Math.max(0, Math.min(100, p)) / 100) * C;
|
||||
|
||||
return `
|
||||
<div style="text-align:center;display:flex;flex-direction:column;align-items:center;gap:10px;">
|
||||
<div style="font-weight:800;margin-bottom:-6px;font-size:15px;">
|
||||
${t}
|
||||
</div>
|
||||
|
||||
<div style="position:relative;width:160px;height:160px;">
|
||||
<svg width="160" height="160" viewBox="0 0 160 160">
|
||||
<circle cx="80" cy="80" r="${r}"
|
||||
stroke="rgba(255,255,255,0.18)"
|
||||
stroke-width="18"
|
||||
fill="none" />
|
||||
|
||||
<circle cx="80" cy="80" r="${r}"
|
||||
stroke="${c}"
|
||||
stroke-width="18"
|
||||
fill="none"
|
||||
stroke-linecap="butt"
|
||||
stroke-dasharray="${dash} ${C}"
|
||||
transform="rotate(-90 80 80)"
|
||||
style="filter: drop-shadow(0 0 12px ${c});" />
|
||||
</svg>
|
||||
|
||||
<div style="position:absolute;inset:0;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
font-size:24px;font-weight:900;">
|
||||
${Number(p).toFixed(1)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
const prod = v.Produktion || 0;
|
||||
const cons = v.Hausverbrauch || 0;
|
||||
const grid = v.Netz || 0;
|
||||
const feed = v.Einspeisung || 0;
|
||||
const eigen = Math.max(cons - grid, 0);
|
||||
|
||||
elGrid.innerHTML =
|
||||
donut('Eigenverbrauchsquote', prod ? (eigen / prod * 100) : 0, '#63B3FF') +
|
||||
donut('Autarkiegrad', cons ? (eigen / cons * 100) : 0, '#A855F7');
|
||||
}
|
||||
|
||||
window.handleMessage = d=>{
|
||||
try{if(typeof d==='string')d=JSON.parse(d)}catch{}
|
||||
render(d);
|
||||
};
|
||||
|
||||
elRange.onchange = ()=>send('SetRange',elRange.value);
|
||||
elDate.onchange = ()=>send('SetDate',elDate.value);
|
||||
elPrev.onclick = ()=>send('Prev',1);
|
||||
elNext.onclick = ()=>send('Next',1);
|
||||
elToday.onclick = ()=>send('Today',1);
|
||||
|
||||
})();
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "{DDE89CBE-4411-5FF4-4931-14204E05CAD0}",
|
||||
"name": "PV_Visu",
|
||||
"id": "{97BFC43B-9BE5-AB7D-EC5A-E31BB54878E0}",
|
||||
"name": "Energy_Pie",
|
||||
"type": 3,
|
||||
"vendor": "Belevo AG",
|
||||
"aliases": [],
|
||||
@@ -10,3 +10,4 @@
|
||||
"prefix": "",
|
||||
"url": ""
|
||||
}
|
||||
|
||||
256
Energy_Pie/module.php
Normal file
256
Energy_Pie/module.php
Normal file
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
class Energy_Pie extends IPSModule
|
||||
{
|
||||
private const ATTR_RANGE = 'Range';
|
||||
private const ATTR_DATE = 'Date';
|
||||
public function Create(): void
|
||||
{
|
||||
parent::Create();
|
||||
// Source variables (logged counters, kWh)
|
||||
$this->RegisterPropertyInteger('VarProduction', 0);
|
||||
$this->RegisterPropertyInteger('VarConsumption', 0);
|
||||
$this->RegisterPropertyInteger('VarFeedIn', 0);
|
||||
$this->RegisterPropertyInteger('VarGrid', 0);
|
||||
// Persisted UI state
|
||||
$this->RegisterAttributeString(self::ATTR_RANGE, 'day');
|
||||
$this->RegisterAttributeString(self::ATTR_DATE, date('Y-m-d'));
|
||||
// Enable individual visualization (HTML-SDK)
|
||||
$this->SetVisualizationType(1);
|
||||
// IMPORTANT: Timer calls global helper below (must exist!)
|
||||
$this->RegisterTimer('AutoPush', 0, 'IPS_RequestAction($_IPS["TARGET"], "Refresh", 1);');
|
||||
}
|
||||
public function ApplyChanges(): void
|
||||
{
|
||||
parent::ApplyChanges();
|
||||
// ensure range valid
|
||||
$range = $this->ReadAttributeString(self::ATTR_RANGE);
|
||||
if (!in_array($range, ['day', 'week', 'month', 'year', 'total'], true)) {
|
||||
$this->WriteAttributeString(self::ATTR_RANGE, 'day');
|
||||
}
|
||||
// ensure date valid (not empty/invalid/future)
|
||||
$date = $this->ReadAttributeString(self::ATTR_DATE);
|
||||
if ($date === '' || !$this->isValidDate($date) || strtotime($date . ' 00:00:00') > time()) {
|
||||
$this->WriteAttributeString(self::ATTR_DATE, date('Y-m-d'));
|
||||
}
|
||||
// Fullscreen-Fix: push periodically (adjust as you like)
|
||||
// 2000ms = alle 2 Sekunden (stabil, aber nicht ganz so brutal wie 1000ms)
|
||||
$this->SetTimerInterval('AutoPush', 2000);
|
||||
$this->RecalculateAndPush();
|
||||
}
|
||||
public function GetVisualizationTile(): string
|
||||
{
|
||||
$path = __DIR__ . '/module.html';
|
||||
if (!file_exists($path)) {
|
||||
return '<div style="padding:12px;font-family:sans-serif;">module.html fehlt</div>';
|
||||
}
|
||||
return file_get_contents($path);
|
||||
}
|
||||
public function RequestAction($Ident, $Value): void
|
||||
{
|
||||
switch ($Ident) {
|
||||
case 'SetRange':
|
||||
$range = (string)$Value;
|
||||
if (!in_array($range, ['day', 'week', 'month', 'year', 'total'], true)) {
|
||||
return;
|
||||
}
|
||||
$this->WriteAttributeString(self::ATTR_RANGE, $range);
|
||||
$this->RecalculateAndPush();
|
||||
break;
|
||||
case 'SetDate':
|
||||
$date = (string)$Value;
|
||||
if (!$this->isValidDate($date)) {
|
||||
return;
|
||||
}
|
||||
$this->WriteAttributeString(self::ATTR_DATE, $date);
|
||||
$this->RecalculateAndPush();
|
||||
break;
|
||||
case 'Prev':
|
||||
case 'Next':
|
||||
case 'Today':
|
||||
$this->ShiftDate($Ident);
|
||||
$this->RecalculateAndPush();
|
||||
break;
|
||||
case 'Refresh':
|
||||
$this->RecalculateAndPush();
|
||||
break;
|
||||
default:
|
||||
// ignore unknown
|
||||
return;
|
||||
}
|
||||
}
|
||||
private function RecalculateAndPush(): void
|
||||
{
|
||||
$range = $this->ReadAttributeString(self::ATTR_RANGE);
|
||||
$date = $this->ReadAttributeString(self::ATTR_DATE);
|
||||
[$tStart, $tEnd] = $this->getRange($range, $date);
|
||||
$dbgProd = [];
|
||||
$dbgFeed = [];
|
||||
$dbgGrid = [];
|
||||
$prod = $this->readDelta($this->ReadPropertyInteger('VarProduction'), $tStart, $tEnd, $dbgProd);
|
||||
$feed = $this->readDelta($this->ReadPropertyInteger('VarFeedIn'), $tStart, $tEnd, $dbgFeed);
|
||||
$grid = $this->readDelta($this->ReadPropertyInteger('VarGrid'), $tStart, $tEnd, $dbgGrid);
|
||||
$hasData = (($dbgProd['count'] ?? 0) > 0) || (($dbgFeed['count'] ?? 0) > 0) || (($dbgGrid['count'] ?? 0) > 0);
|
||||
$noDataHint = (!$hasData && $range !== 'total') ? 'Letzter Zeitpunkt' : '';
|
||||
|
||||
|
||||
// House = Prod - Feed + Grid
|
||||
$house = $prod - $feed + $grid;
|
||||
if ($house < 0) $house = 0.0;
|
||||
$payload = [
|
||||
'range' => $range,
|
||||
'date' => $date,
|
||||
'tStart' => $tStart,
|
||||
'tEnd' => $tEnd,
|
||||
'hasData' => $hasData,
|
||||
'noDataHint' => $noDataHint,
|
||||
'values' => [
|
||||
'Produktion' => (float)$prod,
|
||||
'Einspeisung' => (float)$feed,
|
||||
'Netz' => (float)$grid,
|
||||
'Hausverbrauch' => (float)$house
|
||||
],
|
||||
// kannst du später entfernen
|
||||
'debug' => [
|
||||
'prod' => $dbgProd,
|
||||
'feed' => $dbgFeed,
|
||||
'grid' => $dbgGrid
|
||||
]
|
||||
];
|
||||
$this->UpdateVisualizationValue(json_encode($payload, JSON_THROW_ON_ERROR));
|
||||
}
|
||||
private function getRange(string $range, string $dateYmd): array
|
||||
{
|
||||
$now = time();
|
||||
if ($range === 'total') {
|
||||
return [0, $now];
|
||||
}
|
||||
$base = strtotime($dateYmd . ' 00:00:00') ?: strtotime(date('Y-m-d') . ' 00:00:00');
|
||||
switch ($range) {
|
||||
case 'day':
|
||||
return [$base, $base + 86400];
|
||||
case 'week':
|
||||
$dow = (int)date('N', $base); // 1=Mon..7=Sun
|
||||
$start = $base - (($dow - 1) * 86400);
|
||||
return [$start, $start + 7 * 86400];
|
||||
case 'month':
|
||||
$start = strtotime(date('Y-m-01 00:00:00', $base));
|
||||
$end = strtotime(date('Y-m-01 00:00:00', strtotime('+1 month', $start)));
|
||||
return [$start, $end];
|
||||
|
||||
case 'year':
|
||||
$start = strtotime(date('Y-01-01 00:00:00', $base));
|
||||
$end = strtotime(date('Y-01-01 00:00:00', strtotime('+1 year', $start)));
|
||||
return [$start, $end];
|
||||
|
||||
default:
|
||||
return [$base, $base + 86400];
|
||||
}
|
||||
}
|
||||
public function GetVisualizationPopup(): string
|
||||
{
|
||||
// Popup (Fullscreen) soll das gleiche HTML wie das Tile anzeigen
|
||||
$this->RecalculateAndPush();
|
||||
return $this->GetVisualizationTile();
|
||||
}
|
||||
private function readDelta(int $varId, int $tStart, int $tEnd, array &$dbg): float
|
||||
{
|
||||
$dbg = [
|
||||
'varId' => $varId,
|
||||
'archiveId' => 0,
|
||||
'count' => 0,
|
||||
'first' => null,
|
||||
'last' => null,
|
||||
'vStart' => null,
|
||||
'vEnd' => null
|
||||
];
|
||||
if ($varId <= 0 || !IPS_VariableExists($varId)) {
|
||||
return 0.0;
|
||||
}
|
||||
$archiveID = IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0] ?? 0;
|
||||
$dbg['archiveId'] = $archiveID;
|
||||
if ($archiveID <= 0) {
|
||||
return 0.0;
|
||||
}
|
||||
$values = @AC_GetLoggedValues($archiveID, $varId, $tStart - 86400, $tEnd + 86400, 0);
|
||||
if (empty($values)) {
|
||||
return 0.0;
|
||||
}
|
||||
$firstTs = (int)$values[0]['TimeStamp'];
|
||||
$lastTs = (int)$values[count($values) - 1]['TimeStamp'];
|
||||
if ($firstTs > $lastTs) {
|
||||
$values = array_reverse($values);
|
||||
}
|
||||
$dbg['count'] = count($values);
|
||||
$dbg['first'] = (float)$values[0]['Value'];
|
||||
$dbg['last'] = (float)$values[count($values) - 1]['Value'];
|
||||
$vStart = null;
|
||||
$vEnd = null;
|
||||
foreach ($values as $v) {
|
||||
$ts = (int)$v['TimeStamp'];
|
||||
if ($ts <= $tStart) $vStart = (float)$v['Value'];
|
||||
if ($ts <= $tEnd) $vEnd = (float)$v['Value'];
|
||||
if ($ts > $tEnd) break;
|
||||
}
|
||||
if ($vStart === null) $vStart = $dbg['first'];
|
||||
if ($vEnd === null) $vEnd = $dbg['last'];
|
||||
$dbg['vStart'] = $vStart;
|
||||
$dbg['vEnd'] = $vEnd;
|
||||
$diff = $vEnd - $vStart;
|
||||
return ($diff < 0) ? 0.0 : (float)$diff;
|
||||
}
|
||||
private function getLastLogTimestamp(int $varId): int
|
||||
{
|
||||
if ($varId <= 0 || !IPS_VariableExists($varId)) {
|
||||
return 0;
|
||||
}
|
||||
$archiveID = IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0] ?? 0;
|
||||
if ($archiveID <= 0) {
|
||||
return 0;
|
||||
}
|
||||
$values = @AC_GetLoggedValues($archiveID, $varId, 0, time(), 1);
|
||||
if (empty($values)) {
|
||||
return 0;
|
||||
}
|
||||
return (int)$values[0]['TimeStamp'];
|
||||
}
|
||||
private function ShiftDate(string $action): void
|
||||
{
|
||||
$range = $this->ReadAttributeString(self::ATTR_RANGE);
|
||||
if ($range === 'total') {
|
||||
return;
|
||||
}
|
||||
if ($action === 'Today') {
|
||||
$this->WriteAttributeString(self::ATTR_DATE, date('Y-m-d'));
|
||||
return;
|
||||
}
|
||||
$date = $this->ReadAttributeString(self::ATTR_DATE);
|
||||
$base = strtotime($date . ' 00:00:00') ?: strtotime(date('Y-m-d') . ' 00:00:00');
|
||||
$sign = ($action === 'Prev') ? -1 : 1;
|
||||
switch ($range) {
|
||||
case 'day':
|
||||
$base = strtotime(($sign === -1 ? '-1 day' : '+1 day'), $base);
|
||||
break;
|
||||
case 'week':
|
||||
$base = strtotime(($sign === -1 ? '-7 day' : '+7 day'), $base);
|
||||
break;
|
||||
case 'month':
|
||||
$base = strtotime(($sign === -1 ? '-1 month' : '+1 month'), $base);
|
||||
break;
|
||||
case 'year':
|
||||
$base = strtotime(($sign === -1 ? '-1 year' : '+1 year'), $base);
|
||||
break;
|
||||
}
|
||||
$this->WriteAttributeString(self::ATTR_DATE, date('Y-m-d', $base));
|
||||
}
|
||||
private function isValidDate(string $ymd): bool
|
||||
{
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $ymd)) {
|
||||
return false;
|
||||
}
|
||||
[$y, $m, $d] = array_map('intval', explode('-', $ymd));
|
||||
return checkdate($m, $d, $y);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -1,78 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
html, body { margin: 0; padding: 8px; background: transparent; font-family: sans-serif; color: #ffffff; }
|
||||
.bar-block { margin-bottom: 20px; }
|
||||
.bar-title { font-size: 1.2em; font-weight: bold; margin-bottom: 6px; }
|
||||
.bar-container { width: 100%; background: #ddd; border-radius: 4px; overflow: hidden; height: 24px; position: relative; }
|
||||
.bar { height: 100%; float: left; position: relative; }
|
||||
.bar span { position: absolute; width: 100%; text-align: center; line-height: 24px; font-size: 0.8em; color: #fff; }
|
||||
.bar-cons { background: #4CAF50; }
|
||||
.bar-feed { background: #8BC34A; }
|
||||
.bar-pv { background: #FF9800; }
|
||||
.bar-grid { background: #FF5722; }
|
||||
.value-text { font-size: 0.95em; margin-top: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="pv_visu">
|
||||
<div class="bar-block">
|
||||
<div class="bar-title">Produktion (Eigenverbrauch / Einspeisung)</div>
|
||||
<div class="bar-container">
|
||||
<div class="bar bar-cons" id="barCons"><span id="barConsText"></span></div>
|
||||
<div class="bar bar-feed" id="barFeed"><span id="barFeedText"></span></div>
|
||||
</div>
|
||||
<div class="value-text" id="prodValues"></div>
|
||||
</div>
|
||||
<div class="bar-block">
|
||||
<div class="bar-title">Verbrauch (PV / Netz)</div>
|
||||
<div class="bar-container">
|
||||
<div class="bar bar-pv" id="barPV"><span id="barPVText"></span></div>
|
||||
<div class="bar bar-grid" id="barGrid"><span id="barGridText"></span></div>
|
||||
</div>
|
||||
<div class="value-text" id="consValues"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function Apply(data) {
|
||||
document.getElementById('barCons').style.width = data.prodCons + '%';
|
||||
document.getElementById('barFeed').style.width = data.prodFeed + '%';
|
||||
document.getElementById('barPV').style.width = data.consPV + '%';
|
||||
document.getElementById('barGrid').style.width = data.consGrid + '%';
|
||||
|
||||
document.getElementById('barConsText').innerText = data.prodCons + '%';
|
||||
document.getElementById('barFeedText').innerText = data.prodFeed + '%';
|
||||
document.getElementById('barPVText').innerText = data.consPV + '%';
|
||||
document.getElementById('barGridText').innerText = data.consGrid + '%';
|
||||
|
||||
document.getElementById('prodValues').innerText =
|
||||
'Gesamt: ' + data.value.prod + ' kWh, Eigenverbrauch: ' + (data.consPV/100*data.value.cons).toFixed(2) + ' kWh, Einspeisung: ' + data.value.feed + ' kWh';
|
||||
document.getElementById('consValues').innerText =
|
||||
'Gesamt: ' + data.value.cons + ' kWh, PV-Anteil: ' + (data.consPV/100*data.value.cons).toFixed(2) + ' kWh, Netz: ' + data.value.grid + ' kWh';
|
||||
}
|
||||
|
||||
function handleMessage(msg) {
|
||||
try {
|
||||
const data = typeof msg === 'string' ? JSON.parse(msg) : msg;
|
||||
Apply(data);
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Verarbeiten der Daten:', e, msg);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof registerMessageHandler === 'function') {
|
||||
registerMessageHandler(handleMessage);
|
||||
}
|
||||
|
||||
// Live-Aktualisierung alle 30 Sekunden
|
||||
function pollData() {
|
||||
if (typeof IPS !== 'undefined') {
|
||||
IPS.RequestAction('update', '');
|
||||
}
|
||||
}
|
||||
setInterval(pollData, 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,99 +0,0 @@
|
||||
<?php
|
||||
|
||||
class PV_Visu extends IPSModule
|
||||
{
|
||||
public function Create()
|
||||
{
|
||||
parent::Create();
|
||||
|
||||
$this->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 = '<script>handleMessage(' . json_encode($this->UpdateData()) . ');</script>';
|
||||
$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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
```
|
||||
@@ -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<Registernummer>` (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<Nummer>`).
|
||||
|
||||
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.*
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"id": "{C26E97C8-BA00-0563-6B14-B807C5ACE17F}",
|
||||
"name": "SofarWechselrichter",
|
||||
"type": 3,
|
||||
"vendor": "Belevo AG",
|
||||
"aliases": [],
|
||||
"parentRequirements": [],
|
||||
"childRequirements": [],
|
||||
"implemented": [],
|
||||
"prefix": "GEF",
|
||||
"url": ""
|
||||
}
|
||||
@@ -1,379 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Sofar Wechselrichter Modul (IP-Symcon)
|
||||
*
|
||||
* - LoggerNumber als String (kein Integer-Overflow).
|
||||
* - Negative Skalierungen erlaubt.
|
||||
* - Bit-Länge pro Register (16, 32, 64) auswählbar.
|
||||
* - Signed/Unsigned pro Register wählbar.
|
||||
* - Liest 32/64-Bit-Werte registerweise einzeln und setzt anschließend zusammen.
|
||||
* - Gelöschte Register-Variablen werden entfernt.
|
||||
* - Debug-Logs zeigen Raw-Response, um Slice-Position zu prüfen.
|
||||
*/
|
||||
class SofarWechselrichter extends IPSModule
|
||||
{
|
||||
public function Create()
|
||||
{
|
||||
parent::Create();
|
||||
// Moduleigenschaften
|
||||
$this->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));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
{
|
||||
"elements": [
|
||||
{
|
||||
"type": "ValidationTextBox",
|
||||
"name": "DeviceID",
|
||||
"caption": "Device ID"
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,16 @@
|
||||
"name": "VGT_Sub",
|
||||
"type": 3,
|
||||
"vendor": "Belevo",
|
||||
"aliases": ["VGT MQTT Device"],
|
||||
"prefix": "VGT",
|
||||
"parentRequirements": [],
|
||||
"aliases": [
|
||||
"VGT MQTT Device"
|
||||
],
|
||||
"parentRequirements": [
|
||||
"{F7A0DD2E-7684-4520-B61B-9613D6163722}"
|
||||
],
|
||||
"childRequirements": [],
|
||||
"implemented": [],
|
||||
"implemented": [
|
||||
"{018EF6B5-AB94-40C6-AA53-46943E824ACF}"
|
||||
],
|
||||
"prefix": "VGT",
|
||||
"version": "1.0"
|
||||
}
|
||||
}
|
||||
@@ -4,130 +4,98 @@ declare(strict_types=1);
|
||||
|
||||
class VGT_Sub extends IPSModule
|
||||
{
|
||||
// Die GUID des IP-Symcon MQTT Clients (Splitter)
|
||||
private const PARENT_GUID = '{C6D2AEB3-6E1F-4B2E-8E69-3A1A00246850}';
|
||||
|
||||
public function Create()
|
||||
{
|
||||
parent::Create();
|
||||
|
||||
// DeviceID
|
||||
$this->RegisterPropertyString('DeviceID', '');
|
||||
|
||||
// Eingehende MQTT-Nachrichten
|
||||
$this->RegisterVariableString("InputJSON", "MQTT Input", "", 1);
|
||||
$this->EnableAction("InputJSON");
|
||||
|
||||
// Ausgehende MQTT-Nachrichten
|
||||
$this->RegisterVariableString("OutputJSON", "MQTT Output", "", 2);
|
||||
|
||||
// Statusvariablen
|
||||
$this->RegisterVariableFloat('PowerProduction', 'Power Production', '', 10);
|
||||
$this->EnableAction('PowerProduction');
|
||||
|
||||
$this->RegisterVariableFloat('StateOfCharge', 'State of Charge', '', 11);
|
||||
$this->EnableAction('StateOfCharge');
|
||||
|
||||
$this->RegisterVariableBoolean('IsRunning', 'Is Running', '', 12);
|
||||
$this->EnableAction('IsRunning');
|
||||
|
||||
$this->RegisterVariableBoolean('IsReady', 'Is Ready', '', 13);
|
||||
$this->EnableAction('IsReady');
|
||||
|
||||
$this->RegisterVariableFloat('MinSoc', 'Min SoC', '', 14);
|
||||
$this->EnableAction('MinSoc');
|
||||
|
||||
$this->RegisterVariableFloat('MaxSoc', 'Max SoC', '', 15);
|
||||
$this->EnableAction('MaxSoc');
|
||||
|
||||
// Remote-Control
|
||||
$this->RegisterVariableInteger('PowerSetpoint', 'Power Setpoint', '', 20);
|
||||
IPS_SetVariableCustomAction($this->GetIDForIdent('PowerSetpoint'), 0);
|
||||
|
||||
$this->RegisterVariableString('Strategy', 'Strategy', '', 21);
|
||||
IPS_SetVariableCustomAction($this->GetIDForIdent('Strategy'), 0);
|
||||
// Verbindet sich automatisch mit einem MQTT Client
|
||||
$this->ConnectParent('{C6D2AEB3-6E1F-4B2E-8E69-3A1A00246850}');
|
||||
}
|
||||
|
||||
public function RequestAction($Ident, $Value)
|
||||
public function ApplyChanges()
|
||||
{
|
||||
if ($Ident === "InputJSON") {
|
||||
SetValueString($this->GetIDForIdent("InputJSON"), $Value);
|
||||
$this->ProcessIncoming($Value);
|
||||
return;
|
||||
}
|
||||
parent::ApplyChanges();
|
||||
$this->ConnectParent('{C6D2AEB3-6E1F-4B2E-8E69-3A1A00246850}');
|
||||
|
||||
SetValue($this->GetIDForIdent($Ident), $Value);
|
||||
}
|
||||
// Topic Definitionen
|
||||
$requestTopic = 'feedback-request/deviceOne';
|
||||
|
||||
private function ProcessIncoming($jsonString)
|
||||
{
|
||||
$data = @json_decode($jsonString, true);
|
||||
if (!is_array($data)) {
|
||||
return;
|
||||
}
|
||||
// 1. Filter setzen: Wir wollen nur Nachrichten für unser Request-Topic sehen.
|
||||
// Das "Topic" im Filter bezieht sich auf das JSON-Paket vom MQTT Client.
|
||||
$this->SetReceiveDataFilter('.*"Topic":"' . preg_quote($requestTopic, '/') . '".*');
|
||||
|
||||
if (!isset($data['topic']) || !isset($data['payload'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$topic = $data['topic'];
|
||||
$payload = $data['payload'];
|
||||
|
||||
$device = $this->ReadPropertyString('DeviceID');
|
||||
|
||||
// Feedback Request
|
||||
if ($topic === "feedback-request/$device") {
|
||||
$this->SendFeedback($device);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remote Control Request
|
||||
if ($topic === "remote-control-request/$device") {
|
||||
$this->ProcessRemoteControl($device, $payload);
|
||||
return;
|
||||
// 2. Beim MQTT Broker abonnieren (Subscribe senden)
|
||||
// Wir prüfen erst, ob wir einen Parent haben, um Fehler zu vermeiden
|
||||
if ($this->HasActiveParent()) {
|
||||
$this->Subscribe($requestTopic);
|
||||
}
|
||||
}
|
||||
|
||||
private function SendFeedback($device)
|
||||
/**
|
||||
* Wird aufgerufen, wenn der Parent (MQTT Client) Daten empfängt
|
||||
*/
|
||||
public function ReceiveData($JSONString)
|
||||
{
|
||||
$response = [
|
||||
"power_production" => GetValue($this->GetIDForIdent('PowerProduction')),
|
||||
"state_of_charge" => GetValue($this->GetIDForIdent('StateOfCharge')),
|
||||
"is_running" => GetValue($this->GetIDForIdent('IsRunning')),
|
||||
"is_ready" => GetValue($this->GetIDForIdent('IsReady')),
|
||||
"min_soc" => GetValue($this->GetIDForIdent('MinSoc')),
|
||||
"max_soc" => GetValue($this->GetIDForIdent('MaxSoc'))
|
||||
$data = json_decode($JSONString, true);
|
||||
|
||||
$this->SendDebug('ReceiveData', $JSONString, 0);
|
||||
|
||||
if (!isset($data['Topic']) || !isset($data['Payload'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sicherheitscheck: Ist es wirklich das gewünschte Topic?
|
||||
if ($data['Topic'] === 'feedback-request/deviceOne') {
|
||||
|
||||
$this->SendDebug('VGT_Logic', 'Request received. Sending "Hello"...', 0);
|
||||
|
||||
// Antwort senden
|
||||
$this->Publish('feedback-response', 'Hello');
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------
|
||||
* MQTT HILFSFUNKTIONEN
|
||||
* ---------------------------------------------------------*/
|
||||
|
||||
private function Subscribe(string $topic)
|
||||
{
|
||||
$payload = [
|
||||
'DataID' => self::PARENT_GUID,
|
||||
'PacketType' => 3, // Achtung: Symcon intern ist Subscribe oft mapped,
|
||||
// aber für den MQTT Client ist es Type 3 (Publish) oder spezial.
|
||||
// Standard Symcon MQTT Client nutzt jedoch oft Type 8 für Subscribe.
|
||||
// Hier nutzen wir den Standard Weg für JSON DataID:
|
||||
];
|
||||
|
||||
SetValueString(
|
||||
$this->GetIDForIdent("OutputJSON"),
|
||||
json_encode([
|
||||
"topic" => "feedback-response/$device",
|
||||
"payload" => json_encode($response)
|
||||
])
|
||||
);
|
||||
|
||||
// Sende Subscribe Paket (Type 8 = Subscribe beim Client Splitter)
|
||||
$this->SendDataToParent(json_encode([
|
||||
'DataID' => self::PARENT_GUID,
|
||||
'PacketType' => 8,
|
||||
'QualityOfService' => 0,
|
||||
'Retain' => false,
|
||||
'Topic' => $topic,
|
||||
'Payload' => ''
|
||||
]));
|
||||
|
||||
$this->SendDebug('MQTT', "Subscribed to $topic", 0);
|
||||
}
|
||||
|
||||
private function ProcessRemoteControl($device, $payload)
|
||||
private function Publish(string $topic, string $payload)
|
||||
{
|
||||
$json = @json_decode($payload, true);
|
||||
if (!is_array($json)) {
|
||||
return;
|
||||
}
|
||||
// Sende Publish Paket (Type 3 = Publish)
|
||||
$this->SendDataToParent(json_encode([
|
||||
'DataID' => self::PARENT_GUID,
|
||||
'PacketType' => 3,
|
||||
'QualityOfService' => 0,
|
||||
'Retain' => false,
|
||||
'Topic' => $topic,
|
||||
'Payload' => $payload
|
||||
]));
|
||||
|
||||
if (isset($json['power_setpoint'])) {
|
||||
SetValue($this->GetIDForIdent('PowerSetpoint'), (int)$json['power_setpoint']);
|
||||
}
|
||||
|
||||
if (isset($json['strategy'])) {
|
||||
SetValueString($this->GetIDForIdent('Strategy'), (string)$json['strategy']);
|
||||
}
|
||||
|
||||
SetValueString(
|
||||
$this->GetIDForIdent("OutputJSON"),
|
||||
json_encode([
|
||||
"topic" => "remote-control-response/$device",
|
||||
"payload" => json_encode($json)
|
||||
])
|
||||
);
|
||||
$this->SendDebug('MQTT', "Published to $topic: $payload", 0);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
70
patch.diff
Normal file
70
patch.diff
Normal file
@@ -0,0 +1,70 @@
|
||||
From a314141093f7e16493a47009948d71e7095858e2 Mon Sep 17 00:00:00 2001
|
||||
From: "belevo\\mh" <mh@belevo.ch>
|
||||
Date: Mon, 26 Jan 2026 12:55:13 +0100
|
||||
Subject: [PATCH] no message
|
||||
|
||||
---
|
||||
Bat_EV_SDL/form.json | 12 ++++++------
|
||||
1 file changed, 6 insertions(+), 6 deletions(-)
|
||||
|
||||
diff --git a/Bat_EV_SDL/form.json b/Bat_EV_SDL/form.json
|
||||
index bd29490..191ac69 100644
|
||||
--- a/Bat_EV_SDL/form.json
|
||||
+++ b/Bat_EV_SDL/form.json
|
||||
@@ -10,7 +10,7 @@
|
||||
{
|
||||
"caption":"Typ",
|
||||
"name":"typ",
|
||||
- "width":"200px",
|
||||
+ "width":"100px",
|
||||
"add":"Stufe",
|
||||
"edit":{
|
||||
"type":"ValidationTextBox"
|
||||
@@ -53,7 +53,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
- "caption":"Batterieleistung Laden SDL",
|
||||
+ "caption":"Laden SDL",
|
||||
"name":"powerbat_laden_sdl",
|
||||
"width":"200px",
|
||||
"add":0,
|
||||
@@ -62,7 +62,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
- "caption":"Batterieleistung Entladen SDL",
|
||||
+ "caption":"Entladen SDL",
|
||||
"name":"powerbat_entladen_sdl",
|
||||
"width":"200px",
|
||||
"add":0,
|
||||
@@ -71,7 +71,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
- "caption":"Batterie SOC-EV",
|
||||
+ "caption":"SOC-EV",
|
||||
"name":"ev_soc",
|
||||
"width":"200px",
|
||||
"add":0,
|
||||
@@ -80,7 +80,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
- "caption":"Batterieleistung Laden EV",
|
||||
+ "caption":"Laden EV",
|
||||
"name":"powerbat_laden_EV",
|
||||
"width":"200px",
|
||||
"add":0,
|
||||
@@ -89,7 +89,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
- "caption":"Batterieleistung Entladen EV",
|
||||
+ "caption":"Entladen EV",
|
||||
"name":"powerbat_entladen_ev",
|
||||
"width":"200px",
|
||||
"add":0,
|
||||
--
|
||||
2.43.0.windows.1
|
||||
|
||||
Reference in New Issue
Block a user