Compare commits

...

245 Commits

Author SHA1 Message Date
belevo\mh
2178860a51 no message 2026-04-02 07:18:06 +02:00
belevo\mh
cfcf6ba43c no message 2026-04-02 07:15:10 +02:00
11d72eca10 no message 2026-03-19 17:16:50 +01:00
99aba58f34 no message 2026-03-19 17:13:13 +01:00
584d69b471 no message 2026-03-19 17:06:34 +01:00
7850533911 no message 2026-03-19 17:01:33 +01:00
eb90029ad6 no message 2026-03-19 16:54:23 +01:00
724f3b0d2a no message 2026-03-19 16:44:55 +01:00
belevo\mh
a57a76972d no message 2026-03-10 10:00:40 +01:00
belevo\mh
91e4709612 no message 2026-03-10 10:00:02 +01:00
belevo\mh
944817950a no message 2026-03-10 09:54:34 +01:00
88b6c60890 no message 2026-03-05 14:47:15 +01:00
belevo\mh
6757682399 no message 2026-03-03 11:29:51 +01:00
belevo\mh
f11255b071 no message 2026-02-11 16:10:28 +01:00
belevo\mh
1e23489346 no message 2026-02-11 16:07:31 +01:00
belevo\mh
4f7ecf1aae no message 2026-02-11 15:57:36 +01:00
daff7a81e5 no message 2026-02-10 15:27:04 +01:00
f35c43bacd no message 2026-02-10 15:23:19 +01:00
912e3ad3df no message 2026-02-10 15:21:37 +01:00
851dc7d70e no message 2026-02-10 15:09:06 +01:00
ccb068dd5d no message 2026-02-10 14:57:13 +01:00
57a4594e38 no message 2026-02-10 14:02:55 +01:00
6094694809 no message 2026-02-10 13:59:34 +01:00
c9426c1bfc no message 2026-02-10 13:52:39 +01:00
17fe281fa3 no message 2026-02-10 13:51:09 +01:00
1ee6b409aa no message 2026-02-10 13:43:11 +01:00
8dd82f1abd no message 2026-02-10 13:38:31 +01:00
015b1fc441 no message 2026-02-10 13:31:39 +01:00
e21f760e13 no message 2026-02-10 13:21:43 +01:00
5aa44ad8a2 no message 2026-02-10 11:56:21 +01:00
belevo\mh
8f051aced5 no message 2026-02-09 10:22:52 +01:00
belevo\mh
e6cd727fc1 no message 2026-02-09 10:06:13 +01:00
belevo\mh
494984734b no message 2026-02-09 09:52:22 +01:00
belevo\mh
47beb18f0b no message 2026-02-09 09:48:01 +01:00
belevo\mh
41c53c7a8f no message 2026-02-09 09:46:05 +01:00
belevo\mh
f887dba1e8 no message 2026-02-09 09:31:25 +01:00
belevo\mh
7db0667bd0 no message 2026-02-09 09:19:46 +01:00
belevo\mh
e22ebf328a no message 2026-02-09 09:16:21 +01:00
belevo\mh
e12c192e2d no message 2026-02-09 09:00:49 +01:00
belevo\mh
f89ff7f200 no message 2026-02-09 08:44:44 +01:00
belevo\mh
7314b81cc6 no message 2026-02-09 08:13:28 +01:00
belevo\mh
c6245d870e no message 2026-02-09 08:08:49 +01:00
belevo\mh
20bafaf68f no message 2026-02-09 08:06:17 +01:00
936a7ab46e no message 2026-02-05 17:27:33 +01:00
1d099588d2 no message 2026-02-05 17:24:08 +01:00
a902604003 no message 2026-02-05 17:21:00 +01:00
1e7efaef6e no message 2026-02-05 17:16:52 +01:00
d6a5d2c1f2 no message 2026-02-05 17:12:25 +01:00
875c40e179 no message 2026-02-05 17:09:20 +01:00
4a3fb06b21 no message 2026-02-05 17:00:42 +01:00
92e4393b12 no message 2026-02-05 16:59:06 +01:00
belevo\mh
b8783ebba4 no message 2026-02-05 13:30:45 +01:00
belevo\mh
658b9626b4 no message 2026-02-05 13:21:28 +01:00
belevo\mh
b5973a5878 no message 2026-02-05 13:10:46 +01:00
belevo\mh
716538c758 no message 2026-02-05 10:09:30 +01:00
belevo\mh
11be77f8dd no message 2026-02-05 09:53:24 +01:00
belevo\mh
afc72486b9 no message 2026-01-28 07:12:59 +01:00
belevo\mh
76464b66cf no message 2026-01-27 15:15:48 +01:00
belevo\mh
a5ea4c75ce no message 2026-01-27 14:01:23 +01:00
belevo\mh
8c5a9c5e61 no message 2026-01-27 13:24:58 +01:00
belevo\mh
9bbbad260a no message 2026-01-27 12:47:44 +01:00
belevo\mh
09962a8cd0 no message 2026-01-27 10:18:28 +01:00
belevo\mh
ab9eec4249 no message 2026-01-27 10:01:21 +01:00
belevo\mh
c5ec334421 no message 2026-01-27 09:47:45 +01:00
belevo\mh
1e0b314f2a no message 2026-01-27 09:00:58 +01:00
belevo\mh
979fa1a44f no message 2026-01-27 08:59:49 +01:00
belevo\mh
b779f4ce42 no message 2026-01-27 08:57:08 +01:00
belevo\mh
2847607983 no message 2026-01-27 08:51:03 +01:00
belevo\mh
cf126855ba no message 2026-01-27 08:49:52 +01:00
belevo\mh
c6a8c32a65 no message 2026-01-27 08:48:24 +01:00
belevo\mh
78668405bc no message 2026-01-27 08:46:34 +01:00
belevo\mh
cdc7d762d8 no message 2026-01-27 07:45:28 +01:00
belevo\mh
6e9835d852 no message 2026-01-27 07:42:49 +01:00
belevo\mh
9e6ffb3c01 no message 2026-01-26 15:25:54 +01:00
belevo\mh
4648c5e404 no message 2026-01-26 15:05:45 +01:00
belevo\mh
0b9d53fff9 Revert "no message"
This reverts commit a314141093.
2026-01-26 14:59:04 +01:00
belevo\mh
9f22ae0aa7 no message 2026-01-26 14:56:00 +01:00
belevo\mh
ef49957d7f no message 2026-01-26 14:51:42 +01:00
belevo\mh
8148d62484 no message 2026-01-26 14:10:12 +01:00
belevo\mh
2eab6b60ec no message 2026-01-26 14:04:35 +01:00
belevo\mh
a314141093 no message 2026-01-26 12:55:13 +01:00
belevo\mh
fc411e0d5f no message 2026-01-26 08:18:21 +01:00
belevo\mh
d23b317093 no message 2026-01-26 07:23:43 +01:00
belevo\mh
ff9fc91aae no message 2026-01-23 13:40:16 +01:00
belevo\mh
5b57f2637a no message 2026-01-23 13:38:45 +01:00
belevo\mh
010f6c5785 no message 2026-01-23 11:30:22 +01:00
belevo\mh
6cffe2715f no message 2026-01-23 10:41:52 +01:00
belevo\mh
b79e0d1430 no message 2026-01-23 10:31:11 +01:00
belevo\mh
adf6b573de no message 2026-01-23 10:27:37 +01:00
belevo\mh
20228fbcf1 no message 2026-01-23 10:23:15 +01:00
belevo\mh
9319e3a2f8 no message 2026-01-23 10:13:17 +01:00
belevo\mh
391184a26e no message 2026-01-23 09:57:21 +01:00
belevo\mh
4afd1cf6fa no message 2026-01-23 09:54:01 +01:00
belevo\mh
f235142561 no message 2026-01-23 09:48:14 +01:00
belevo\mh
e06333be0b no message 2026-01-23 09:43:40 +01:00
belevo\mh
31d950f6d2 no message 2026-01-23 09:39:52 +01:00
belevo\mh
3390330a82 no message 2026-01-22 18:40:22 +01:00
belevo\mh
225e254366 no message 2026-01-22 18:35:55 +01:00
belevo\mh
d94837f597 no message 2026-01-22 18:28:44 +01:00
belevo\mh
e859ead314 no message 2026-01-22 18:06:19 +01:00
belevo\mh
ba9c92c4ac no message 2026-01-22 17:53:33 +01:00
belevo\mh
b0929f77a9 no message 2026-01-22 17:48:29 +01:00
belevo\mh
2c86a7c181 no message 2026-01-22 17:21:08 +01:00
belevo\mh
18cbf73cd0 no message 2026-01-22 16:57:48 +01:00
belevo\mh
83bf7c5907 no message 2026-01-22 16:49:48 +01:00
belevo\mh
5df7c0c305 no message 2026-01-22 16:38:00 +01:00
belevo\mh
f80ebc8367 no message 2026-01-22 16:31:34 +01:00
belevo\mh
519e845eeb no message 2026-01-22 16:24:36 +01:00
belevo\mh
230e8bd890 no message 2026-01-22 14:55:18 +01:00
belevo\mh
a61ca6cff4 no message 2026-01-22 14:50:51 +01:00
belevo\mh
eed1e585d6 no message 2026-01-22 14:48:36 +01:00
belevo\mh
0e9ad10626 no message 2026-01-22 14:40:06 +01:00
belevo\mh
9ed5471d6a no message 2026-01-22 14:26:25 +01:00
belevo\mh
234482bd8d no message 2026-01-22 14:17:03 +01:00
belevo\mh
1a07130db2 no message 2026-01-22 14:15:59 +01:00
belevo\mh
ba2cdaede8 no message 2026-01-22 14:13:59 +01:00
belevo\mh
4535be0f2c no message 2026-01-22 14:11:53 +01:00
belevo\mh
41b4a85409 no message 2026-01-21 08:29:49 +01:00
belevo\mh
1207792810 no message 2026-01-21 08:08:07 +01:00
belevo\mh
63912b3606 no message 2026-01-21 07:36:23 +01:00
belevo\mh
3be124e81f no message 2026-01-20 11:55:54 +01:00
belevo\mh
422ea69b4f no message 2026-01-20 11:52:24 +01:00
belevo\mh
0a6ce80266 no message 2026-01-20 11:46:06 +01:00
belevo\mh
b83d9459eb no message 2026-01-20 11:21:13 +01:00
belevo\mh
a25f440097 no message 2026-01-20 11:09:04 +01:00
belevo\mh
b241cc3f77 no message 2026-01-20 11:06:37 +01:00
belevo\mh
cfb196f346 no message 2026-01-20 10:44:32 +01:00
belevo\mh
83c74ad05d no message 2026-01-20 10:34:26 +01:00
belevo\mh
27536d3d16 no message 2026-01-20 10:25:14 +01:00
belevo\mh
33667256ec no message 2026-01-20 10:18:51 +01:00
belevo\mh
5db059fc51 no message 2026-01-20 10:05:08 +01:00
belevo\mh
2a605a6138 no message 2026-01-20 09:50:47 +01:00
belevo\mh
bf79f30e76 no message 2026-01-20 09:29:17 +01:00
belevo\mh
ce57b11224 no message 2026-01-20 09:06:08 +01:00
belevo\mh
4711fb0208 no message 2026-01-20 08:41:06 +01:00
belevo\mh
2f68634145 no message 2025-12-23 09:57:19 +01:00
belevo\mh
f21acfe6a6 no message 2025-12-22 13:56:13 +01:00
belevo\mh
110120f398 no message 2025-12-22 08:27:05 +01:00
belevo\mh
ba8e3af69b no message 2025-12-18 13:43:44 +01:00
belevo\mh
b1d8dbeeaf no message 2025-12-18 13:41:00 +01:00
belevo\mh
9bb2b90ce0 no message 2025-12-18 13:36:22 +01:00
belevo\mh
aa01389bf3 no message 2025-12-18 13:34:05 +01:00
belevo\mh
a86c8dba66 no message 2025-12-18 11:40:52 +01:00
belevo\mh
7ac31ef2a2 no message 2025-12-18 11:15:11 +01:00
belevo\mh
bbb8ee8bfe no message 2025-12-18 11:04:33 +01:00
belevo\mh
692bf28e0c no message 2025-12-18 10:58:01 +01:00
belevo\mh
322c4acbb5 no message 2025-12-18 10:56:36 +01:00
belevo\mh
d345d26b1e no message 2025-12-18 10:54:40 +01:00
belevo\mh
f5d6ad2a72 no message 2025-12-18 10:51:21 +01:00
belevo\mh
3f86c5583a no message 2025-12-18 10:41:12 +01:00
belevo\mh
bb16399d7d no message 2025-12-18 10:22:34 +01:00
belevo\mh
05f4b8b32d no message 2025-12-18 10:13:07 +01:00
belevo\mh
09f1c52f87 no message 2025-12-18 10:05:26 +01:00
belevo\mh
6cb64b47fa no message 2025-12-18 09:48:45 +01:00
belevo\mh
13ff15a0d9 no message 2025-12-18 09:21:12 +01:00
belevo\mh
1699ad4e7c no message 2025-12-18 09:02:57 +01:00
belevo\mh
ff747d1f04 no message 2025-12-18 08:54:15 +01:00
belevo\mh
3a18f52492 no message 2025-12-18 08:39:42 +01:00
belevo\mh
80e35c372e no message 2025-12-18 08:33:45 +01:00
belevo\mh
d1f0b876aa no message 2025-12-18 08:32:15 +01:00
belevo\mh
feb7301fc1 no message 2025-12-18 08:31:04 +01:00
belevo\mh
f4e87772e1 no message 2025-12-18 08:28:17 +01:00
belevo\mh
d2bae877a4 no message 2025-12-18 08:23:16 +01:00
belevo\mh
90701ec350 no message 2025-12-18 08:16:40 +01:00
belevo\mh
b7ef02eb86 no message 2025-12-18 07:29:23 +01:00
belevo\mh
ba99c8f2e3 no message 2025-12-18 07:18:41 +01:00
belevo\mh
fdc23168b3 no message 2025-12-18 06:56:39 +01:00
belevo\mh
4335c37d9f no message 2025-12-18 06:46:24 +01:00
belevo\mh
13f458e10c no message 2025-12-17 15:55:57 +01:00
belevo\mh
1627f81b9e no message 2025-12-17 15:53:29 +01:00
belevo\mh
03e02558d5 no message 2025-12-17 15:42:51 +01:00
belevo\mh
94778e2209 no message 2025-12-17 15:34:09 +01:00
belevo\mh
e99c5c0902 no message 2025-12-17 15:32:48 +01:00
belevo\mh
c8d5bb3ab4 no message 2025-12-17 15:06:34 +01:00
belevo\mh
499eddccb9 no message 2025-12-17 14:17:23 +01:00
belevo\mh
26ec00c63e no message 2025-12-17 14:12:50 +01:00
belevo\mh
aafd768e94 no message 2025-12-17 13:54:59 +01:00
belevo\mh
78be02d507 no message 2025-12-17 13:47:41 +01:00
belevo\mh
78299cee43 no message 2025-12-17 13:31:48 +01:00
belevo\mh
e31ad76616 no message 2025-12-17 13:24:01 +01:00
belevo\mh
2c01458065 no message 2025-12-17 11:47:17 +01:00
belevo\mh
3651502d6e no message 2025-12-17 11:01:27 +01:00
belevo\mh
f0de973f26 no message 2025-12-17 10:44:59 +01:00
belevo\mh
394b797e84 no message 2025-12-17 10:19:41 +01:00
belevo\mh
9f18caf7d0 no message 2025-12-17 10:11:38 +01:00
belevo\mh
883b3815be no message 2025-12-17 10:01:23 +01:00
belevo\mh
a36c575ad7 no message 2025-12-17 09:58:28 +01:00
belevo\mh
a6a758b332 no message 2025-12-17 09:54:42 +01:00
belevo\mh
cc3cb5fd38 no message 2025-12-17 09:49:27 +01:00
belevo\mh
ca74e3d946 no message 2025-12-17 09:29:16 +01:00
belevo\mh
7f5a6bcade no message 2025-12-17 08:57:27 +01:00
belevo\mh
65d2be480f no message 2025-12-17 08:42:17 +01:00
belevo\mh
38f450b00e no message 2025-12-17 08:36:43 +01:00
belevo\mh
26fd4a4159 no message 2025-12-17 08:34:38 +01:00
belevo\mh
25630326c7 no message 2025-12-17 08:23:24 +01:00
belevo\mh
b6a1625433 no message 2025-12-17 08:18:44 +01:00
belevo\mh
195209a6da no message 2025-12-17 08:06:52 +01:00
belevo\mh
06a8327619 no message 2025-12-17 08:05:02 +01:00
belevo\mh
5f127b9ede no message 2025-12-17 08:04:01 +01:00
belevo\mh
284ed6ff3b no message 2025-12-17 07:57:55 +01:00
belevo\mh
b28d9b94ce no message 2025-12-17 07:50:58 +01:00
belevo\mh
da24c80b6c no message 2025-12-17 07:28:59 +01:00
belevo\mh
2c37115071 no message 2025-12-17 07:19:43 +01:00
belevo\mh
e7a95c36da no message 2025-12-17 07:17:04 +01:00
belevo\mh
58421f7fbd no message 2025-12-17 07:13:02 +01:00
belevo\mh
800d56907c no message 2025-12-17 07:10:55 +01:00
belevo\mh
2ca3b5b83d no message 2025-12-17 07:09:20 +01:00
belevo\mh
b55e8c67af no message 2025-12-17 07:05:42 +01:00
9f9207f17f no message 2025-12-12 11:17:51 +01:00
40b995d5e3 no message 2025-12-12 10:17:12 +01:00
31a9f0afe7 no message 2025-12-12 10:15:24 +01:00
14ff6781c2 no message 2025-12-12 10:13:53 +01:00
a514a45a3b no message 2025-12-12 10:08:23 +01:00
9b1008d7f5 no message 2025-12-12 09:59:22 +01:00
459c115a7d no message 2025-12-12 09:57:54 +01:00
1b20b2e637 sdf 2025-12-12 09:42:23 +01:00
422ea912b0 no message 2025-12-12 09:26:01 +01:00
4e6f5f6fb7 no message 2025-12-12 09:01:13 +01:00
6650164acc no message 2025-12-12 08:54:06 +01:00
b68db49f72 no message 2025-12-12 08:47:18 +01:00
558fd9aa4e no message 2025-12-12 08:14:12 +01:00
b4de095a20 no message 2025-12-12 07:49:48 +01:00
cd9209f800 no message 2025-12-11 14:23:26 +01:00
47bf006229 no message 2025-12-11 14:13:08 +01:00
7a4af1a67f no message 2025-12-11 14:10:21 +01:00
f3e7d81fb5 no message 2025-12-11 14:07:26 +01:00
7daa8b7671 no message 2025-12-11 11:16:37 +01:00
7680feda8d Absatz reingemacht 2025-12-11 10:49:43 +01:00
b46b09ba29 no message 2025-12-05 15:04:05 +01:00
3098547209 no message 2025-12-05 14:56:41 +01:00
3180ac7922 no message 2025-12-05 14:50:01 +01:00
9d628b7a3f no message 2025-12-05 14:31:52 +01:00
17ca7c4c63 no message 2025-12-05 14:29:20 +01:00
3a0eb6113e no message 2025-12-05 14:28:10 +01:00
62e397db10 no message 2025-12-05 14:26:48 +01:00
376dcc121e no message 2025-12-05 14:20:08 +01:00
391f5716e1 no message 2025-12-05 14:17:33 +01:00
c0cc49261b no message 2025-12-05 14:14:48 +01:00
37f5c3bede no message 2025-12-05 14:09:12 +01:00
f5ddc77d36 no message 2025-12-05 13:51:34 +01:00
920bd27e9e no message 2025-12-05 13:46:03 +01:00
51e038400b no message 2025-12-05 13:27:31 +01:00
353b8afb3d no message 2025-12-05 11:59:01 +01:00
1d4914dbf1 no message 2025-12-05 11:44:30 +01:00
4213aa4804 no message 2025-12-05 11:39:56 +01:00
27 changed files with 2531 additions and 2053 deletions

View File

@@ -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
View 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
View 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
}
]
}

View File

@@ -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
View 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);
}
}
?>

View File

@@ -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": ""
},
{

View File

@@ -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");

View File

@@ -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"
}
]
}

View File

@@ -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
View 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);`

View File

@@ -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
View 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>

View File

@@ -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
View 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);
}
}
?>

View File

@@ -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>

View File

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

View File

@@ -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);
```

View File

@@ -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**
| RegisterNummer | 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 `RegisterNummer` ein, gib als Skalierungsfaktor `1` (oder `0.1` etc.) an.
Der absolute Rohwert wird stets als UINT16 interpretiert (065535).
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.*

View File

@@ -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": []
}

View File

@@ -1,12 +0,0 @@
{
"id": "{C26E97C8-BA00-0563-6B14-B807C5ACE17F}",
"name": "SofarWechselrichter",
"type": 3,
"vendor": "Belevo AG",
"aliases": [],
"parentRequirements": [],
"childRequirements": [],
"implemented": [],
"prefix": "GEF",
"url": ""
}

View File

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

View File

@@ -1,9 +1,5 @@
{
"elements": [
{
"type": "ValidationTextBox",
"name": "DeviceID",
"caption": "Device ID"
}
]
}
}

View File

@@ -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"
}
}

View File

@@ -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
View 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