Compare commits
657 Commits
master
...
2024.01.16
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c66965ced | ||
|
|
63205f88be | ||
|
|
b8fcdf3998 | ||
|
|
e136e096f4 | ||
|
|
ffd189c1f5 | ||
|
|
a012d81427 | ||
|
|
2806b6db86 | ||
|
|
642f38ce51 | ||
|
|
44d207a5f5 | ||
|
|
dd5d5ce9fd | ||
|
|
d0e1da7b1d | ||
|
|
d6d274f078 | ||
|
|
ec93004724 | ||
|
|
210fce67ce | ||
|
|
92de3e9f87 | ||
|
|
67e2134f7e | ||
|
|
d5155a07be | ||
|
|
bfcce16bc9 | ||
|
|
6573c51052 | ||
|
|
24c8a40fad | ||
|
|
f968179b60 | ||
|
|
f00cd1bd61 | ||
|
|
88d75673fc | ||
|
|
a65f1e48a5 | ||
|
|
2a15677923 | ||
|
|
2ed66eb992 | ||
|
|
377406f10c | ||
|
|
e09ffcbb53 | ||
|
|
85d0f2a8fb | ||
|
|
9ed5a78818 | ||
|
|
bc38ce344f | ||
|
|
3c8b8d4427 | ||
|
|
d5eba2392c | ||
|
|
f5c69060f5 | ||
|
|
aa5a762d2a | ||
|
|
e9def28f3e | ||
|
|
e7a005839b | ||
|
|
65319ed07e | ||
|
|
4548fc2a00 | ||
|
|
7946dfc0c2 | ||
|
|
2c1e145575 | ||
|
|
bbf0003d1c | ||
|
|
5bd3ce5a8f | ||
|
|
cbf7680836 | ||
|
|
8f5c4878c5 | ||
|
|
ef1aec3b26 | ||
|
|
463226082f | ||
|
|
fe2f82e303 | ||
|
|
08bc181a5e | ||
|
|
7928f2f8cf | ||
|
|
bb34fa74fd | ||
|
|
c7098b6c42 | ||
|
|
f89f9da67a | ||
|
|
d769cdd30a | ||
|
|
3bf4d35db8 | ||
|
|
6ab706c87d | ||
|
|
367e0f9b6e | ||
|
|
d494810975 | ||
|
|
9ff39fde26 | ||
|
|
fb2ca28692 | ||
|
|
6e78c5bd1c | ||
|
|
0c829a5947 | ||
|
|
dd8446df0a | ||
|
|
e44328bb13 | ||
|
|
0085970567 | ||
|
|
a1d0dad128 | ||
|
|
8cccce5875 | ||
|
|
6f7470feb4 | ||
|
|
28f46471bf | ||
|
|
a004e94e57 | ||
|
|
6e336f1117 | ||
|
|
7637dc591b | ||
|
|
3dceddfe49 | ||
|
|
d4d42167ec | ||
|
|
3af0437857 | ||
|
|
eb578f08c5 | ||
|
|
49a10305e2 | ||
|
|
9ca3655de6 | ||
|
|
254b95cb0f | ||
|
|
0fa2745ace | ||
|
|
8ba9048f99 | ||
|
|
c5427dedce | ||
|
|
b159c31258 | ||
|
|
b833d5ab52 | ||
|
|
4d4aadf8de | ||
|
|
d23b991f5c | ||
|
|
917242909b | ||
|
|
5bbbe6bfcb | ||
|
|
b461aff622 | ||
|
|
116da05114 | ||
|
|
4324ae3081 | ||
|
|
f7abbdbe06 | ||
|
|
7fb26e1e81 | ||
|
|
b1164d6c69 | ||
|
|
f0a55ea32b | ||
|
|
4ee49a6ecb | ||
|
|
933345d659 | ||
|
|
e937fd1cb8 | ||
|
|
7142921021 | ||
|
|
160d3f23bd | ||
|
|
ae392329ea | ||
|
|
b081845b95 | ||
|
|
ed2a189a61 | ||
|
|
f2893220a5 | ||
|
|
4d0f958943 | ||
|
|
9d6b459dc6 | ||
|
|
b501d25ab6 | ||
|
|
954a98dbc8 | ||
|
|
2f9539e4b3 | ||
|
|
f7bd4a40d8 | ||
|
|
4e489febfe | ||
|
|
24018a1432 | ||
|
|
88a5117007 | ||
|
|
2eeb5f1d19 | ||
|
|
a7ea15cde9 | ||
|
|
eb1c2dbd8c | ||
|
|
0cb42a6424 | ||
|
|
d984912d7c | ||
|
|
8dd96c450b | ||
|
|
3df47d1fee | ||
|
|
68783b450f | ||
|
|
ba303da742 | ||
|
|
a7a38e74a1 | ||
|
|
8c36532cea | ||
|
|
f744629b0b | ||
|
|
2ba7ea2744 | ||
|
|
d5308b1029 | ||
|
|
929b477275 | ||
|
|
d4afc5940a | ||
|
|
88744bfa38 | ||
|
|
96ee78156d | ||
|
|
ca308d0895 | ||
|
|
84647d80e2 | ||
|
|
026bca9fe3 | ||
|
|
be2846a07a | ||
|
|
487304125c | ||
|
|
7c7a15e016 | ||
|
|
b7214161b8 | ||
|
|
1100f10c99 | ||
|
|
8e4f234517 | ||
|
|
6601e9a44b | ||
|
|
801fe5d027 | ||
|
|
c8a561dbd7 | ||
|
|
cc3ab9b14b | ||
|
|
3702fb3eef | ||
|
|
bbc1319090 | ||
|
|
9475a78211 | ||
|
|
5a72d74982 | ||
|
|
40c720aa57 | ||
|
|
c06299878b | ||
|
|
940d1a6145 | ||
|
|
401a3b86a8 | ||
|
|
21bbed9b8e | ||
|
|
ec67fe1ff7 | ||
|
|
3db237c109 | ||
|
|
c950eb7245 | ||
|
|
3cb30b14cd | ||
|
|
e6eaa001e7 | ||
|
|
aff7924411 | ||
|
|
5335ec1bde | ||
|
|
69456affce | ||
|
|
81864b3420 | ||
|
|
587b2dc553 | ||
|
|
2bce8311a7 | ||
|
|
6b425d96b0 | ||
|
|
9bc334e368 | ||
|
|
43e836ac41 | ||
|
|
2440028d38 | ||
|
|
18c464e524 | ||
|
|
8b01fa07cc | ||
|
|
e06740fbb8 | ||
|
|
cea1f94b5e | ||
|
|
f0def2ae89 | ||
|
|
a7da000345 | ||
|
|
0dd1566dc6 | ||
|
|
c393e52185 | ||
|
|
cbc99d715f | ||
|
|
f68f68be77 | ||
|
|
633ef88296 | ||
|
|
f3297930b5 | ||
|
|
95d7ac7adf | ||
|
|
1d559c1c40 | ||
|
|
475aab1e9a | ||
|
|
cdf5c85510 | ||
|
|
23ff4ef22a | ||
|
|
2a858e096b | ||
|
|
3fb062b5cc | ||
|
|
ffa9be0835 | ||
|
|
344498d440 | ||
|
|
d3adc65d11 | ||
|
|
107182f948 | ||
|
|
e457ab73f9 | ||
|
|
006f63ed02 | ||
|
|
9a4eb75160 | ||
|
|
1f39ed7b9b | ||
|
|
afd8790c3c | ||
|
|
99876d7c6b | ||
|
|
97f58eeba7 | ||
|
|
097d464bbb | ||
|
|
e279ce08c4 | ||
|
|
9aeb1583b5 | ||
|
|
461fce8ff4 | ||
|
|
9bab740c43 | ||
|
|
b2d58af5e8 | ||
|
|
71079fa0cc | ||
|
|
2970e84193 | ||
|
|
18b1076660 | ||
|
|
0b0bcf1dfb | ||
|
|
fd208cf6bb | ||
|
|
8b23324693 | ||
|
|
8433820529 | ||
|
|
0caffe9411 | ||
|
|
6ffb5bb897 | ||
|
|
2223afac62 | ||
|
|
acbf80d196 | ||
|
|
6ed3ce7417 | ||
|
|
d6163ddb7d | ||
|
|
07bb0b03f7 | ||
|
|
7d73ae3c20 | ||
|
|
4d05035661 | ||
|
|
9995c1172e | ||
|
|
016e30ec00 | ||
|
|
cd1db49a98 | ||
|
|
f018a0136e | ||
|
|
fd58ad2003 | ||
|
|
080a3eb29e | ||
|
|
da7628dafc | ||
|
|
ceb28030a4 | ||
|
|
ded5ceec58 | ||
|
|
86ee7e1a64 | ||
|
|
8dac88e7b9 | ||
|
|
0fd7b75e83 | ||
|
|
51a21de189 | ||
|
|
e68baa3086 | ||
|
|
e0a8da84d7 | ||
|
|
a6f5e8a3a2 | ||
|
|
8298a0c36f | ||
|
|
c727e21a49 | ||
|
|
91ce844b36 | ||
|
|
e7c8a89bd3 | ||
|
|
2e811b7ab1 | ||
|
|
52af52eb3a | ||
|
|
a3e7439181 | ||
|
|
9f511fb985 | ||
|
|
8b64671151 | ||
|
|
13b318690d | ||
|
|
47f81f2579 | ||
|
|
3a991708d0 | ||
|
|
b7ac70b1ca | ||
|
|
1c7b7d3cdf | ||
|
|
556f3e0abf | ||
|
|
40a65198fe | ||
|
|
065c169b20 | ||
|
|
178d40d5b4 | ||
|
|
569edbe69e | ||
|
|
33423dfc2f | ||
|
|
6fe19f00a7 | ||
|
|
d1885b6177 | ||
|
|
8021052cfd | ||
|
|
e5af5be70a | ||
|
|
db4125ae7a | ||
|
|
dae4c6fbf5 | ||
|
|
2d14d9f69e | ||
|
|
3f0291dce0 | ||
|
|
5d4c6866da | ||
|
|
c621f2d3e3 | ||
|
|
80f1af32f8 | ||
|
|
46c733ca31 | ||
|
|
32913c2b2e | ||
|
|
dfa0a1c98b | ||
|
|
cc32e3973e | ||
|
|
23b35d9b00 | ||
|
|
4abc89d43c | ||
|
|
70060559da | ||
|
|
6b437b5ea1 | ||
|
|
9ff9a8a6d6 | ||
|
|
00490b80af | ||
|
|
89209b6bf7 | ||
|
|
83b42a36b9 | ||
|
|
e65b2196bf | ||
|
|
d4a5570806 | ||
|
|
c337df605c | ||
|
|
e91935ab38 | ||
|
|
37b5edb010 | ||
|
|
71dda4b89c | ||
|
|
c7505eaae6 | ||
|
|
0a0488f73a | ||
|
|
a8554f97b0 | ||
|
|
322f532ac0 | ||
|
|
8f386c8611 | ||
|
|
e2a6468304 | ||
|
|
8764809259 | ||
|
|
3861ab89f1 | ||
|
|
75a59f5d1d | ||
|
|
8c9afbcdc0 | ||
|
|
422ad3f909 | ||
|
|
71da704d38 | ||
|
|
1e30323915 | ||
|
|
38b990fbbc | ||
|
|
41cc5ff4f9 | ||
|
|
84a6ed540d | ||
|
|
943a0eb58e | ||
|
|
6a9585cd1e | ||
|
|
545a18db6f | ||
|
|
a1b3bdfee8 | ||
|
|
8cc548d13e | ||
|
|
b8e06bf824 | ||
|
|
1e16eca16e | ||
|
|
40cee1f9ca | ||
|
|
3504924bb0 | ||
|
|
82699b1c88 | ||
|
|
4ddaa7643b | ||
|
|
0d0a624fe2 | ||
|
|
a306bc1351 | ||
|
|
b00ca02aac | ||
|
|
1a0f44dac4 | ||
|
|
1dc73f91ee | ||
|
|
3c5082287e | ||
|
|
690025e5fd | ||
|
|
be7a43fbfb | ||
|
|
01849dc90a | ||
|
|
ee376827fd | ||
|
|
9999fa28e8 | ||
|
|
ee82c8c9b8 | ||
|
|
9efe076cc2 | ||
|
|
fc5089e70b | ||
|
|
9ff1885d5a | ||
|
|
b79619bf8b | ||
|
|
1970b939e0 | ||
|
|
1d62e36303 | ||
|
|
03ce71519d | ||
|
|
74dcddaa1a | ||
|
|
616d0425db | ||
|
|
43f7553cdd | ||
|
|
72289cda1a | ||
|
|
d2fc00b7d6 | ||
|
|
f99f80159d | ||
|
|
c430b69322 | ||
|
|
79834e4d47 | ||
|
|
1b29133ee0 | ||
|
|
1d29781804 | ||
|
|
d28c9dbc4b | ||
|
|
515bb1c7ce | ||
|
|
1e4337e900 | ||
|
|
a893260de0 | ||
|
|
97a8545d78 | ||
|
|
b6edc11eb2 | ||
|
|
4bff31e3b1 | ||
|
|
06f6a4da8b | ||
|
|
2d7115e1e8 | ||
|
|
f6d0b0997f | ||
|
|
9da5be7fd8 | ||
|
|
71128e5a55 | ||
|
|
406332f6cd | ||
|
|
a1252c5701 | ||
|
|
28e204fd80 | ||
|
|
86ecc62b33 | ||
|
|
e3964f8bbe | ||
|
|
869d8e6d8b | ||
|
|
19b2dd4c7a | ||
|
|
00def1d8d1 | ||
|
|
734d34b7a8 | ||
|
|
f84bdf7287 | ||
|
|
cb6b98499a | ||
|
|
9521deea25 | ||
|
|
fd94a69ff8 | ||
|
|
e4822f00ec | ||
|
|
e29708f871 | ||
|
|
724f1cf713 | ||
|
|
4eec055f84 | ||
|
|
ef51d75f2c | ||
|
|
d6ff90260e | ||
|
|
7f17176462 | ||
|
|
0ff9ebfac9 | ||
|
|
09fb0618b4 | ||
|
|
2a5f9776a3 | ||
|
|
52deea3033 | ||
|
|
ede572f6e3 | ||
|
|
ca8c9b55ae | ||
|
|
ca0b8e9afc | ||
|
|
dab5b4d723 | ||
|
|
9eb15274a2 | ||
|
|
e0150a8962 | ||
|
|
f37b23b706 | ||
|
|
53b965651d | ||
|
|
d501c4b836 | ||
|
|
7d50fa373e | ||
|
|
f45de7ba36 | ||
|
|
3e208219b6 | ||
|
|
edaa223856 | ||
|
|
d8a15c39cb | ||
|
|
a1b63b61d7 | ||
|
|
801ad469c5 | ||
|
|
8ca664a8fe | ||
|
|
c0dff1e7df | ||
|
|
459f9ffc2c | ||
|
|
2950f55879 | ||
|
|
169ea3d5d5 | ||
|
|
abffc38c11 | ||
|
|
0c34554b9c | ||
|
|
160b5b5b01 | ||
|
|
cd4a327671 | ||
|
|
98faffc3ca | ||
|
|
e35254c8f2 | ||
|
|
bd57d0f19a | ||
|
|
a1da3f9842 | ||
|
|
20bb7fc372 | ||
|
|
90f5ed4251 | ||
|
|
4a664a7b3d | ||
|
|
6b85b8d4a2 | ||
|
|
a4053dcf19 | ||
|
|
7dee289b5b | ||
|
|
78838585f7 | ||
|
|
fa5b52210a | ||
|
|
59c84bcb85 | ||
|
|
4de043f3d4 | ||
|
|
c86c5133f0 | ||
|
|
c7ef661db7 | ||
|
|
a0bbf61db2 | ||
|
|
ebaf5c4565 | ||
|
|
e514ef744b | ||
|
|
d1e43c11b9 | ||
|
|
1f6301c2c0 | ||
|
|
f4455ccb93 | ||
|
|
a091e80ed0 | ||
|
|
a968f09d73 | ||
|
|
5f42f66c02 | ||
|
|
ece131995a | ||
|
|
b3c17c8ee8 | ||
|
|
6f2901b324 | ||
|
|
e0a80734f3 | ||
|
|
2aad13dc72 | ||
|
|
7ba2058625 | ||
|
|
07ea03d12b | ||
|
|
06d5da50a2 | ||
|
|
d80f62d1b9 | ||
|
|
26eedc9701 | ||
|
|
89be653a51 | ||
|
|
a4767827b4 | ||
|
|
0be574809b | ||
|
|
56e52156e5 | ||
|
|
0e2b7767c7 | ||
|
|
3b57550ead | ||
|
|
bbf3d44d69 | ||
|
|
0b5c47cd2e | ||
|
|
7d48e426dc | ||
|
|
db7ad52a4d | ||
|
|
1aab888bf2 | ||
|
|
8576034b77 | ||
|
|
3f8226c36c | ||
|
|
0832ef86e4 | ||
|
|
55dc4dbdfc | ||
|
|
5d5124dd5b | ||
|
|
a441a6eaf7 | ||
|
|
aaa9f5cd98 | ||
|
|
cf7f815eba | ||
|
|
d96be997ac | ||
|
|
080ef03eec | ||
|
|
41da6d489d | ||
|
|
c06be1c56c | ||
|
|
e6963822e9 | ||
|
|
effd4e89ab | ||
|
|
05a5b2367b | ||
|
|
dbb548e731 | ||
|
|
caeff77dae | ||
|
|
a4c8e85be5 | ||
|
|
19252629cb | ||
|
|
a6c852a82c | ||
|
|
2dc7089aa6 | ||
|
|
b9acbe6f2c | ||
|
|
06370baa0e | ||
|
|
c95468f972 | ||
|
|
46ce6ad50f | ||
|
|
32a96bbd06 | ||
|
|
04c7e4fa01 | ||
|
|
9214897245 | ||
|
|
e95acbec46 | ||
|
|
c7f6aea763 | ||
|
|
63c956af15 | ||
|
|
6bc6796c23 | ||
|
|
01a2ffaed5 | ||
|
|
7952becd17 | ||
|
|
fe9959cc97 | ||
|
|
a043d55b01 | ||
|
|
1b2d1afb9b | ||
|
|
43dc10b868 | ||
|
|
26dc262641 | ||
|
|
a79f7b2026 | ||
|
|
a3f35f2491 | ||
|
|
4cf4594945 | ||
|
|
87794e0793 | ||
|
|
8df2c16cce | ||
|
|
418fea2cfc | ||
|
|
6f3e33c0b1 | ||
|
|
5b0e627f6d | ||
|
|
a66f818e75 | ||
|
|
1e6e40a3ab | ||
|
|
83d61b1b80 | ||
|
|
08cf6a523d | ||
|
|
6a89ae986e | ||
|
|
6dd34a8401 | ||
|
|
716fc867a1 | ||
|
|
44a770be0e | ||
|
|
06a0f76fed | ||
|
|
06fbdf1f12 | ||
|
|
43436e19b7 | ||
|
|
304d90062d | ||
|
|
a6e720f154 | ||
|
|
804f225908 | ||
|
|
b70407d7fe | ||
|
|
3ffc1c947d | ||
|
|
9391b11403 | ||
|
|
b7dda83545 | ||
|
|
cf4a35e148 | ||
|
|
9f2d79f2dc | ||
|
|
6709338dbd | ||
|
|
67a5217482 | ||
|
|
260f4ccdb8 | ||
|
|
c36e4cc3da | ||
|
|
028b43f4d1 | ||
|
|
c0735c5b1f | ||
|
|
3e38941b57 | ||
|
|
6241a31e8c | ||
|
|
4a51ae5038 | ||
|
|
26a8809121 | ||
|
|
4a49f48969 | ||
|
|
4feaa1db98 | ||
|
|
3b62d5708a | ||
|
|
d146adcc3b | ||
|
|
e9fce49fee | ||
|
|
32342dcd5d | ||
|
|
17dd9db946 | ||
|
|
3ed61319ad | ||
|
|
43815f6711 | ||
|
|
c2ba4a334e | ||
|
|
1e968a1713 | ||
|
|
3f3540bd33 | ||
|
|
28b24d01ad | ||
|
|
cd7ece7caf | ||
|
|
10f907477d | ||
|
|
56151b0d12 | ||
|
|
73fe3bfb96 | ||
|
|
a2473645a5 | ||
|
|
1caeb1d88b | ||
|
|
a758d894f6 | ||
|
|
0f80a25937 | ||
|
|
9e1df83a87 | ||
|
|
1e7f6b8f0f | ||
|
|
30440472f7 | ||
|
|
f973de4ab6 | ||
|
|
f560f25302 | ||
|
|
09942e8e18 | ||
|
|
cb7874ac8d | ||
|
|
ce752f0d75 | ||
|
|
6e0ae6d152 | ||
|
|
27f20a76f0 | ||
|
|
1a4a8cc921 | ||
|
|
dc44cc8e1f | ||
|
|
0ac529146e | ||
|
|
a529c91254 | ||
|
|
7022ed95b6 | ||
|
|
43bfee4d55 | ||
|
|
957bc91828 | ||
|
|
17e564a094 | ||
|
|
f89ccdd2ee | ||
|
|
e5fa0050cd | ||
|
|
ed12f814dd | ||
|
|
997023e52f | ||
|
|
4f0a45c902 | ||
|
|
82ecf6cd6d | ||
|
|
37d3bb0eb0 | ||
|
|
1c01e927f9 | ||
|
|
9e79f02787 | ||
|
|
9e70d2dfc6 | ||
|
|
bec6c20531 | ||
|
|
9a7a0d293e | ||
|
|
ddb6346087 | ||
|
|
035251c04c | ||
|
|
e49bbe0faf | ||
|
|
df5cde2e82 | ||
|
|
a213082c4d | ||
|
|
f740dceb78 | ||
|
|
0a0cb9905e | ||
|
|
ae414c42c1 | ||
|
|
2109520bde | ||
|
|
1fc0e76c41 | ||
|
|
2c6dff3714 | ||
|
|
f35395e76f | ||
|
|
407e0ee8c1 | ||
|
|
b3295f5f33 | ||
|
|
6b4129c400 | ||
|
|
a6d734018a | ||
|
|
61fd54b026 | ||
|
|
38e159d5e6 | ||
|
|
cbc64936e6 | ||
|
|
8a5d027374 | ||
|
|
596f1f1183 | ||
|
|
a9336968c7 | ||
|
|
48ef4c6c04 | ||
|
|
cd219e4fa8 | ||
|
|
3617e9a260 | ||
|
|
732ab3e5c6 | ||
|
|
c3df1e7328 | ||
|
|
905dc359a5 | ||
|
|
d5740e4851 | ||
|
|
9f2bbe3c03 | ||
|
|
5de35ee353 | ||
|
|
c8b9288f1c | ||
|
|
3febc28c78 | ||
|
|
366e7dc409 | ||
|
|
25285b10ee | ||
|
|
ba3183e10b | ||
|
|
9c71652b2c | ||
|
|
fb638cba43 | ||
|
|
df7c821bd4 | ||
|
|
6587282ac2 | ||
|
|
4b02426ab2 | ||
|
|
3843a46de9 | ||
|
|
e6b2be6fdf | ||
|
|
bee600bfd8 | ||
|
|
5a25ce50db | ||
|
|
41eb94e5d9 | ||
|
|
f8e7db1ba7 | ||
|
|
b21990e1d1 | ||
|
|
12fc34d4de | ||
|
|
723fc96ec5 | ||
|
|
3e4e1bcea4 | ||
|
|
ece592287c | ||
|
|
63ddbed359 | ||
|
|
044931c08e | ||
|
|
48e5b567cb | ||
|
|
7140574c37 | ||
|
|
b8ffa37e97 | ||
|
|
17abb57ed6 | ||
|
|
f78561cef7 | ||
|
|
82c5fbcf46 | ||
|
|
0d08c6a136 | ||
|
|
6bdc1c5d3f | ||
|
|
865c9cdac5 | ||
|
|
25094ae5b6 | ||
|
|
d9bf0ab2e9 | ||
|
|
72c0e8579a | ||
|
|
2ff8f84387 | ||
|
|
70136e20aa | ||
|
|
47dda553d9 | ||
|
|
0eeafe69d9 | ||
|
|
e43a45b979 | ||
|
|
c889efef7c | ||
|
|
c7c1506e42 | ||
|
|
9142dd9930 | ||
|
|
6a802d607c | ||
|
|
5aa27a8dc8 | ||
|
|
a4752e1379 | ||
|
|
15c1717290 | ||
|
|
0793856259 | ||
|
|
6ca3fb61ce | ||
|
|
0e4edc9571 |
40
.github/workflows/build.yml
vendored
@ -1,10 +1,15 @@
|
||||
name: OpenDTU Build
|
||||
name: OpenDTU-onBattery Build
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- docs/**
|
||||
- '**/*.md'
|
||||
branches:
|
||||
- master
|
||||
- development
|
||||
tags-ignore:
|
||||
- v*
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- docs/**
|
||||
@ -96,26 +101,43 @@ jobs:
|
||||
run: pio run -e ${{ matrix.environment }}
|
||||
|
||||
- name: Rename Firmware
|
||||
run: mv .pio/build/${{ matrix.environment }}/firmware.bin .pio/build/${{ matrix.environment }}/opendtu-${{ matrix.environment }}.bin
|
||||
run: mv .pio/build/${{ matrix.environment }}/firmware.bin .pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.bin
|
||||
|
||||
- name: Rename Factory Firmware
|
||||
run: mv .pio/build/${{ matrix.environment }}/firmware.factory.bin .pio/build/${{ matrix.environment }}/opendtu-${{ matrix.environment }}.factory.bin
|
||||
run: mv .pio/build/${{ matrix.environment }}/firmware.factory.bin .pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.factory.bin
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: opendtu-${{ matrix.environment }}
|
||||
name: opendtu-onbattery-${{ matrix.environment }}
|
||||
path: |
|
||||
.pio/build/${{ matrix.environment }}/opendtu-${{ matrix.environment }}.bin
|
||||
.pio/build/${{ matrix.environment }}/opendtu-${{ matrix.environment }}.factory.bin
|
||||
.pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.bin
|
||||
.pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.factory.bin
|
||||
|
||||
release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: [get_default_envs, build]
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
if: startsWith(github.ref, 'refs/tags/2')
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get tags
|
||||
run: git fetch --force --tags origin
|
||||
|
||||
- name: Get openDTU core release
|
||||
run: |
|
||||
echo "OPEN_DTU_CORE_RELEASE=$(git for-each-ref --sort=creatordate --format '%(refname) %(creatordate)' refs/tags | grep 'refs/tags/v' | tail -1 | sed 's#.*/##' | sed 's/ .*//')" >> $GITHUB_ENV
|
||||
|
||||
- name: Create openDTU-core-release-Badge
|
||||
uses: schneegans/dynamic-badges-action@v1.6.0
|
||||
with:
|
||||
auth: ${{ secrets.GIST_SECRET }}
|
||||
gistID: 68b47cc8c8994d04ab3a4fa9d8aee5e6
|
||||
filename: openDTUcoreRelease.json
|
||||
label: based on original OpenDTU
|
||||
message: ${{ env.OPEN_DTU_CORE_RELEASE }}
|
||||
color: lightblue
|
||||
|
||||
- name: Build Changelog
|
||||
id: github_release
|
||||
@ -135,7 +157,7 @@ jobs:
|
||||
run: |
|
||||
ls -R
|
||||
cd artifacts
|
||||
for i in */; do cp ${i}opendtu-*.bin ./; done
|
||||
for i in */; do cp ${i}opendtu-onbattery-*.bin ./; done
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v1
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
}
|
||||
],
|
||||
"template": "${{CHANGELOG}}",
|
||||
"pr_template": "- [${{TITLE}}](https://github.com/tbnobody/OpenDTU/commit/${{MERGE_SHA}})",
|
||||
"pr_template": "- [${{TITLE}}](https://github.com/helgeerbe/OpenDTU-OnBattery/commit/${{MERGE_SHA}})",
|
||||
"empty_template": "- no changes",
|
||||
"label_extractor": [
|
||||
{
|
||||
|
||||
103
.github/workflows/test_build.yml
vendored
Normal file
@ -0,0 +1,103 @@
|
||||
name: OpenDTU-onBattery Test Build
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
get_default_envs:
|
||||
name: Gather Environments
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.9"
|
||||
|
||||
- name: Install PlatformIO
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install --upgrade platformio
|
||||
|
||||
- name: Get default environments
|
||||
id: envs
|
||||
run: |
|
||||
echo "environments=$(pio project config --json-output | jq -cr '.[1][1][0][1]|split(",")')" >> $GITHUB_OUTPUT
|
||||
|
||||
outputs:
|
||||
environments: ${{ steps.envs.outputs.environments }}
|
||||
|
||||
build:
|
||||
name: Build Enviornments
|
||||
runs-on: ubuntu-latest
|
||||
needs: get_default_envs
|
||||
strategy:
|
||||
matrix:
|
||||
environment: ${{ fromJSON(needs.get_default_envs.outputs.environments) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Get tags
|
||||
run: git fetch --force --tags origin
|
||||
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
|
||||
- name: Cache PlatformIO
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.9"
|
||||
|
||||
- name: Install PlatformIO
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install --upgrade platformio
|
||||
|
||||
- name: Setup Node.js and yarn
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
cache: "yarn"
|
||||
cache-dependency-path: "webapp/yarn.lock"
|
||||
|
||||
- name: Install WebApp dependencies
|
||||
run: yarn --cwd webapp install --frozen-lockfile
|
||||
|
||||
- name: Build WebApp
|
||||
run: yarn --cwd webapp build
|
||||
|
||||
- name: Build firmware
|
||||
run: pio run -e ${{ matrix.environment }}
|
||||
|
||||
- name: Rename Firmware
|
||||
run: mv .pio/build/${{ matrix.environment }}/firmware.bin .pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.bin
|
||||
|
||||
- name: Rename Factory Firmware
|
||||
run: mv .pio/build/${{ matrix.environment }}/firmware.factory.bin .pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.factory.bin
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: opendtu-onbattery-${{ matrix.environment }}
|
||||
path: |
|
||||
.pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.bin
|
||||
.pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.factory.bin
|
||||
|
||||
|
||||
62
.vscode/settings.json
vendored
@ -1,3 +1,63 @@
|
||||
{
|
||||
"C_Cpp.clang_format_style": "WebKit"
|
||||
"C_Cpp.clang_format_style": "WebKit",
|
||||
"files.associations": {
|
||||
"*.tcc": "cpp",
|
||||
"algorithm": "cpp",
|
||||
"array": "cpp",
|
||||
"atomic": "cpp",
|
||||
"bitset": "cpp",
|
||||
"cctype": "cpp",
|
||||
"chrono": "cpp",
|
||||
"clocale": "cpp",
|
||||
"cmath": "cpp",
|
||||
"condition_variable": "cpp",
|
||||
"cstdarg": "cpp",
|
||||
"cstddef": "cpp",
|
||||
"cstdint": "cpp",
|
||||
"cstdio": "cpp",
|
||||
"cstdlib": "cpp",
|
||||
"cstring": "cpp",
|
||||
"ctime": "cpp",
|
||||
"cwchar": "cpp",
|
||||
"cwctype": "cpp",
|
||||
"deque": "cpp",
|
||||
"list": "cpp",
|
||||
"unordered_map": "cpp",
|
||||
"unordered_set": "cpp",
|
||||
"vector": "cpp",
|
||||
"exception": "cpp",
|
||||
"functional": "cpp",
|
||||
"iterator": "cpp",
|
||||
"map": "cpp",
|
||||
"memory": "cpp",
|
||||
"memory_resource": "cpp",
|
||||
"numeric": "cpp",
|
||||
"optional": "cpp",
|
||||
"random": "cpp",
|
||||
"ratio": "cpp",
|
||||
"regex": "cpp",
|
||||
"string": "cpp",
|
||||
"string_view": "cpp",
|
||||
"system_error": "cpp",
|
||||
"tuple": "cpp",
|
||||
"type_traits": "cpp",
|
||||
"utility": "cpp",
|
||||
"fstream": "cpp",
|
||||
"initializer_list": "cpp",
|
||||
"iomanip": "cpp",
|
||||
"iosfwd": "cpp",
|
||||
"iostream": "cpp",
|
||||
"istream": "cpp",
|
||||
"limits": "cpp",
|
||||
"mutex": "cpp",
|
||||
"new": "cpp",
|
||||
"ostream": "cpp",
|
||||
"sstream": "cpp",
|
||||
"stdexcept": "cpp",
|
||||
"streambuf": "cpp",
|
||||
"thread": "cpp",
|
||||
"cinttypes": "cpp",
|
||||
"typeinfo": "cpp",
|
||||
"variant": "cpp"
|
||||
}
|
||||
}
|
||||
136
README.md
@ -1,104 +1,60 @@
|
||||
# OpenDTU
|
||||
- [OpenDTU-onBattery](#opendtu-onbattery)
|
||||
- [What is OpenDTU-onBattery](#what-is-opendtu-onbattery)
|
||||
- [History of the project](#history-of-the-project)
|
||||
- [Highlights of OpenDTU-onBattery](#highlights-of-opendtu-onbattery)
|
||||
- [Documentation](#documentation)
|
||||
- [Acknowledgment](#acknowledgment)
|
||||
|
||||
[](https://github.com/tbnobody/OpenDTU/actions/workflows/build.yml)
|
||||
[](https://github.com/tbnobody/OpenDTU/actions/workflows/cpplint.yml)
|
||||
[](https://github.com/tbnobody/OpenDTU/actions/workflows/yarnlint.yml)
|
||||
# OpenDTU-onBattery
|
||||
|
||||
## !! IMPORTANT UPGRADE NOTES !!
|
||||
This is a fork from the Hoymiles project [OpenDTU](https://github.com/tbnobody/OpenDTU).
|
||||
|
||||
If you are upgrading from a version before 15.03.2023 you have to upgrade the partition table of the ESP32. Please follow the [this](docs/UpgradePartition.md) documentation!
|
||||

|
||||
|
||||
## Background
|
||||
[](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/build.yml)
|
||||
[](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/cpplint.yml)
|
||||
[](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/yarnlint.yml)
|
||||
|
||||
This project was started from [this](https://www.mikrocontroller.net/topic/525778) discussion (Mikrocontroller.net).
|
||||
It was the goal to replace the original Hoymiles DTU (Telemetry Gateway) with their cloud access. With a lot of reverse engineering the Hoymiles protocol was decrypted and analyzed.
|
||||
## What is OpenDTU-onBattery
|
||||
|
||||
OpenDTU-onBattery is an extension of the original OpenDTU to support battery chargers, battery management systems (BMS) and power meters on a single esp32. With the help of a dynamic power limiter, the power production can be adjusted to the actual consumption. In this way, it is possible to come as close as possible to the goal of zero feed-in.
|
||||
|
||||
## History of the project
|
||||
|
||||
The original OpenDTU project was started from [this](https://www.mikrocontroller.net/topic/525778) discussion (Mikrocontroller.net). It was the goal to replace the original Hoymiles DTU (Telemetry Gateway) with their cloud access. With a lot of reverse engineering the Hoymiles protocol was decrypted and analyzed.
|
||||
|
||||
Summer 2022 I bought my Victron MPPT battery charger, and didn't like the idea to set up a separate esp32 to recieve the charger data. I decided to fork OpenDTU and extend it with battery charger support and a dynamic power limitter to my own needs. Hoping someone can make use of it.
|
||||
|
||||
## Highlights of OpenDTU-onBattery
|
||||
|
||||
This project is still under development and adds following features:
|
||||
|
||||
> **Warning**
|
||||
>
|
||||
> In contrast to the original openDTU, with release 2023.05.23.post1 openDTU-onBattery supports only 5 inverters. Otherwise, there is not enough memory for the liveData view.
|
||||
|
||||
* Support Victron's Ve.Direct protocol on the same chip (cable based serial interface!). Additional information about Ve.direct can be downloaded directly from [Victron's website](https://www.victronenergy.com/support-and-downloads/technical-information).
|
||||
* Dynamically sets the Hoymiles power limited according to the currently used energy in the household. Needs an HTTP JSON based power meter (e.g. Tasmota), an MQTT based power meter like Shelly 3EM or an SDM power meter.
|
||||
* Battery support: Read the voltage from Victron MPPT charge controller or from the Hoymiles DC inputs and starts/stops the power producing based on configurable voltage thresholds
|
||||
* Voltage correction that takes the voltage drop because of the current output load into account (not 100% reliable calculation)
|
||||
* Can read the current solar panel power from the Victron MPPT and adjust the limiter accordingly to not save energy in the battery (for increased system efficiency). Increases the battery lifespan and reduces energy loses.
|
||||
* Settings can be configured in the UI
|
||||
* Pylontech Battery support (via CAN bus interface). Use the SOC for starting/stopping the power output and provide the battery data via MQTT (autodiscovery for home assistant is currently not supported). Pin Mapping is supported (default RX PIN 27, TX PIN 26). Actual no live view support for Pylontech Battery.
|
||||
* Huawei R4850G2 power supply unit that can act as AC charger. Supports status shown on the web interface and options to set voltage and current limits on the web interface and via MQTT. Connection is done using CAN bus (needs to be separate from Pylontech CAN bus) via SN65HVD230 interface.
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation can be found [here](https://tbnobody.github.io/OpenDTU-docs/).
|
||||
Please feel free to support and create a PR in [this](https://github.com/tbnobody/OpenDTU-docs) repository to make the documentation even better.
|
||||
[Full documentation of OpenDTU-onBattery extensions can be found at the project's wiki](https://github.com/helgeerbe/OpenDTU-OnBattery/wiki).
|
||||
|
||||
## Breaking changes
|
||||
For documentation of openDTU core functionality I refer to the original [repo](https://github.com/tbnobody/OpenDTU) and its [wiki](https://github.com/tbnobody/OpenDTU/wiki).
|
||||
|
||||
Generated using: `git log --date=short --pretty=format:"* %h%x09%ad%x09%s" | grep BREAKING`
|
||||
Please note that openDTU-onBattery may change significantly during its development.
|
||||
Bug reports, comments, feature requests and fixes are most welcome!
|
||||
|
||||
```code
|
||||
* 71d1b3b 2023-11-07 BREAKING CHANGE: Home Assistant Auto Discovery to new naming scheme
|
||||
* 04f62e0 2023-04-20 BREAKING CHANGE: Web API Endpoint /api/eventlog/status no nested serial object
|
||||
* 59f43a8 2023-04-17 BREAKING CHANGE: Web API Endpoint /api/devinfo/status requires GET parameter inv=
|
||||
* 318136d 2023-03-15 BREAKING CHANGE: Updated partition table: Make sure you have a configuration backup and completly reflash the device!
|
||||
* 3b7aef6 2023-02-13 BREAKING CHANGE: Web API!
|
||||
* d4c838a 2023-02-06 BREAKING CHANGE: Prometheus API!
|
||||
* daf847e 2022-11-14 BREAKING CHANGE: Removed deprecated config parsing method
|
||||
* 69b675b 2022-11-01 BREAKING CHANGE: Structure WebAPI /api/livedata/status changed
|
||||
* 27ed4e3 2022-10-31 BREAKING: Change power factor from percent value to value between 0 and 1
|
||||
```
|
||||
To find out what's new or improved have a look at the [changelog](https://github.com/helgeerbe/OpenDTU-OnBattery/releases).
|
||||
|
||||
## Currently supported Inverters
|
||||
## Acknowledgment
|
||||
|
||||
| Model | Required RF Module | DC Inputs | MPP-Tracker | AC Phases |
|
||||
| ---------------------| ------------------ | --------- | ----------- | --------- |
|
||||
| Hoymiles HM-300-1T | NRF24L01+ | 1 | 1 | 1 |
|
||||
| Hoymiles HM-350-1T | NRF24L01+ | 1 | 1 | 1 |
|
||||
| Hoymiles HM-400-1T | NRF24L01+ | 1 | 1 | 1 |
|
||||
| Hoymiles HM-600-2T | NRF24L01+ | 2 | 2 | 1 |
|
||||
| Hoymiles HM-700-2T | NRF24L01+ | 2 | 2 | 1 |
|
||||
| Hoymiles HM-800-2T | NRF24L01+ | 2 | 2 | 1 |
|
||||
| Hoymiles HM-1000-4T | NRF24L01+ | 4 | 2 | 1 |
|
||||
| Hoymiles HM-1200-4T | NRF24L01+ | 4 | 2 | 1 |
|
||||
| Hoymiles HM-1500-4T | NRF24L01+ | 4 | 2 | 1 |
|
||||
| Hoymiles HMS-300-1T | CMT2300A | 1 | 1 | 1 |
|
||||
| Hoymiles HMS-350-1T | CMT2300A | 1 | 1 | 1 |
|
||||
| Hoymiles HMS-400-1T | CMT2300A | 1 | 1 | 1 |
|
||||
| Hoymiles HMS-450-1T | CMT2300A | 1 | 1 | 1 |
|
||||
| Hoymiles HMS-500-1T | CMT2300A | 1 | 1 | 1 |
|
||||
| Hoymiles HMS-600-2T | CMT2300A | 2 | 2 | 1 |
|
||||
| Hoymiles HMS-700-2T | CMT2300A | 2 | 2 | 1 |
|
||||
| Hoymiles HMS-800-2T | CMT2300A | 2 | 2 | 1 |
|
||||
| Hoymiles HMS-900-2T | CMT2300A | 2 | 2 | 1 |
|
||||
| Hoymiles HMS-1000-2T | CMT2300A | 2 | 2 | 1 |
|
||||
| Hoymiles HMS-1600-4T | CMT2300A | 4 | 4 | 1 |
|
||||
| Hoymiles HMS-1800-4T | CMT2300A | 4 | 4 | 1 |
|
||||
| Hoymiles HMS-2000-4T | CMT2300A | 4 | 4 | 1 |
|
||||
| Hoymiles HMT-1600-4T | CMT2300A | 4 | 2 | 3 |
|
||||
| Hoymiles HMT-1800-4T | CMT2300A | 4 | 2 | 3 |
|
||||
| Hoymiles HMT-2000-4T | CMT2300A | 4 | 2 | 3 |
|
||||
| Hoymiles HMT-1800-6T | CMT2300A | 6 | 3 | 3 |
|
||||
| Hoymiles HMT-2250-6T | CMT2300A | 6 | 3 | 3 |
|
||||
| Solenso SOL-H350 | NRF24L01+ | 1 | 1 | 1 |
|
||||
| Solenso SOL-H400 | NRF24L01+ | 1 | 1 | 1 |
|
||||
| Solenso SOL-H800 | NRF24L01+ | 2 | 2 | 1 |
|
||||
| TSUN TSOL-M350 | NRF24L01+ | 1 | 1 | 1 |
|
||||
| TSUN TSOL-M800 | NRF24L01+ | 2 | 2 | 1 |
|
||||
| TSUN TSOL-M1600 | NRF24L01+ | 4 | 2 | 1 |
|
||||
A special Thank to Thomas Basler (tbnobody) the author of the original [OpenDTU](https://github.com/tbnobody/OpenDTU) project. You are doing a great job!
|
||||
|
||||
|
||||
|
||||
## Hardware you need
|
||||
|
||||
### ESP32 board
|
||||
|
||||
For ease of use, buy a "ESP32 DEVKIT DOIT" or "ESP32 NodeMCU Development Board" with an ESP32-S3 or ESP-WROOM-32 chipset on it.
|
||||
|
||||
Sample Picture:
|
||||
|
||||

|
||||
|
||||
Also supported: Board with Ethernet-Connector and Power-over-Ethernet [Olimex ESP32-POE](https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware)
|
||||
|
||||
### Change pin assignment
|
||||
|
||||
Its possible to change all the pins of the NRF24L01+ module, the Display, the LED etc.
|
||||
The recommend way to change the pin assignment is by creating a custom [device profile](docs/DeviceProfiles.md).
|
||||
It is also possible to create a custom environment and compile the source yourself. This can be achieved by copying one of the [env:....] sections from 'platformio.ini' to 'platformio_override.ini' and editing the 'platformio_override.ini' file and add/change one or more of the following lines to the 'build_flags' parameter:
|
||||
|
||||
```makefile
|
||||
-DHOYMILES_PIN_MISO=19
|
||||
-DHOYMILES_PIN_MOSI=23
|
||||
-DHOYMILES_PIN_SCLK=18
|
||||
-DHOYMILES_PIN_IRQ=16
|
||||
-DHOYMILES_PIN_CE=4
|
||||
-DHOYMILES_PIN_CS=5
|
||||
```
|
||||
|
||||
It is recommended to make all changes only in the 'platformio_override.ini', this is your personal copy.
|
||||
Last but not least, I would like to thank all the contributors. With your ideas and enhancements, you have made OpenDTU-onBattery much more than I originally had in mind.
|
||||
|
||||
186
README_onBattery.md
Normal file
@ -0,0 +1,186 @@
|
||||
# OpenDTU-OnBattery
|
||||
|
||||
This is a fork from the Hoymiles project [OpenDTU](https://github.com/tbnobody/OpenDTU). This project is still under development but is being used on a day to day basis as well.
|
||||
|
||||

|
||||
|
||||
> **Warning**
|
||||
>
|
||||
> In contrast to the original openDTU, with release 2023.05.23.post1 openDTU-onBattery supports only 5 inverters. Otherwise, there is not enough memory for the liveData view.
|
||||
|
||||
## Features
|
||||
|
||||
* Hoymiles inverter support for live data and display of various inverter internal information. (Partial) support for multiple inverters.
|
||||
* MQTT support (with TLS) with partial Home Assistant MQTT Auto Discovery
|
||||
* Automatic inverter power control of a selected Hoymiles inverter to compensate the currently used energy in the household.
|
||||
* Energy meter support with interface options to HTTP JSON based power meters (e.g. Tasmota), MQTT based power meters (e.g. Shelly 3EM) or SDM power meters.
|
||||
* Support for Victron MPPT charge controller using Ve.Direct. cf. Ve.direct: https://www.victronenergy.com/support-and-downloads/technical-information.
|
||||
* Generic voltage based battery support using Victron MPPT charge controller or Hoymiles inverter voltage values to start / stop inverter power output. (with load compensation)
|
||||
* Pylontech battery support via CAN bus interface. State of Charge reported by BMS is used to start / stop inverter power output. Battery data is exported via MQTT (no support for home assistant auto discovery).
|
||||
* Support for Huawei R4850G2 power supply unit that can act as AC charging source. [Overview](https://www.beyondlogic.org/review-huawei-r4850g2-power-supply-53-5vdc-3kw/)
|
||||
* Other features from [OpenDTU](https://github.com/tbnobody/OpenDTU) maintained
|
||||
|
||||
## Hardware
|
||||
|
||||
To get started with this project you will need to assemble a few hardware components that allow interfacing with the desired devices. What is needed depends on the use-case but may consist of:
|
||||
|
||||
* ESP32 board that contains the CPU and WIFI connectivity
|
||||
* NRF24L01+ or CMT2300A radio board to interface with the inverter. Please check the list of the supported inverters below for the board needed.
|
||||
* 3.3V / 5V logic level shifter to interface with the Victron MPPT charge controller
|
||||
* SN65HVD230 CAN bus transceiver to interface with a Pylontech battery
|
||||
* MCP2515 SPI / CAN bus transceiver to interface with the Huawei AC PSU
|
||||
* Relais board + 3.3V / 5 V logic level shifter to switch the slot detect on the Huawei AC PSU
|
||||
* Display [Display](docs/Display.md)
|
||||
|
||||
More detailed information on the hardware can be found in the [Hardware and flashing](docs/hardware_flash.md) document.
|
||||
|
||||
### Currently supported Inverters
|
||||
|
||||
| Model | Required RF Module | DC Inputs | MPP-Tracker | AC Phases |
|
||||
| --------------------| ------------------ | --------- | ----------- | --------- |
|
||||
| Hoymiles HM-300 | NRF24L01+ | 1 | 1 | 1 |
|
||||
| Hoymiles HM-350 | NRF24L01+ | 1 | 1 | 1 |
|
||||
| Hoymiles HM-400 | NRF24L01+ | 1 | 1 | 1 |
|
||||
| Hoymiles HM-600 | NRF24L01+ | 2 | 2 | 1 |
|
||||
| Hoymiles HM-700 | NRF24L01+ | 2 | 2 | 1 |
|
||||
| Hoymiles HM-800 | NRF24L01+ | 2 | 2 | 1 |
|
||||
| Hoymiles HM-1000 | NRF24L01+ | 4 | 2 | 1 |
|
||||
| Hoymiles HM-1200 | NRF24L01+ | 4 | 2 | 1 |
|
||||
| Hoymiles HM-1500 | NRF24L01+ | 4 | 2 | 1 |
|
||||
| Hoymiles HMS-300 | CMT2300A | 1 | 1 | 1 |
|
||||
| Hoymiles HMS-350 | CMT2300A | 1 | 1 | 1 |
|
||||
| Hoymiles HMS-400 | CMT2300A | 1 | 1 | 1 |
|
||||
| Hoymiles HMS-450 | CMT2300A | 1 | 1 | 1 |
|
||||
| Hoymiles HMS-500 | CMT2300A | 1 | 1 | 1 |
|
||||
| Hoymiles HMS-600 | CMT2300A | 2 | 2 | 1 |
|
||||
| Hoymiles HMS-700 | CMT2300A | 2 | 2 | 1 |
|
||||
| Hoymiles HMS-800 | CMT2300A | 2 | 2 | 1 |
|
||||
| Hoymiles HMS-900 | CMT2300A | 2 | 2 | 1 |
|
||||
| Hoymiles HMS-1000 | CMT2300A | 2 | 2 | 1 |
|
||||
| Hoymiles HMS-1600 | CMT2300A | 4 | 4 | 1 |
|
||||
| Hoymiles HMS-1800 | CMT2300A | 4 | 4 | 1 |
|
||||
| Hoymiles HMS-2000 | CMT2300A | 4 | 4 | 1 |
|
||||
| Hoymiles HMT-1800 | CMT2300A | 6 | 3 | 3 |
|
||||
| Hoymiles HMT-2250 | CMT2300A | 6 | 3 | 3 |
|
||||
| Solenso SOL-H350 | NRF24L01+ | 1 | 1 | 1 |
|
||||
| Solenso SOL-H400 | NRF24L01+ | 1 | 1 | 1 |
|
||||
| Solenso SOL-H800 | NRF24L01+ | 2 | 2 | 1 |
|
||||
| TSUN TSOL-M350 | NRF24L01+ | 1 | 1 | 1 |
|
||||
| TSUN TSOL-M800 | NRF24L01+ | 2 | 2 | 1 |
|
||||
| TSUN TSOL-M1600 | NRF24L01+ | 4 | 2 | 1 |
|
||||
|
||||
**TSUN compatibility remark:**
|
||||
Compatibility with OpenDTU is most likely related to the serial number of the inverter. Current findings indicate that TSUN inverters with a serial number starting with "11" are supported, whereby inverters with a serial number starting with "10" are not.
|
||||
|
||||
|
||||
## Screenshots
|
||||
|
||||
Several screenshots of the frontend can be found here: [Screenshots](docs/screenshots/README.md)
|
||||
|
||||
## Configuration and usage
|
||||
|
||||
### First configuration
|
||||
|
||||
* After the [initial flashing](docs/hardware_flash.md#flashing-and-starting-up) of the microcontroller, an Access Point called "OpenDTU-*" is opened. The default password is "openDTU42".
|
||||
* Use a web browser to open the address [http://192.168.4.1](http://192.168.4.1)
|
||||
* Navigate to Settings --> Network Settings and enter your WiFi credentials. The username to access the config menu is "admin" and the password the same as for accessing the Access Point (default: "openDTU42").
|
||||
* OpenDTU then simultaneously connects to your WiFi AP with these credentials. Navigate to Info --> Network and look into section "Network Interface (Station)" for the IP address received via DHCP.
|
||||
* If your WiFi AP uses an allow-list for MAC-addresses, please be aware that the ESP32 has two different MAC addresses for its AP and client modes, they are also listed at Info --> Network.
|
||||
* When OpenDTU is connected to a configured WiFI AP, the "OpenDTU-*" Access Point is closed after 3 minutes.
|
||||
* OpenDTU needs access to a working NTP server to get the current date & time. Both are sent to the inverter with each request. Default NTP server is pool.ntp.org. If your network has different requirements please change accordingly (Settings --> NTP Settings).
|
||||
* Activate Ve.direct, Battery and the AC Charger according to the available hardware
|
||||
* Configure a Power Meter to provide a data source for the current consumption
|
||||
* Configure the Dynamic Power Limiter according to the used battery. Documentation about the power limiter interface and states can be found below.
|
||||
* If desired connect to a home automation system using MQTT or the Webapi.
|
||||
* A documentation of all available MQTT Topics can be found here: [MQTT Documentation](docs/MQTT_Topics.md)
|
||||
* A documentation of the Web API can be found here: [Web-API Documentation](docs/Web-API.md)
|
||||
* Home Assistant auto discovery is supported. [Example image](https://user-images.githubusercontent.com/59169507/217558862-a83846c5-6070-43cd-9a0b-90a8b2e2e8c6.png)
|
||||
|
||||
### Huawei PSU
|
||||
|
||||
The Huawei PSU can be used to charge a battery. This can be be useful if an external (AC) connected solar system shall be utilized or if variable energy prices should be exploited.
|
||||
|
||||
Some points for consideration are:
|
||||
* Make sure to consider the PSU voltage range when selecting the battery voltage as lower voltages <42V are not supported.
|
||||
* The PSU runs a noisy fan and it is therefore desireable to switch it off when not being used. Some users have found that switching the slot detect pins with a relay accomplishes this. A GPIO pin is made available from the ESP to turn the PSU on/off
|
||||
|
||||
#### Operation modes
|
||||
|
||||
openDTU-onBattery supports three operation modes for the Huawei PSU:
|
||||
1. Fully manual - In this mode the PSU needs to be turned on/off externally using MQTT and voltage and current limits need to be provided. See [MQTT Documentation](docs/MQTT_Topics.md) for details on these commands
|
||||
2. Manual with auto power on / off - In this mode the PSU is turned on when a current limit > 1A is set. If the current limit is < 1A for some time the PSU is turned off. Current and voltage limits need to be provided externally using MQTT. See [MQTT Documentation](docs/MQTT_Topics.md) for details on these commands.
|
||||
3. Automatic - In this mode the PSU power is controlled by the Power Meter and information provided in the web-interface. If excess power is present the PSU is turned on. The voltage limit is set as per web-interface and the current limit is set so that the maximum PSU output power equals the Power Meter value. Minium and maximum PSU power levels as configured in the web-interface are respected in this process. The PSU is turned off if the output current is limited and the output power drops below the minium power level. This will disable automatic mode until the battery is discharged below the start voltage level (set in the web-interface). This mode can be enabled using the web-interface and MQTT. See [MQTT Documentation](docs/MQTT_Topics.md)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
* First: When there is no light on the solar panels, the inverter completely turns off and does not answer to OpenDTU! So if you assembled your OpenDTU in the evening, wait until tomorrow.
|
||||
* When there is no data received from the inverter(s) - try to reduce the distance between the openDTU and the inverter (e.g. move it to the window towards the roof)
|
||||
* Under Settings -> DTU Settings you can increase the transmit power "PA level". Default is "minimum".
|
||||
* The NRF24L01+ needs relatively much current. With bad power supply (and especially bad cables!) a 10 µF capacitor soldered directly to the NRF24L01+ board connector brings more stability (pin 1+2 are the power supply). Note the polarity of the capacitor…
|
||||
* You can try to use an USB power supply with 1 A or more instead of connecting the ESP32 to the computer.
|
||||
* Try a different USB cable. Once again, a stable power source is important. Some USB cables are made of much plastic and very little copper inside.
|
||||
* Double check that you have a radio module NRF24L01+ with a plus sign at the end. NRF24L01 module without the plus are not compatible with this project.
|
||||
* There is no possibility of auto-discovering the inverters. Double check you have entered the serial numbers of the inverters correctly.
|
||||
* OpenDTU needs access to a working NTP server to get the current date & time.
|
||||
* If your problem persists, check the [Issues on Github](https://github.com/tbnobody/OpenDTU/issues). Please inspect not only the open issues, also the closed issues contain useful information.
|
||||
* Another source of information are the [Discussions](https://github.com/tbnobody/OpenDTU/discussions/)
|
||||
* When flashing with VSCode Plattform.IO fails and also with ESPRESSIF tool a demo bin file cannot be flashed to the ESP32 with error message "A fatal error occurred: MD5 of file does not match data in flash!" than un-wire/unconnect ESP32 from the NRF24L01+ board. Try to flash again and rewire afterwards.
|
||||
|
||||
## Background
|
||||
|
||||
This project was started from [this](https://www.mikrocontroller.net/topic/525778) discussion (Mikrocontroller.net).
|
||||
It was the goal to replace the original Hoymiles DTU (Telemetry Gateway) with their cloud access. With a lot of reverse engineering the Hoymiles protocol was decrypted and analyzed.
|
||||
|
||||
## Features for developers
|
||||
|
||||
### Status
|
||||
|
||||
[](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/build.yml)
|
||||
[](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/cpplint.yml)
|
||||
[](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/yarnlint.yml)
|
||||
|
||||
### Core technologies used
|
||||
|
||||
* The microcontroller part
|
||||
* Build with Arduino PlatformIO Framework for the ESP32
|
||||
* Uses a fork of [ESPAsyncWebserver](https://github.com/yubox-node-org/ESPAsyncWebServer) and [espMqttClient](https://github.com/bertmelis/espMqttClient)
|
||||
|
||||
* The WebApp part
|
||||
* Build with [Vue.js](https://vuejs.org)
|
||||
* Source is written in TypeScript
|
||||
|
||||
### Breaking changes
|
||||
|
||||
Generated using: `git log --date=short --pretty=format:"* %h%x09%ad%x09%s" | grep BREAKING`
|
||||
|
||||
```code
|
||||
* 59f43a8 2023-04-17 BREAKING CHANGE: Web API Endpoint /api/devinfo/status requires GET parameter inv=
|
||||
* 318136d 2023-03-15 BREAKING CHANGE: Updated partition table: Make sure you have a configuration backup and completly reflash the device!
|
||||
* 3b7aef6 2023-02-13 BREAKING CHANGE: Web API!
|
||||
* d4c838a 2023-02-06 BREAKING CHANGE: Prometheus API!
|
||||
* daf847e 2022-11-14 BREAKING CHANGE: Removed deprecated config parsing method
|
||||
* 69b675b 2022-11-01 BREAKING CHANGE: Structure WebAPI /api/livedata/status changed
|
||||
* 27ed4e3 2022-10-31 BREAKING: Change power factor from percent value to value between 0 and 1
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
* Building the WebApp
|
||||
* The WebApp can be build using yarn
|
||||
|
||||
```bash
|
||||
cd webapp
|
||||
yarn install
|
||||
yarn build
|
||||
```
|
||||
|
||||
* The updated output is placed in the 'webapp_dist' directory
|
||||
* It is only necessary to build the webapp when you made changes to it
|
||||
* Building the microcontroller firmware
|
||||
* Visual Studio Code with the PlatformIO Extension is required for building
|
||||
|
||||
## Related Projects
|
||||
|
||||
* [Ahoy](https://github.com/grindylow/ahoy)
|
||||
* [DTU Simulator](https://github.com/Ziyatoe/DTUsimMI1x00-Hoymiles)
|
||||
* [OpenDTU extended to talk to Victrons MPPT battery chargers (Ve.Direct)](https://github.com/helgeerbe/OpenDTU_VeDirect)
|
||||
@ -1,3 +1,91 @@
|
||||
# Device Profiles
|
||||
|
||||
This documentation will has been moved and can be found here: <https://tbnobody.github.io/OpenDTU-docs/firmware/device_profiles/>
|
||||
|
||||
## Structure of the json file for openDTU-onBattery (outdated example)
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "Generic NodeMCU 38 pin",
|
||||
"nrf24": {
|
||||
"miso": 19,
|
||||
"mosi": 23,
|
||||
"clk": 18,
|
||||
"irq": 16,
|
||||
"en": 4,
|
||||
"cs": 5
|
||||
},
|
||||
"victron": {
|
||||
"rx": 22,
|
||||
"tx": 21
|
||||
},
|
||||
"battery": {
|
||||
"rx": 27,
|
||||
"tx": 14
|
||||
},
|
||||
"huawei": {
|
||||
"miso": 12,
|
||||
"mosi": 13,
|
||||
"clk": 26,
|
||||
"irq": 25,
|
||||
"power": 33,
|
||||
"cs": 15
|
||||
},
|
||||
"eth": {
|
||||
"enabled": false,
|
||||
"phy_addr": -1,
|
||||
"power": -1,
|
||||
"mdc": -1,
|
||||
"mdio": -1,
|
||||
"type": -1,
|
||||
"clk_mode": -1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Generic NodeMCU 38 pin with SSD1306",
|
||||
"nrf24": {
|
||||
"miso": 19,
|
||||
"mosi": 23,
|
||||
"clk": 18,
|
||||
"irq": 16,
|
||||
"en": 4,
|
||||
"cs": 5
|
||||
},
|
||||
"eth": {
|
||||
"enabled": false,
|
||||
"phy_addr": -1,
|
||||
"power": -1,
|
||||
"mdc": -1,
|
||||
"mdio": -1,
|
||||
"type": -1,
|
||||
"clk_mode": -1
|
||||
},
|
||||
"display": {
|
||||
"type": 2,
|
||||
"data": 21,
|
||||
"clk": 22
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Olimex ESP32-POE",
|
||||
"nrf24": {
|
||||
"miso": 15,
|
||||
"mosi": 2,
|
||||
"clk": 14,
|
||||
"irq": 13,
|
||||
"en": 16,
|
||||
"cs": 5
|
||||
},
|
||||
"eth": {
|
||||
"enabled": true,
|
||||
"phy_addr": 0,
|
||||
"power": 12,
|
||||
"mdc": 23,
|
||||
"mdio": 18,
|
||||
"type": 0,
|
||||
"clk_mode": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
275
docs/PowerLimiterInverterStates.drawio
Normal file
@ -0,0 +1,275 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram name="Page-1" id="b5b7bab2-c9e2-2cf4-8b2a-24fd1a2a6d21">
|
||||
<mxGraphModel dx="1370" dy="985" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" background="none" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="6e0c8c40b5770093-72" value="" style="shape=folder;fontStyle=1;spacingTop=10;tabWidth=194;tabHeight=22;tabPosition=left;html=1;rounded=0;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fillColor=none;fontFamily=Verdana;fontSize=10;align=center;" parent="1" vertex="1">
|
||||
<mxGeometry x="150" y="114.5" width="1090" height="1065.5" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="6e0c8c40b5770093-73" value="<div style="color: rgb(212, 212, 212); background-color: rgb(30, 30, 30); font-family: Menlo, Monaco, &quot;Courier New&quot;, monospace; font-size: 12px; line-height: 18px;"><span style="color: #4ec9b0;">PowerLimiterClass</span>::<span style="color: #dcdcaa;">loop</span>()</div>" style="text;html=1;align=left;verticalAlign=top;spacingTop=-4;fontSize=10;fontFamily=Verdana" parent="1" vertex="1">
|
||||
<mxGeometry x="150" y="114.5" width="130" height="20" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="sqCMRMHiXPc9LqBIY9SA-1" value="" style="ellipse;html=1;shape=startState;fillColor=#000000;strokeColor=#ff0000;" parent="1" vertex="1">
|
||||
<mxGeometry x="475" y="50" width="30" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="sqCMRMHiXPc9LqBIY9SA-2" value="discover state on initial call or after enabling powerLimiter" style="html=1;verticalAlign=bottom;endArrow=open;endSize=8;strokeColor=#ff0000;rounded=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="sqCMRMHiXPc9LqBIY9SA-1" target="2" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="205" y="370" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="sqCMRMHiXPc9LqBIY9SA-6" value="<p style="margin:0px;margin-top:4px;text-align:center;">STATE_OFF</p><hr><p></p><p style="margin:0px;margin-left:8px;text-align:left;">entry / stop inverter, limit lower limit<br>do / nothing<br>exit / nothing</p>" style="shape=mxgraph.sysml.simpleState;html=1;overflow=fill;whiteSpace=wrap;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="170" y="585" width="200" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="sqCMRMHiXPc9LqBIY9SA-7" value="<p style="margin:0px;margin-top:4px;text-align:center;">STATE_CONSUME_SOLAR_POWER_ONLY</p><hr><p></p><p style="margin:0px;margin-left:8px;text-align:left;">entry /<br>do / setNewLimit<br>exit /</p>" style="shape=mxgraph.sysml.simpleState;html=1;overflow=fill;whiteSpace=wrap;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="525" y="590" width="270" height="90" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="sqCMRMHiXPc9LqBIY9SA-8" value="<p style="margin:0px;margin-top:4px;text-align:center;">STATE_NORMAL_OPERATION</p><hr><p></p><p style="margin:0px;margin-left:8px;text-align:left;">entry /<br>do / setNewLimit<br>exit /</p>" style="shape=mxgraph.sysml.simpleState;html=1;overflow=fill;whiteSpace=wrap;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="970" y="585" width="200" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="2" value="<p style="margin:0px;margin-top:4px;text-align:center;">STATE_DISOVER</p><hr><p></p><p style="margin:0px;margin-left:8px;text-align:left;">entry /&nbsp;<br>do / nothing<br>exit /&nbsp;</p>" style="shape=mxgraph.sysml.simpleState;html=1;overflow=fill;whiteSpace=wrap;align=center;fillColor=#f5f5f5;strokeColor=#666666;fontColor=#333333;" parent="1" vertex="1">
|
||||
<mxGeometry x="390" y="180" width="200" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="" style="shape=ellipse;html=1;dashed=0;whitespace=wrap;aspect=fixed;strokeWidth=5;perimeter=ellipsePerimeter;" parent="1" vertex="1">
|
||||
<mxGeometry x="414" y="780" width="30" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="7" value="<p><span style="font-size: 11px; background-color: rgb(255, 255, 255);">!Inverter-&gt;isProducing || isStopThresholdReached</span></p>" style="rhombus;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="425" y="300" width="130" height="130" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="10" value="<span style="font-size: 11px; background-color: rgb(255, 255, 255);">canUseDirectSolarPower</span>" style="rhombus;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="595" y="390" width="130" height="120" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="11" value="" style="endArrow=classic;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="10" target="sqCMRMHiXPc9LqBIY9SA-7" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="620" y="540" as="sourcePoint"/>
|
||||
<mxPoint x="670" y="490" as="targetPoint"/>
|
||||
<Array as="points"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="20" value="yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="11" vertex="1" connectable="0">
|
||||
<mxGeometry x="0.075" y="1" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="12" value="" style="endArrow=classic;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="2" target="7" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="484" y="280" as="sourcePoint"/>
|
||||
<mxPoint x="420" y="220" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="13" value="" style="endArrow=classic;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryPerimeter=0;" parent="1" source="7" target="sqCMRMHiXPc9LqBIY9SA-6" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="570" y="290" as="sourcePoint"/>
|
||||
<mxPoint x="400" y="330" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="270" y="365"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="14" value="yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="13" vertex="1" connectable="0">
|
||||
<mxGeometry x="0.3049" relative="1" as="geometry">
|
||||
<mxPoint x="80" y="-105" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="15" value="" style="endArrow=classic;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="7" target="10" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="360" y="370" as="sourcePoint"/>
|
||||
<mxPoint x="280" y="595" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="660" y="365"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="16" value="no" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="15" vertex="1" connectable="0">
|
||||
<mxGeometry x="0.3049" relative="1" as="geometry">
|
||||
<mxPoint x="-45" y="-15" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="18" value="" style="endArrow=classic;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;edgeStyle=orthogonalEdgeStyle;" parent="1" source="10" target="sqCMRMHiXPc9LqBIY9SA-8" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="760" y="340" as="sourcePoint"/>
|
||||
<mxPoint x="810" y="290" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="19" value="no" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="18" vertex="1" connectable="0">
|
||||
<mxGeometry x="0.142" y="2" relative="1" as="geometry">
|
||||
<mxPoint x="-239" y="-8" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="21" value="isStopThresholdReached" style="rhombus;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="220" y="730" width="130" height="130" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="22" value="" style="endArrow=classic;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="21" target="24" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="620" y="540" as="sourcePoint"/>
|
||||
<mxPoint x="670" y="490" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="285" y="995"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="31" value="no" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="22" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.702" y="1" relative="1" as="geometry">
|
||||
<mxPoint x="39" y="91" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="23" value="" style="endArrow=classic;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.581;exitY=0.998;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="sqCMRMHiXPc9LqBIY9SA-6" target="21" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="480" y="810" as="sourcePoint"/>
|
||||
<mxPoint x="530" y="760" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="41" style="edgeStyle=none;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="24" target="25">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="42" value="no" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="41">
|
||||
<mxGeometry x="0.36" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="24" value="canUseDirectSolarPower" style="rhombus;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="600" y="930" width="130" height="130" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="25" value="isStartThresholdReached" style="rhombus;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="830" y="930" width="130" height="130" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="29" value="" style="endArrow=classic;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="21" target="5" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="350" y="830" as="sourcePoint"/>
|
||||
<mxPoint x="400" y="780" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="30" value="yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="29" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.1" relative="1" as="geometry">
|
||||
<mxPoint x="3" y="-15" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="JKiNQljIdbqBsyxyxwz1-38" value="" style="endArrow=classic;html=1;rounded=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="sqCMRMHiXPc9LqBIY9SA-7" target="sqCMRMHiXPc9LqBIY9SA-6" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="570" y="820" as="sourcePoint"/>
|
||||
<mxPoint x="620" y="770" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="JKiNQljIdbqBsyxyxwz1-39" value="isStopThresholdReached ||<br>(!canUseDirectSolarPower <br>&amp;&amp; EMPTY_WHEN_FULL)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="JKiNQljIdbqBsyxyxwz1-38" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.2129" y="-2" relative="1" as="geometry">
|
||||
<mxPoint x="-14" y="-33" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="JKiNQljIdbqBsyxyxwz1-40" value="isStartThresholdReached ||<br style="border-color: var(--border-color);">(!canUseDirectSolarPower<br style="border-color: var(--border-color);">&amp;&amp; EMPTY_AT_NIGHT)" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="sqCMRMHiXPc9LqBIY9SA-7" target="sqCMRMHiXPc9LqBIY9SA-8" edge="1">
|
||||
<mxGeometry x="-0.0286" y="25" width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="570" y="560" as="sourcePoint"/>
|
||||
<mxPoint x="620" y="510" as="targetPoint"/>
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="JKiNQljIdbqBsyxyxwz1-41" value="" style="endArrow=classic;html=1;rounded=0;entryX=0.22;entryY=1;entryDx=0;entryDy=0;entryPerimeter=0;exitX=0.354;exitY=1.025;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="sqCMRMHiXPc9LqBIY9SA-8" target="sqCMRMHiXPc9LqBIY9SA-6" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1040" y="680" as="sourcePoint"/>
|
||||
<mxPoint x="890" y="750" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="1040" y="1070"/>
|
||||
<mxPoint x="214" y="1070"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="JKiNQljIdbqBsyxyxwz1-42" value="isStopThresholdReached ||" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="JKiNQljIdbqBsyxyxwz1-41" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.2129" y="-2" relative="1" as="geometry">
|
||||
<mxPoint x="-151" y="32" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="JKiNQljIdbqBsyxyxwz1-43" value="" style="shape=ellipse;html=1;dashed=0;whitespace=wrap;aspect=fixed;strokeWidth=5;perimeter=ellipsePerimeter;" parent="1" vertex="1">
|
||||
<mxGeometry x="1055" y="799.7" width="30" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="JKiNQljIdbqBsyxyxwz1-44" value="" style="endArrow=classic;html=1;exitX=0.82;exitY=1.003;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitPerimeter=0;" parent="1" target="JKiNQljIdbqBsyxyxwz1-43" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1069.0000000000002" y="685" as="sourcePoint"/>
|
||||
<mxPoint x="1119" y="749.7" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="JKiNQljIdbqBsyxyxwz1-45" value="inTargetRange do nothing" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="JKiNQljIdbqBsyxyxwz1-44" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.1" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="JKiNQljIdbqBsyxyxwz1-46" value="" style="shape=ellipse;html=1;dashed=0;whitespace=wrap;aspect=fixed;strokeWidth=5;perimeter=ellipsePerimeter;" parent="1" vertex="1">
|
||||
<mxGeometry x="530" y="795" width="30" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="JKiNQljIdbqBsyxyxwz1-47" value="" style="endArrow=classic;html=1;exitX=0.82;exitY=1.003;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitPerimeter=0;" parent="1" target="JKiNQljIdbqBsyxyxwz1-46" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="544" y="680" as="sourcePoint"/>
|
||||
<mxPoint x="594" y="745" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="JKiNQljIdbqBsyxyxwz1-48" value="after setNewLimit" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="JKiNQljIdbqBsyxyxwz1-47" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.1" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="JKiNQljIdbqBsyxyxwz1-49" value="" style="shape=ellipse;html=1;dashed=0;whitespace=wrap;aspect=fixed;strokeWidth=5;perimeter=ellipsePerimeter;" parent="1" vertex="1">
|
||||
<mxGeometry x="1110" y="800" width="30" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="JKiNQljIdbqBsyxyxwz1-50" value="" style="endArrow=classic;html=1;exitX=0.82;exitY=1.003;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitPerimeter=0;" parent="1" target="JKiNQljIdbqBsyxyxwz1-49" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1124.0000000000002" y="685.3" as="sourcePoint"/>
|
||||
<mxPoint x="1174" y="750" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="JKiNQljIdbqBsyxyxwz1-51" value="after setNewLimit" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="JKiNQljIdbqBsyxyxwz1-50" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.1" relative="1" as="geometry">
|
||||
<mxPoint x="16" y="33" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="38" value="" style="endArrow=classic;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="24" target="sqCMRMHiXPc9LqBIY9SA-7">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="580" y="710" as="sourcePoint"/>
|
||||
<mxPoint x="630" y="660" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="39" value="yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="38">
|
||||
<mxGeometry x="0.104" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="40" value="" style="endArrow=classic;html=1;entryX=1;entryY=0.75;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" target="sqCMRMHiXPc9LqBIY9SA-7">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="970" y="658" as="sourcePoint"/>
|
||||
<mxPoint x="800" y="660" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="48" value="canUseDirectSolarPower<br style="border-color: var(--border-color);">&amp;&amp; EMPTY_AT_NIGHT" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="40">
|
||||
<mxGeometry x="-0.04" y="1" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="43" value="" style="endArrow=classic;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="25" target="sqCMRMHiXPc9LqBIY9SA-8">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="960" y="980" as="sourcePoint"/>
|
||||
<mxPoint x="1010" y="930" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="1020" y="995"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="44" value="yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="43">
|
||||
<mxGeometry x="0.1135" y="2" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="45" value="" style="shape=ellipse;html=1;dashed=0;whitespace=wrap;aspect=fixed;strokeWidth=5;perimeter=ellipsePerimeter;" vertex="1" parent="1">
|
||||
<mxGeometry x="880" y="850" width="30" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="46" value="" style="endArrow=classic;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="25" target="45">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="580" y="710" as="sourcePoint"/>
|
||||
<mxPoint x="630" y="660" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="47" value="no" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="46">
|
||||
<mxGeometry x="-0.08" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
BIN
docs/PowerLimiterInverterStates.png
Normal file
|
After Width: | Height: | Size: 147 KiB |
@ -1,3 +1,44 @@
|
||||
# Web API
|
||||
|
||||
This documentation will has been moved and can be found here: <https://tbnobody.github.io/OpenDTU-docs/firmware/web_api/>
|
||||
|
||||
## List of URLs
|
||||
|
||||
This list may be incomplete
|
||||
|
||||
| GET/POST | Auth required | URL |
|
||||
| -------- | --- | -- |
|
||||
| Get | no | /api/vedirectlivedata/status |
|
||||
| Get | no | /api/vedirect/status |
|
||||
| Get | no | /api/huawei/status |
|
||||
| Get | no | /api/huawei/config |
|
||||
| Get | no | /api/huawei/limit/config |
|
||||
| Get | no | /api/batterylivedata/status |
|
||||
| Get | no | /api/battery/status |
|
||||
| Get | no | /api/powerlimiter/status |
|
||||
|
||||
### Victron REST-API (/api/vedirectlivedata/status):
|
||||
````JSON
|
||||
{
|
||||
"data_age":0,
|
||||
"age_critical":false,
|
||||
"PID":"SmartSolar MPPT 100|30",
|
||||
"SER":"XXX",
|
||||
"FW":"159",
|
||||
"LOAD":"ON",
|
||||
"CS":"Bulk",
|
||||
"ERR":"No error",
|
||||
"OR":"Not off",
|
||||
"MPPT":"MPP Tracker active",
|
||||
"HSDS":{"v":46,"u":"Days"},
|
||||
"V":{"v":26.36,"u":"V"},
|
||||
"I":{"v":3.4,"u":"A"},
|
||||
"VPV":{"v":37.13,"u":"V"},
|
||||
"PPV":{"v":93,"u":"W"},
|
||||
"H19":{"v":83.16,"u":"kWh"},
|
||||
"H20":{"v":1.39,"u":"kWh"},
|
||||
"H21":{"v":719,"u":"W"},
|
||||
"H22":{"v":1.43,"u":"kWh"},
|
||||
"H23":{"v":737,"u":"W"}
|
||||
}
|
||||
````
|
||||
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 261 KiB |
194
docs/components.drawio.svg
Normal file
|
After Width: | Height: | Size: 3.9 MiB |
191
docs/hardware_flash.md
Normal file
@ -0,0 +1,191 @@
|
||||
# Hardware, building and flashing Firmware
|
||||
|
||||
## Hardware you need
|
||||
|
||||
### ESP32 board
|
||||
|
||||
For ease of use, buy a "ESP32 DEVKIT DOIT" or "ESP32 NodeMCU Development Board" with an ESP32-S3 or ESP-WROOM-32 chipset on it.
|
||||
|
||||
Sample Picture:
|
||||
|
||||

|
||||
|
||||
Also supported: Board with Ethernet-Connector and Power-over-Ethernet [Olimex ESP32-POE](https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware)
|
||||
|
||||
### NRF24L01+ radio board (See inverter table above for supported inverters)
|
||||
|
||||
The PLUS sign is IMPORTANT! There are different variants available, with antenna on the printed circuit board or external antenna.
|
||||
|
||||
Sample picture:
|
||||
|
||||

|
||||
|
||||
Buy your hardware from a trusted source, at best from a dealer/online shop in your country where you have support and the right to return non-functional hardware.
|
||||
When you want to buy from Amazon, AliExpress, eBay etc., take note that there is a lot of low-quality or fake hardware offered. Read customer comments and ratings carefully!
|
||||
|
||||
A heavily incomplete list of trusted hardware shops in Germany is:
|
||||
|
||||
* [AZ-Delivery](https://www.az-delivery.de/)
|
||||
* [Makershop](https://www.makershop.de/)
|
||||
* [Berrybase](https://www.berrybase.de/)
|
||||
|
||||
This list is for your convenience only, the project is not related to any of these shops.
|
||||
|
||||
### CMT2300A radio board (See inverter table above for supported inverters)
|
||||
|
||||
It is important to get a module which supports SPI communication. The following modules are currently supported:
|
||||
|
||||
* EBYTE E49-900M20S
|
||||
|
||||
The CMT2300A uses 3-Wire half duplex SPI communication. Due to this fact it currently requires a separate SPI bus. If you want to run the CMT2300A module on the same ESP32 as a NRF24L01+ module or a PCD8544 display make sure you get a ESP which supports 2 SPI busses. Currently the SPI bus host is hardcoded to number 2. This may change in future.
|
||||
|
||||
### 3.3V / 5V logic level converter
|
||||
|
||||
The logic level converter is used to interface with the Victron MPPT charge controller and the relay board. It converts the 3.3V logic level used by the ESP32 to 5V logic used by the other devices.
|
||||
|
||||
### SN65HVD230 CAN bus transceiver
|
||||
|
||||
The SN65HVD230 CAN bus transceiver is used to interface with the Pylontech battery. It leverages the CAN bus controller of the ESP32. This CAN bus operates at 500kbit/s
|
||||
|
||||
### MCP2515 CAN bus module
|
||||
|
||||
The MCP2515 CAN bus module consists of a CAN bus controller and a CAN bus transceiver and is used to interface with the Huawei AC charger. This CAN bus operates at 125kbit/s. The module is connected via SPI and currently requires a separate SPI bus. If you want to use the Huawei AC charger make sure to get an ESP which supports 2 SPI busses. Currently the SPI bus host is hardcoded to number 2. This may change in future. Please note: Using the Huawei AC charger in combination with the CMT2300A radio board is not supported at the moment.
|
||||
|
||||
MCP2515 CAN bus modules that are widely available are designed for 5V supply voltage. To make them work with 3.3V / the ESP32 a modification is required. [This modification is described here.](https://forums.raspberrypi.com/viewtopic.php?t=141052)
|
||||
|
||||
### Relay module
|
||||
|
||||
The Huawei PSU can be switched on / off using the slot detect port. This is done by this relay.
|
||||
|
||||
### Power supply
|
||||
|
||||
Use a power supply with 5 V and 1 A. The USB cable connected to your PC/Notebook may be powerful enough or may be not.
|
||||
|
||||
## Wiring up the NRF24L01+ module
|
||||
|
||||
### Schematic
|
||||
|
||||

|
||||
|
||||
### Symbolic view
|
||||
|
||||

|
||||
|
||||
### Change pin assignment
|
||||
|
||||
Its possible to change all the pins of the NRF24L01+ module, the Display, the LED etc.
|
||||
The recommend way to change the pin assignment is by creating a custom [device profile](DeviceProfiles.md).
|
||||
It is also possible to create a custom environment and compile the source yourself. This can be achieved by copying one of the [env:....] sections from 'platformio.ini' to 'platformio_override.ini' and editing the 'platformio_override.ini' file and add/change one or more of the following lines to the 'build_flags' parameter:
|
||||
|
||||
```makefile
|
||||
-DHOYMILES_PIN_MISO=19
|
||||
-DHOYMILES_PIN_MOSI=23
|
||||
-DHOYMILES_PIN_SCLK=18
|
||||
-DHOYMILES_PIN_IRQ=16
|
||||
-DHOYMILES_PIN_CE=4
|
||||
-DHOYMILES_PIN_CS=5
|
||||
-DVICTRON_PIN_TX=21
|
||||
-DVICTRON_PIN_RX=22
|
||||
-DPYLONTECH_PIN_RX=27
|
||||
-DPYLONTECH_PIN_TX=14
|
||||
-DHUAWEI_PIN_MISO=12
|
||||
-DHUAWEI_PIN_MOSI=13
|
||||
-DHUAWEI_PIN_SCLK=26
|
||||
-DHUAWEI_PIN_IRQ=25
|
||||
-DHUAWEI_PIN_CS=15
|
||||
-DHUAWEI_PIN_POWER=33
|
||||
```
|
||||
|
||||
It is recommended to make all changes only in the 'platformio_override.ini', this is your personal copy.
|
||||
|
||||
## Flashing and starting up
|
||||
|
||||
### with Visual Studio Code
|
||||
|
||||
* Install [Visual Studio Code](https://code.visualstudio.com/download) (from now named "vscode")
|
||||
* In Visual Studio Code, install the [PlatformIO Extension](https://marketplace.visualstudio.com/items?itemName=platformio.platformio-ide)
|
||||
* Install git and enable git in vscode - [git download](https://git-scm.com/downloads/) - [Instructions](https://www.jcchouinard.com/install-git-in-vscode/)
|
||||
* Clone this repository (you really have to clone it, don't just download the ZIP file. During the build process the git hash gets embedded into the firmware. If you download the ZIP file a build error will occur): Inside vscode open the command palette by pressing `CTRL` + `SHIFT` + `P`. Enter `git clone`, add the repository-URL `https://github.com/tbnobody/OpenDTU`. Next you have to choose (or create) a target directory.
|
||||
* In vscode, choose File --> Open Folder and select the previously downloaded source code. (You have to select the folder which contains the "platformio.ini" and "platformio_override.ini" file)
|
||||
* Adjust the COM port in the file "platformio_override.ini" for your USB-to-serial-converter. It occurs twice:
|
||||
* upload_port
|
||||
* monitor_port
|
||||
* Select the arrow button in the blue bottom status bar (PlatformIO: Upload) to compile and upload the firmware. During the compilation, all required libraries are downloaded automatically.
|
||||
* Under Linux, if the upload fails with error messages "Could not open /dev/ttyUSB0, the port doesn't exist", you can check via ```ls -la /dev/tty*``` to which group your port belongs to, and then add your user this group via ```sudo adduser <yourusername> dialout``` (if you are using ```arch-linux``` use: ```sudo gpasswd -a <yourusername> uucp```, this method requires a logout/login of the affected user).
|
||||
* There are two videos showing these steps:
|
||||
* [Git Clone and compilation](https://youtu.be/9cA_esv3zeA)
|
||||
* [Full installation and compilation](https://youtu.be/xs6TqHn7QWM)
|
||||
|
||||
### on the commandline with PlatformIO Core
|
||||
|
||||
* Install [PlatformIO Core](https://platformio.org/install/cli)
|
||||
* Clone this repository (you really have to clone it, don't just download the ZIP file. During the build process the git hash gets embedded into the firmware. If you download the ZIP file a build error will occur)
|
||||
* Adjust the COM port in the file "platformio_override.ini". It occurs twice:
|
||||
* upload_port
|
||||
* monitor_port
|
||||
* build: `platformio run -e generic`
|
||||
* upload to esp module: `platformio run -e generic -t upload`
|
||||
* other options:
|
||||
* clean the sources: `platformio run -e generic -t clean`
|
||||
* erase flash: `platformio run -e generic -t erase`
|
||||
|
||||
### using the pre-compiled .bin files
|
||||
|
||||
The pre-compiled files can be found on the [github page](https://github.com/tbnobody/OpenDTU) in the tab "Actions" and the sub menu "OpenDTU Build". Just choose the latest build from the master branch (search for "master" in the blue font text but click on the white header text!). You need to be logged in with your github account to download the files.
|
||||
Use a ESP32 flash tool of your choice (see next chapter) and flash the `.bin` files to the right addresses:
|
||||
|
||||
| Address | File |
|
||||
| ---------| ---------------------- |
|
||||
| 0x1000 | bootloader.bin |
|
||||
| 0x8000 | partitions.bin |
|
||||
| 0xe000 | boot_app0.bin |
|
||||
| 0x10000 | opendtu-*.bin |
|
||||
|
||||
For further updates you can just use the web interface and upload the `opendtu-*.bin` file.
|
||||
|
||||
#### Flash with esptool.py (Linux)
|
||||
|
||||
```bash
|
||||
esptool.py --port /dev/ttyUSB0 --chip esp32 --before default_reset --after hard_reset \
|
||||
write_flash --flash_mode dout --flash_freq 40m --flash_size detect \
|
||||
0x1000 bootloader.bin \
|
||||
0x8000 partitions.bin \
|
||||
0xe000 boot_app0.bin \
|
||||
0x10000 opendtu-generic.bin
|
||||
```
|
||||
|
||||
#### Flash with Espressif Flash Download Tool (Windows)
|
||||
|
||||
[Download link](https://www.espressif.com/en/support/download/other-tools)
|
||||
|
||||
* On startup, select Chip Type -> "ESP32" / WorkMode -> "Develop"
|
||||
* Prepare all settings (see picture). Make sure to uncheck the `DoNotChgBin` option. Otherwise you may get errors like "invalid header".
|
||||
* 
|
||||
* Press "Erase" button on screen. Look into the terminal window, you should see dots appear. Then press the "Boot" button on the ESP32 board. Wait for "FINISH" to see if flashing/erasing is done.
|
||||
* To program, press "Start" on screen, then the "Boot" button.
|
||||
* When flashing is complete (FINISH appears) then press the Reset button on the ESP32 board (or powercycle ) to start the OpenDTU application.
|
||||
|
||||
#### Flash with ESP_Flasher (Windows)
|
||||
|
||||
Users report that [ESP_Flasher](https://github.com/Jason2866/ESP_Flasher/releases/) is suitable for flashing OpenDTU on Windows.
|
||||
|
||||
#### Flash with [ESP_Flasher](https://espressif.github.io/esptool-js/) - web version
|
||||
|
||||
It is also possible to flash it via the web tools which might be more convenient and is platform independent.
|
||||
|
||||
## Flashing an Update using "Over The Air" OTA Update
|
||||
|
||||
Once you have your OpenDTU running and connected to WLAN, you can do further updates through the web interface.
|
||||
Navigate to Settings --> Firmware upgrade and press the browse button. Select the firmware file from your local computer.
|
||||
|
||||
You'll find the firmware file (after a successful build process) under `.pio/build/generic/firmware.bin`.
|
||||
|
||||
If you downloaded a precompiled zip archive, unpack it and choose `opendtu-generic.bin`.
|
||||
|
||||
After the successful upload, the OpenDTU immediately restarts into the new firmware.
|
||||
|
||||
|
||||
## Builds
|
||||
|
||||
Different builds from existing installations can be found here [Builds](builds/README.md)
|
||||
Like to show your own build? Just send me a Pull Request.
|
||||
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 146 KiB |
37
include/Battery.h
Normal file
@ -0,0 +1,37 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
|
||||
#include "BatteryStats.h"
|
||||
|
||||
class BatteryProvider {
|
||||
public:
|
||||
// returns true if the provider is ready for use, false otherwise
|
||||
virtual bool init(bool verboseLogging) = 0;
|
||||
|
||||
virtual void deinit() = 0;
|
||||
virtual void loop() = 0;
|
||||
virtual std::shared_ptr<BatteryStats> getStats() const = 0;
|
||||
};
|
||||
|
||||
class BatteryClass {
|
||||
public:
|
||||
void init(Scheduler&);
|
||||
void updateSettings();
|
||||
|
||||
std::shared_ptr<BatteryStats const> getStats() const;
|
||||
private:
|
||||
void loop();
|
||||
|
||||
Task _loopTask;
|
||||
|
||||
uint32_t _lastMqttPublish = 0;
|
||||
mutable std::mutex _mutex;
|
||||
std::unique_ptr<BatteryProvider> _upProvider = nullptr;
|
||||
};
|
||||
|
||||
extern BatteryClass Battery;
|
||||
143
include/BatteryStats.h
Normal file
@ -0,0 +1,143 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "AsyncJson.h"
|
||||
#include "Arduino.h"
|
||||
#include "JkBmsDataPoints.h"
|
||||
#include "VeDirectShuntController.h"
|
||||
|
||||
// mandatory interface for all kinds of batteries
|
||||
class BatteryStats {
|
||||
public:
|
||||
String const& getManufacturer() const { return _manufacturer; }
|
||||
|
||||
// the last time *any* datum was updated
|
||||
uint32_t getAgeSeconds() const { return (millis() - _lastUpdate) / 1000; }
|
||||
bool updateAvailable(uint32_t since) const { return _lastUpdate > since; }
|
||||
|
||||
uint8_t getSoC() const { return _SoC; }
|
||||
uint32_t getSoCAgeSeconds() const { return (millis() - _lastUpdateSoC) / 1000; }
|
||||
|
||||
// convert stats to JSON for web application live view
|
||||
virtual void getLiveViewData(JsonVariant& root) const;
|
||||
|
||||
virtual void mqttPublish() const;
|
||||
|
||||
bool isValid() const { return _lastUpdateSoC > 0 && _lastUpdate > 0; }
|
||||
|
||||
protected:
|
||||
String _manufacturer = "unknown";
|
||||
uint8_t _SoC = 0;
|
||||
uint32_t _lastUpdateSoC = 0;
|
||||
uint32_t _lastUpdate = 0;
|
||||
};
|
||||
|
||||
class PylontechBatteryStats : public BatteryStats {
|
||||
friend class PylontechCanReceiver;
|
||||
|
||||
public:
|
||||
void getLiveViewData(JsonVariant& root) const final;
|
||||
void mqttPublish() const final;
|
||||
|
||||
private:
|
||||
void setManufacturer(String&& m) { _manufacturer = std::move(m); }
|
||||
void setSoC(uint8_t SoC) { _SoC = SoC; _lastUpdateSoC = millis(); }
|
||||
void setLastUpdate(uint32_t ts) { _lastUpdate = ts; }
|
||||
|
||||
float _chargeVoltage;
|
||||
float _chargeCurrentLimitation;
|
||||
float _dischargeCurrentLimitation;
|
||||
uint16_t _stateOfHealth;
|
||||
float _voltage; // total voltage of the battery pack
|
||||
// total current into (positive) or from (negative)
|
||||
// the battery, i.e., the charging current
|
||||
float _current;
|
||||
float _temperature;
|
||||
|
||||
bool _alarmOverCurrentDischarge;
|
||||
bool _alarmOverCurrentCharge;
|
||||
bool _alarmUnderTemperature;
|
||||
bool _alarmOverTemperature;
|
||||
bool _alarmUnderVoltage;
|
||||
bool _alarmOverVoltage;
|
||||
bool _alarmBmsInternal;
|
||||
|
||||
bool _warningHighCurrentDischarge;
|
||||
bool _warningHighCurrentCharge;
|
||||
bool _warningLowTemperature;
|
||||
bool _warningHighTemperature;
|
||||
bool _warningLowVoltage;
|
||||
bool _warningHighVoltage;
|
||||
bool _warningBmsInternal;
|
||||
|
||||
bool _chargeEnabled;
|
||||
bool _dischargeEnabled;
|
||||
bool _chargeImmediately;
|
||||
};
|
||||
|
||||
class JkBmsBatteryStats : public BatteryStats {
|
||||
public:
|
||||
void getLiveViewData(JsonVariant& root) const final {
|
||||
getJsonData(root, false);
|
||||
}
|
||||
|
||||
void getInfoViewData(JsonVariant& root) const {
|
||||
getJsonData(root, true);
|
||||
}
|
||||
|
||||
void mqttPublish() const final;
|
||||
|
||||
void updateFrom(JkBms::DataPointContainer const& dp);
|
||||
|
||||
private:
|
||||
void getJsonData(JsonVariant& root, bool verbose) const;
|
||||
|
||||
JkBms::DataPointContainer _dataPoints;
|
||||
mutable uint32_t _lastMqttPublish = 0;
|
||||
mutable uint32_t _lastFullMqttPublish = 0;
|
||||
|
||||
uint16_t _cellMinMilliVolt = 0;
|
||||
uint16_t _cellAvgMilliVolt = 0;
|
||||
uint16_t _cellMaxMilliVolt = 0;
|
||||
uint32_t _cellVoltageTimestamp = 0;
|
||||
};
|
||||
|
||||
class VictronSmartShuntStats : public BatteryStats {
|
||||
public:
|
||||
void getLiveViewData(JsonVariant& root) const final;
|
||||
void mqttPublish() const final;
|
||||
|
||||
void updateFrom(VeDirectShuntController::veShuntStruct const& shuntData);
|
||||
|
||||
private:
|
||||
float _voltage;
|
||||
float _current;
|
||||
float _temperature;
|
||||
bool _tempPresent;
|
||||
uint8_t _chargeCycles;
|
||||
uint32_t _timeToGo;
|
||||
float _chargedEnergy;
|
||||
float _dischargedEnergy;
|
||||
String _modelName;
|
||||
|
||||
bool _alarmLowVoltage;
|
||||
bool _alarmHighVoltage;
|
||||
bool _alarmLowSOC;
|
||||
bool _alarmLowTemperature;
|
||||
bool _alarmHighTemperature;
|
||||
};
|
||||
|
||||
class MqttBatteryStats : public BatteryStats {
|
||||
public:
|
||||
// since the source of information was MQTT in the first place,
|
||||
// we do NOT publish the same data under a different topic.
|
||||
void mqttPublish() const final { }
|
||||
|
||||
// the SoC is the only interesting value in this case, which is already
|
||||
// displayed at the top of the live view. do not generate a card.
|
||||
void getLiveViewData(JsonVariant& root) const final { }
|
||||
|
||||
void setSoC(uint8_t SoC) { _SoC = SoC; _lastUpdateSoC = _lastUpdate = millis(); }
|
||||
};
|
||||
@ -18,19 +18,28 @@
|
||||
#define MQTT_MAX_HOSTNAME_STRLEN 128
|
||||
#define MQTT_MAX_USERNAME_STRLEN 64
|
||||
#define MQTT_MAX_PASSWORD_STRLEN 64
|
||||
#define MQTT_MAX_TOPIC_STRLEN 32
|
||||
#define MQTT_MAX_TOPIC_STRLEN 256
|
||||
#define MQTT_MAX_LWTVALUE_STRLEN 20
|
||||
#define MQTT_MAX_CERT_STRLEN 2560
|
||||
|
||||
#define INV_MAX_NAME_STRLEN 31
|
||||
#define INV_MAX_COUNT 10
|
||||
#define INV_MAX_COUNT 5
|
||||
#define INV_MAX_CHAN_COUNT 6
|
||||
|
||||
#define CHAN_MAX_NAME_STRLEN 31
|
||||
|
||||
#define DEV_MAX_MAPPING_NAME_STRLEN 63
|
||||
|
||||
#define JSON_BUFFER_SIZE 12288
|
||||
#define POWERMETER_MAX_PHASES 3
|
||||
#define POWERMETER_MAX_HTTP_URL_STRLEN 1024
|
||||
#define POWERMETER_MAX_USERNAME_STRLEN 64
|
||||
#define POWERMETER_MAX_PASSWORD_STRLEN 64
|
||||
#define POWERMETER_MAX_HTTP_HEADER_KEY_STRLEN 64
|
||||
#define POWERMETER_MAX_HTTP_HEADER_VALUE_STRLEN 256
|
||||
#define POWERMETER_MAX_HTTP_JSON_PATH_STRLEN 256
|
||||
#define POWERMETER_HTTP_TIMEOUT 1000
|
||||
|
||||
#define JSON_BUFFER_SIZE 15360
|
||||
|
||||
struct CHANNEL_CONFIG_T {
|
||||
uint16_t MaxChannelPower;
|
||||
@ -53,6 +62,19 @@ struct INVERTER_CONFIG_T {
|
||||
CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT];
|
||||
};
|
||||
|
||||
enum Auth { none, basic, digest };
|
||||
struct POWERMETER_HTTP_PHASE_CONFIG_T {
|
||||
bool Enabled;
|
||||
char Url[POWERMETER_MAX_HTTP_URL_STRLEN + 1];
|
||||
Auth AuthType;
|
||||
char Username[POWERMETER_MAX_USERNAME_STRLEN +1];
|
||||
char Password[POWERMETER_MAX_USERNAME_STRLEN +1];
|
||||
char HeaderKey[POWERMETER_MAX_HTTP_HEADER_KEY_STRLEN + 1];
|
||||
char HeaderValue[POWERMETER_MAX_HTTP_HEADER_VALUE_STRLEN + 1];
|
||||
uint16_t Timeout;
|
||||
char JsonPath[POWERMETER_MAX_HTTP_JSON_PATH_STRLEN + 1];
|
||||
};
|
||||
|
||||
struct CONFIG_T {
|
||||
struct {
|
||||
uint32_t Version;
|
||||
@ -88,6 +110,7 @@ struct CONFIG_T {
|
||||
struct {
|
||||
bool Enabled;
|
||||
char Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1];
|
||||
bool VerboseLogging;
|
||||
uint32_t Port;
|
||||
char Username[MQTT_MAX_USERNAME_STRLEN + 1];
|
||||
char Password[MQTT_MAX_PASSWORD_STRLEN + 1];
|
||||
@ -131,6 +154,7 @@ struct CONFIG_T {
|
||||
uint32_t Frequency;
|
||||
uint8_t CountryMode;
|
||||
} Cmt;
|
||||
bool VerboseLogging;
|
||||
} Dtu;
|
||||
|
||||
struct {
|
||||
@ -154,6 +178,72 @@ struct CONFIG_T {
|
||||
uint8_t Brightness;
|
||||
} Led_Single[PINMAPPING_LED_COUNT];
|
||||
|
||||
struct {
|
||||
bool Enabled;
|
||||
bool VerboseLogging;
|
||||
bool UpdatesOnly;
|
||||
} Vedirect;
|
||||
|
||||
struct {
|
||||
bool Enabled;
|
||||
bool VerboseLogging;
|
||||
uint32_t Interval;
|
||||
uint32_t Source;
|
||||
char MqttTopicPowerMeter1[MQTT_MAX_TOPIC_STRLEN + 1];
|
||||
char MqttTopicPowerMeter2[MQTT_MAX_TOPIC_STRLEN + 1];
|
||||
char MqttTopicPowerMeter3[MQTT_MAX_TOPIC_STRLEN + 1];
|
||||
uint32_t SdmBaudrate;
|
||||
uint32_t SdmAddress;
|
||||
uint32_t HttpInterval;
|
||||
bool HttpIndividualRequests;
|
||||
POWERMETER_HTTP_PHASE_CONFIG_T Http_Phase[POWERMETER_MAX_PHASES];
|
||||
} PowerMeter;
|
||||
|
||||
struct {
|
||||
bool Enabled;
|
||||
bool VerboseLogging;
|
||||
bool SolarPassThroughEnabled;
|
||||
uint8_t SolarPassThroughLosses;
|
||||
uint8_t BatteryDrainStategy;
|
||||
uint32_t Interval;
|
||||
bool IsInverterBehindPowerMeter;
|
||||
uint8_t InverterId;
|
||||
uint8_t InverterChannelId;
|
||||
int32_t TargetPowerConsumption;
|
||||
int32_t TargetPowerConsumptionHysteresis;
|
||||
int32_t LowerPowerLimit;
|
||||
int32_t UpperPowerLimit;
|
||||
uint32_t BatterySocStartThreshold;
|
||||
uint32_t BatterySocStopThreshold;
|
||||
float VoltageStartThreshold;
|
||||
float VoltageStopThreshold;
|
||||
float VoltageLoadCorrectionFactor;
|
||||
int8_t RestartHour;
|
||||
uint32_t FullSolarPassThroughSoc;
|
||||
float FullSolarPassThroughStartVoltage;
|
||||
float FullSolarPassThroughStopVoltage;
|
||||
} PowerLimiter;
|
||||
|
||||
struct {
|
||||
bool Enabled;
|
||||
bool VerboseLogging;
|
||||
uint8_t Provider;
|
||||
uint8_t JkBmsInterface;
|
||||
uint8_t JkBmsPollingInterval;
|
||||
char MqttTopic[MQTT_MAX_TOPIC_STRLEN + 1];
|
||||
} Battery;
|
||||
|
||||
struct {
|
||||
bool Enabled;
|
||||
uint32_t CAN_Controller_Frequency;
|
||||
bool Auto_Power_Enabled;
|
||||
float Auto_Power_Voltage_Limit;
|
||||
float Auto_Power_Enable_Voltage_Limit;
|
||||
float Auto_Power_Lower_Power_Limit;
|
||||
float Auto_Power_Upper_Power_Limit;
|
||||
} Huawei;
|
||||
|
||||
|
||||
INVERTER_CONFIG_T Inverter[INV_MAX_COUNT];
|
||||
char Dev_PinMapping[DEV_MAX_MAPPING_NAME_STRLEN + 1];
|
||||
};
|
||||
|
||||
33
include/HttpPowerMeter.h
Normal file
@ -0,0 +1,33 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <Arduino.h>
|
||||
#include <HTTPClient.h>
|
||||
|
||||
class HttpPowerMeterClass {
|
||||
public:
|
||||
void init();
|
||||
bool updateValues();
|
||||
float getPower(int8_t phase);
|
||||
char httpPowerMeterError[256];
|
||||
bool queryPhase(int phase, const String& url, Auth authType, const char* username, const char* password,
|
||||
const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath);
|
||||
|
||||
|
||||
private:
|
||||
float power[POWERMETER_MAX_PHASES];
|
||||
HTTPClient httpClient;
|
||||
String httpResponse;
|
||||
bool httpRequest(int phase, WiFiClient &wifiClient, const String& host, const String& uri, bool https, Auth authType, const char* username,
|
||||
const char* password, const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath);
|
||||
void extractUrlComponents(const String& url, String& protocol, String& host, String& uri);
|
||||
String extractParam(String& authReq, const String& param, const char delimit);
|
||||
String getcNonce(const int len);
|
||||
String getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter);
|
||||
bool tryGetFloatValueForPhase(int phase, int httpCode, const char* jsonPath);
|
||||
void prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue);
|
||||
String sha256(const String& data);
|
||||
};
|
||||
|
||||
extern HttpPowerMeterClass HttpPowerMeter;
|
||||
156
include/Huawei_can.h
Normal file
@ -0,0 +1,156 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include "SPI.h"
|
||||
#include <mcp_can.h>
|
||||
#include <mutex>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
|
||||
#ifndef HUAWEI_PIN_MISO
|
||||
#define HUAWEI_PIN_MISO 12
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_MOSI
|
||||
#define HUAWEI_PIN_MOSI 13
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_SCLK
|
||||
#define HUAWEI_PIN_SCLK 26
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_IRQ
|
||||
#define HUAWEI_PIN_IRQ 25
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_CS
|
||||
#define HUAWEI_PIN_CS 15
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_POWER
|
||||
#define HUAWEI_PIN_POWER 33
|
||||
#endif
|
||||
|
||||
#define HUAWEI_MINIMAL_OFFLINE_VOLTAGE 48
|
||||
#define HUAWEI_MINIMAL_ONLINE_VOLTAGE 42
|
||||
|
||||
#define MAX_CURRENT_MULTIPLIER 20
|
||||
|
||||
// Index values for rec_values array
|
||||
#define HUAWEI_INPUT_POWER_IDX 0
|
||||
#define HUAWEI_INPUT_FREQ_IDX 1
|
||||
#define HUAWEI_INPUT_CURRENT_IDX 2
|
||||
#define HUAWEI_OUTPUT_POWER_IDX 3
|
||||
#define HUAWEI_EFFICIENCY_IDX 4
|
||||
#define HUAWEI_OUTPUT_VOLTAGE_IDX 5
|
||||
#define HUAWEI_OUTPUT_CURRENT_MAX_IDX 6
|
||||
#define HUAWEI_INPUT_VOLTAGE_IDX 7
|
||||
#define HUAWEI_OUTPUT_TEMPERATURE_IDX 8
|
||||
#define HUAWEI_INPUT_TEMPERATURE_IDX 9
|
||||
#define HUAWEI_OUTPUT_CURRENT_IDX 10
|
||||
#define HUAWEI_OUTPUT_CURRENT1_IDX 11
|
||||
|
||||
// Defines and index values for tx_values array
|
||||
#define HUAWEI_OFFLINE_VOLTAGE 0x01
|
||||
#define HUAWEI_ONLINE_VOLTAGE 0x00
|
||||
#define HUAWEI_OFFLINE_CURRENT 0x04
|
||||
#define HUAWEI_ONLINE_CURRENT 0x03
|
||||
|
||||
// Modes of operation
|
||||
#define HUAWEI_MODE_OFF 0
|
||||
#define HUAWEI_MODE_ON 1
|
||||
#define HUAWEI_MODE_AUTO_EXT 2
|
||||
#define HUAWEI_MODE_AUTO_INT 3
|
||||
|
||||
// Error codes
|
||||
#define HUAWEI_ERROR_CODE_RX 0x01
|
||||
#define HUAWEI_ERROR_CODE_TX 0x02
|
||||
|
||||
// Wait time/current before shuting down the PSU / charger
|
||||
// This is set to allow the fan to run for some time
|
||||
#define HUAWEI_AUTO_MODE_SHUTDOWN_DELAY 60000
|
||||
#define HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT 0.75
|
||||
|
||||
// Updateinterval used to request new values from the PSU
|
||||
#define HUAWEI_DATA_REQUEST_INTERVAL_MS 2500
|
||||
|
||||
typedef struct RectifierParameters {
|
||||
float input_voltage;
|
||||
float input_frequency;
|
||||
float input_current;
|
||||
float input_power;
|
||||
float input_temp;
|
||||
float efficiency;
|
||||
float output_voltage;
|
||||
float output_current;
|
||||
float max_output_current;
|
||||
float output_power;
|
||||
float output_temp;
|
||||
float amp_hour;
|
||||
} RectifierParameters_t;
|
||||
|
||||
class HuaweiCanCommClass {
|
||||
public:
|
||||
bool init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk,
|
||||
uint8_t huawei_irq, uint8_t huawei_cs, uint32_t frequency);
|
||||
void loop();
|
||||
bool gotNewRxDataFrame(bool clear);
|
||||
uint8_t getErrorCode(bool clear);
|
||||
uint32_t getParameterValue(uint8_t parameter);
|
||||
void setParameterValue(uint16_t in, uint8_t parameterType);
|
||||
|
||||
private:
|
||||
void sendRequest();
|
||||
|
||||
SPIClass *SPI;
|
||||
MCP_CAN *_CAN;
|
||||
uint8_t _huaweiIrq; // IRQ pin
|
||||
uint32_t _nextRequestMillis = 0; // When to send next data request to PSU
|
||||
|
||||
std::mutex _mutex;
|
||||
|
||||
uint32_t _recValues[12];
|
||||
uint16_t _txValues[5];
|
||||
bool _hasNewTxValue[5];
|
||||
|
||||
uint8_t _errorCode;
|
||||
bool _completeUpdateReceived;
|
||||
};
|
||||
|
||||
class HuaweiCanClass {
|
||||
public:
|
||||
void init(Scheduler& scheduler, uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power);
|
||||
void updateSettings(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power);
|
||||
void setValue(float in, uint8_t parameterType);
|
||||
void setMode(uint8_t mode);
|
||||
|
||||
RectifierParameters_t * get();
|
||||
uint32_t getLastUpdate();
|
||||
bool getAutoPowerStatus();
|
||||
|
||||
private:
|
||||
void loop();
|
||||
void processReceivedParameters();
|
||||
void _setValue(float in, uint8_t parameterType);
|
||||
|
||||
Task _loopTask;
|
||||
|
||||
TaskHandle_t _HuaweiCanCommunicationTaskHdl = NULL;
|
||||
bool _initialized = false;
|
||||
uint8_t _huaweiPower; // Power pin
|
||||
uint8_t _mode = HUAWEI_MODE_AUTO_EXT;
|
||||
|
||||
RectifierParameters_t _rp;
|
||||
|
||||
uint32_t _lastUpdateReceivedMillis; // Timestamp for last data seen from the PSU
|
||||
uint32_t _outputCurrentOnSinceMillis; // Timestamp since when the PSU was idle at zero amps
|
||||
uint32_t _nextAutoModePeriodicIntMillis; // When to set the next output voltage in automatic mode
|
||||
uint32_t _lastPowerMeterUpdateReceivedMillis; // Timestamp of last seen power meter value
|
||||
uint32_t _autoModeBlockedTillMillis = 0; // Timestamp to block running auto mode for some time
|
||||
|
||||
uint8_t _autoPowerEnabledCounter = 0;
|
||||
bool _autoPowerEnabled = false;
|
||||
};
|
||||
|
||||
extern HuaweiCanClass HuaweiCan;
|
||||
extern HuaweiCanCommClass HuaweiCanComm;
|
||||
76
include/JkBmsController.h
Normal file
@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <frozen/string.h>
|
||||
|
||||
#include "Battery.h"
|
||||
#include "JkBmsSerialMessage.h"
|
||||
|
||||
class DataPointContainer;
|
||||
|
||||
namespace JkBms {
|
||||
|
||||
class Controller : public BatteryProvider {
|
||||
public:
|
||||
Controller() = default;
|
||||
|
||||
bool init(bool verboseLogging) final;
|
||||
void deinit() final;
|
||||
void loop() final;
|
||||
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
|
||||
|
||||
private:
|
||||
enum class Status : unsigned {
|
||||
Initializing,
|
||||
Timeout,
|
||||
WaitingForPollInterval,
|
||||
HwSerialNotAvailableForWrite,
|
||||
BusyReading,
|
||||
RequestSent,
|
||||
FrameCompleted
|
||||
};
|
||||
|
||||
frozen::string const& getStatusText(Status status);
|
||||
void announceStatus(Status status);
|
||||
void sendRequest(uint8_t pollInterval);
|
||||
void rxData(uint8_t inbyte);
|
||||
void reset();
|
||||
void frameComplete();
|
||||
void processDataPoints(DataPointContainer const& dataPoints);
|
||||
|
||||
enum class Interface : unsigned {
|
||||
Invalid,
|
||||
Uart,
|
||||
Transceiver
|
||||
};
|
||||
|
||||
Interface getInterface() const;
|
||||
|
||||
enum class ReadState : unsigned {
|
||||
Idle,
|
||||
WaitingForFrameStart,
|
||||
FrameStartReceived,
|
||||
StartMarkerReceived,
|
||||
FrameLengthMsbReceived,
|
||||
ReadingFrame
|
||||
};
|
||||
ReadState _readState;
|
||||
void setReadState(ReadState state) {
|
||||
_readState = state;
|
||||
}
|
||||
|
||||
bool _verboseLogging = true;
|
||||
int8_t _rxEnablePin = -1;
|
||||
int8_t _txEnablePin = -1;
|
||||
Status _lastStatus = Status::Initializing;
|
||||
uint32_t _lastStatusPrinted = 0;
|
||||
uint32_t _lastRequest = 0;
|
||||
uint16_t _frameLength = 0;
|
||||
uint8_t _protocolVersion = -1;
|
||||
SerialResponse::tData _buffer = {};
|
||||
std::shared_ptr<JkBmsBatteryStats> _stats =
|
||||
std::make_shared<JkBmsBatteryStats>();
|
||||
};
|
||||
|
||||
} /* namespace JkBms */
|
||||
304
include/JkBmsDataPoints.h
Normal file
@ -0,0 +1,304 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <variant>
|
||||
#include <frozen/map.h>
|
||||
#include <frozen/string.h>
|
||||
|
||||
namespace JkBms {
|
||||
|
||||
#define ALARM_BITS(fnc) \
|
||||
fnc(LowCapacity, (1<<0)) \
|
||||
fnc(BmsOvertemperature, (1<<1)) \
|
||||
fnc(ChargingOvervoltage, (1<<2)) \
|
||||
fnc(DischargeUndervoltage, (1<<3)) \
|
||||
fnc(BatteryOvertemperature, (1<<4)) \
|
||||
fnc(ChargingOvercurrent, (1<<5)) \
|
||||
fnc(DischargeOvercurrent, (1<<6)) \
|
||||
fnc(CellVoltageDifference, (1<<7)) \
|
||||
fnc(BatteryBoxOvertemperature, (1<<8)) \
|
||||
fnc(BatteryUndertemperature, (1<<9)) \
|
||||
fnc(CellOvervoltage, (1<<10)) \
|
||||
fnc(CellUndervoltage, (1<<11)) \
|
||||
fnc(AProtect, (1<<12)) \
|
||||
fnc(BProtect, (1<<13)) \
|
||||
fnc(Reserved1, (1<<14)) \
|
||||
fnc(Reserved2, (1<<15))
|
||||
|
||||
enum class AlarmBits : uint16_t {
|
||||
#define ALARM_ENUM(name, value) name = value,
|
||||
ALARM_BITS(ALARM_ENUM)
|
||||
#undef ALARM_ENUM
|
||||
};
|
||||
|
||||
static const frozen::map<AlarmBits, frozen::string, 16> AlarmBitTexts = {
|
||||
#define ALARM_TEXT(name, value) { AlarmBits::name, #name },
|
||||
ALARM_BITS(ALARM_TEXT)
|
||||
#undef ALARM_TEXT
|
||||
};
|
||||
|
||||
#define STATUS_BITS(fnc) \
|
||||
fnc(ChargingActive, (1<<0)) \
|
||||
fnc(DischargingActive, (1<<1)) \
|
||||
fnc(BalancingActive, (1<<2)) \
|
||||
fnc(BatteryOnline, (1<<3))
|
||||
|
||||
enum class StatusBits : uint16_t {
|
||||
#define STATUS_ENUM(name, value) name = value,
|
||||
STATUS_BITS(STATUS_ENUM)
|
||||
#undef STATUS_ENUM
|
||||
};
|
||||
|
||||
static const frozen::map<StatusBits, frozen::string, 4> StatusBitTexts = {
|
||||
#define STATUS_TEXT(name, value) { StatusBits::name, #name },
|
||||
STATUS_BITS(STATUS_TEXT)
|
||||
#undef STATUS_TEXT
|
||||
};
|
||||
|
||||
enum class DataPointLabel : uint8_t {
|
||||
CellsMilliVolt = 0x79,
|
||||
BmsTempCelsius = 0x80,
|
||||
BatteryTempOneCelsius = 0x81,
|
||||
BatteryTempTwoCelsius = 0x82,
|
||||
BatteryVoltageMilliVolt = 0x83,
|
||||
BatteryCurrentMilliAmps = 0x84,
|
||||
BatterySoCPercent = 0x85,
|
||||
BatteryTemperatureSensorAmount = 0x86,
|
||||
BatteryCycles = 0x87,
|
||||
BatteryCycleCapacity = 0x89,
|
||||
BatteryCellAmount = 0x8a,
|
||||
AlarmsBitmask = 0x8b,
|
||||
StatusBitmask = 0x8c,
|
||||
TotalOvervoltageThresholdMilliVolt = 0x8e,
|
||||
TotalUndervoltageThresholdMilliVolt = 0x8f,
|
||||
CellOvervoltageThresholdMilliVolt = 0x90,
|
||||
CellOvervoltageRecoveryMilliVolt = 0x91,
|
||||
CellOvervoltageProtectionDelaySeconds = 0x92,
|
||||
CellUndervoltageThresholdMilliVolt = 0x93,
|
||||
CellUndervoltageRecoveryMilliVolt = 0x94,
|
||||
CellUndervoltageProtectionDelaySeconds = 0x95,
|
||||
CellVoltageDiffThresholdMilliVolt = 0x96,
|
||||
DischargeOvercurrentThresholdAmperes = 0x97,
|
||||
DischargeOvercurrentDelaySeconds = 0x98,
|
||||
ChargeOvercurrentThresholdAmps = 0x99,
|
||||
ChargeOvercurrentDelaySeconds = 0x9a,
|
||||
BalanceCellVoltageThresholdMilliVolt = 0x9b,
|
||||
BalanceVoltageDiffThresholdMilliVolt = 0x9c,
|
||||
BalancingEnabled = 0x9d,
|
||||
BmsTempProtectionThresholdCelsius = 0x9e,
|
||||
BmsTempRecoveryThresholdCelsius = 0x9f,
|
||||
BatteryTempProtectionThresholdCelsius = 0xa0,
|
||||
BatteryTempRecoveryThresholdCelsius = 0xa1,
|
||||
BatteryTempDiffThresholdCelsius = 0xa2,
|
||||
ChargeHighTempThresholdCelsius = 0xa3,
|
||||
DischargeHighTempThresholdCelsius = 0xa4,
|
||||
ChargeLowTempThresholdCelsius = 0xa5,
|
||||
ChargeLowTempRecoveryCelsius = 0xa6,
|
||||
DischargeLowTempThresholdCelsius = 0xa7,
|
||||
DischargeLowTempRecoveryCelsius = 0xa8,
|
||||
CellAmountSetting = 0xa9,
|
||||
BatteryCapacitySettingAmpHours = 0xaa,
|
||||
BatteryChargeEnabled = 0xab,
|
||||
BatteryDischargeEnabled = 0xac,
|
||||
CurrentCalibrationMilliAmps = 0xad,
|
||||
BmsAddress = 0xae,
|
||||
BatteryType = 0xaf,
|
||||
SleepWaitTime = 0xb0, // what's this?
|
||||
LowCapacityAlarmThresholdPercent = 0xb1,
|
||||
ModificationPassword = 0xb2,
|
||||
DedicatedChargerSwitch = 0xb3, // what's this?
|
||||
EquipmentId = 0xb4,
|
||||
DateOfManufacturing = 0xb5,
|
||||
BmsHourMeterMinutes = 0xb6,
|
||||
BmsSoftwareVersion = 0xb7,
|
||||
CurrentCalibration = 0xb8,
|
||||
ActualBatteryCapacityAmpHours = 0xb9,
|
||||
ProductId = 0xba,
|
||||
ProtocolVersion = 0xc0
|
||||
};
|
||||
|
||||
using tCells = std::map<uint8_t, uint16_t>;
|
||||
|
||||
template<DataPointLabel> struct DataPointLabelTraits;
|
||||
|
||||
#define LABEL_TRAIT(n, t, u) template<> struct DataPointLabelTraits<DataPointLabel::n> { \
|
||||
using type = t; \
|
||||
static constexpr char const name[] = #n; \
|
||||
static constexpr char const unit[] = u; \
|
||||
};
|
||||
|
||||
/**
|
||||
* the types associated with the labels are the types for the respective data
|
||||
* points in the JkBms::DataPoint class. they are *not* always equal to the
|
||||
* type used in the serial message.
|
||||
*
|
||||
* it is unfortunate that we have to repeat all enum values here to define the
|
||||
* traits. code generation could help here (labels are defined in a single
|
||||
* source of truth and this code is generated -- no typing errors, etc.).
|
||||
* however, the compiler will complain if an enum is misspelled or traits are
|
||||
* defined for a removed enum, so we will notice. it will also complain when a
|
||||
* trait is missing and if a data point for a label without traits is added to
|
||||
* the DataPointContainer class, because the traits must be available then.
|
||||
* even though this is tedious to maintain, human errors will be caught.
|
||||
*/
|
||||
LABEL_TRAIT(CellsMilliVolt, tCells, "mV");
|
||||
LABEL_TRAIT(BmsTempCelsius, int16_t, "°C");
|
||||
LABEL_TRAIT(BatteryTempOneCelsius, int16_t, "°C");
|
||||
LABEL_TRAIT(BatteryTempTwoCelsius, int16_t, "°C");
|
||||
LABEL_TRAIT(BatteryVoltageMilliVolt, uint32_t, "mV");
|
||||
LABEL_TRAIT(BatteryCurrentMilliAmps, int32_t, "mA");
|
||||
LABEL_TRAIT(BatterySoCPercent, uint8_t, "%");
|
||||
LABEL_TRAIT(BatteryTemperatureSensorAmount, uint8_t, "");
|
||||
LABEL_TRAIT(BatteryCycles, uint16_t, "");
|
||||
LABEL_TRAIT(BatteryCycleCapacity, uint32_t, "Ah");
|
||||
LABEL_TRAIT(BatteryCellAmount, uint16_t, "");
|
||||
LABEL_TRAIT(AlarmsBitmask, uint16_t, "");
|
||||
LABEL_TRAIT(StatusBitmask, uint16_t, "");
|
||||
LABEL_TRAIT(TotalOvervoltageThresholdMilliVolt, uint32_t, "mV");
|
||||
LABEL_TRAIT(TotalUndervoltageThresholdMilliVolt, uint32_t, "mV");
|
||||
LABEL_TRAIT(CellOvervoltageThresholdMilliVolt, uint16_t, "mV");
|
||||
LABEL_TRAIT(CellOvervoltageRecoveryMilliVolt, uint16_t, "mV");
|
||||
LABEL_TRAIT(CellOvervoltageProtectionDelaySeconds, uint16_t, "s");
|
||||
LABEL_TRAIT(CellUndervoltageThresholdMilliVolt, uint16_t, "mV");
|
||||
LABEL_TRAIT(CellUndervoltageRecoveryMilliVolt, uint16_t, "mV");
|
||||
LABEL_TRAIT(CellUndervoltageProtectionDelaySeconds, uint16_t, "s");
|
||||
LABEL_TRAIT(CellVoltageDiffThresholdMilliVolt, uint16_t, "mV");
|
||||
LABEL_TRAIT(DischargeOvercurrentThresholdAmperes, uint16_t, "A");
|
||||
LABEL_TRAIT(DischargeOvercurrentDelaySeconds, uint16_t, "s");
|
||||
LABEL_TRAIT(ChargeOvercurrentThresholdAmps, uint16_t, "A");
|
||||
LABEL_TRAIT(ChargeOvercurrentDelaySeconds, uint16_t, "s");
|
||||
LABEL_TRAIT(BalanceCellVoltageThresholdMilliVolt, uint16_t, "mV");
|
||||
LABEL_TRAIT(BalanceVoltageDiffThresholdMilliVolt, uint16_t, "mV");
|
||||
LABEL_TRAIT(BalancingEnabled, bool, "");
|
||||
LABEL_TRAIT(BmsTempProtectionThresholdCelsius, uint16_t, "°C");
|
||||
LABEL_TRAIT(BmsTempRecoveryThresholdCelsius, uint16_t, "°C");
|
||||
LABEL_TRAIT(BatteryTempProtectionThresholdCelsius, uint16_t, "°C");
|
||||
LABEL_TRAIT(BatteryTempRecoveryThresholdCelsius, uint16_t, "°C");
|
||||
LABEL_TRAIT(BatteryTempDiffThresholdCelsius, uint16_t, "°C");
|
||||
LABEL_TRAIT(ChargeHighTempThresholdCelsius, uint16_t, "°C");
|
||||
LABEL_TRAIT(DischargeHighTempThresholdCelsius, uint16_t, "°C");
|
||||
LABEL_TRAIT(ChargeLowTempThresholdCelsius, int16_t, "°C");
|
||||
LABEL_TRAIT(ChargeLowTempRecoveryCelsius, int16_t, "°C");
|
||||
LABEL_TRAIT(DischargeLowTempThresholdCelsius, int16_t, "°C");
|
||||
LABEL_TRAIT(DischargeLowTempRecoveryCelsius, int16_t, "°C");
|
||||
LABEL_TRAIT(CellAmountSetting, uint8_t, "");
|
||||
LABEL_TRAIT(BatteryCapacitySettingAmpHours, uint32_t, "Ah");
|
||||
LABEL_TRAIT(BatteryChargeEnabled, bool, "");
|
||||
LABEL_TRAIT(BatteryDischargeEnabled, bool, "");
|
||||
LABEL_TRAIT(CurrentCalibrationMilliAmps, uint16_t, "mA");
|
||||
LABEL_TRAIT(BmsAddress, uint8_t, "");
|
||||
LABEL_TRAIT(BatteryType, uint8_t, "");
|
||||
LABEL_TRAIT(SleepWaitTime, uint16_t, "s");
|
||||
LABEL_TRAIT(LowCapacityAlarmThresholdPercent, uint8_t, "%");
|
||||
LABEL_TRAIT(ModificationPassword, std::string, "");
|
||||
LABEL_TRAIT(DedicatedChargerSwitch, bool, "");
|
||||
LABEL_TRAIT(EquipmentId, std::string, "");
|
||||
LABEL_TRAIT(DateOfManufacturing, std::string, "");
|
||||
LABEL_TRAIT(BmsHourMeterMinutes, uint32_t, "min");
|
||||
LABEL_TRAIT(BmsSoftwareVersion, std::string, "");
|
||||
LABEL_TRAIT(CurrentCalibration, bool, "");
|
||||
LABEL_TRAIT(ActualBatteryCapacityAmpHours, uint32_t, "Ah");
|
||||
LABEL_TRAIT(ProductId, std::string, "");
|
||||
LABEL_TRAIT(ProtocolVersion, uint8_t, "");
|
||||
#undef LABEL_TRAIT
|
||||
|
||||
class DataPoint {
|
||||
friend class DataPointContainer;
|
||||
|
||||
public:
|
||||
using tValue = std::variant<bool, uint8_t, uint16_t, uint32_t,
|
||||
int16_t, int32_t, std::string, tCells>;
|
||||
|
||||
DataPoint() = delete;
|
||||
|
||||
DataPoint(DataPoint const& other)
|
||||
: _strLabel(other._strLabel)
|
||||
, _strValue(other._strValue)
|
||||
, _strUnit(other._strUnit)
|
||||
, _value(other._value)
|
||||
, _timestamp(other._timestamp) { }
|
||||
|
||||
DataPoint(std::string const& strLabel, std::string const& strValue,
|
||||
std::string const& strUnit, tValue value, uint32_t timestamp)
|
||||
: _strLabel(strLabel)
|
||||
, _strValue(strValue)
|
||||
, _strUnit(strUnit)
|
||||
, _value(std::move(value))
|
||||
, _timestamp(timestamp) { }
|
||||
|
||||
std::string const& getLabelText() const { return _strLabel; }
|
||||
std::string const& getValueText() const { return _strValue; }
|
||||
std::string const& getUnitText() const { return _strUnit; }
|
||||
uint32_t getTimestamp() const { return _timestamp; }
|
||||
|
||||
bool operator==(DataPoint const& other) const {
|
||||
return _value == other._value;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string _strLabel;
|
||||
std::string _strValue;
|
||||
std::string _strUnit;
|
||||
tValue _value;
|
||||
uint32_t _timestamp;
|
||||
};
|
||||
|
||||
template<typename T> std::string dataPointValueToStr(T const& v);
|
||||
|
||||
class DataPointContainer {
|
||||
public:
|
||||
DataPointContainer() = default;
|
||||
|
||||
using Label = DataPointLabel;
|
||||
template<Label L> using Traits = JkBms::DataPointLabelTraits<L>;
|
||||
|
||||
template<Label L>
|
||||
void add(typename Traits<L>::type val) {
|
||||
_dataPoints.emplace(
|
||||
L,
|
||||
DataPoint(
|
||||
Traits<L>::name,
|
||||
dataPointValueToStr(val),
|
||||
Traits<L>::unit,
|
||||
DataPoint::tValue(std::move(val)),
|
||||
millis()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// make sure add() is only called with the type expected for the
|
||||
// respective label, no implicit conversions allowed.
|
||||
template<Label L, typename T>
|
||||
void add(T) = delete;
|
||||
|
||||
template<Label L>
|
||||
std::optional<DataPoint const> getDataPointFor() const {
|
||||
auto it = _dataPoints.find(L);
|
||||
if (it == _dataPoints.end()) { return std::nullopt; }
|
||||
return it->second;
|
||||
}
|
||||
|
||||
template<Label L>
|
||||
std::optional<typename Traits<L>::type> get() const {
|
||||
auto optionalDataPoint = getDataPointFor<L>();
|
||||
if (!optionalDataPoint.has_value()) { return std::nullopt; }
|
||||
return std::get<typename Traits<L>::type>(optionalDataPoint->_value);
|
||||
}
|
||||
|
||||
using tMap = std::unordered_map<Label, DataPoint const>;
|
||||
tMap::const_iterator cbegin() const { return _dataPoints.cbegin(); }
|
||||
tMap::const_iterator cend() const { return _dataPoints.cend(); }
|
||||
|
||||
// copy all data points from source into this instance, overwriting
|
||||
// existing data points in this instance.
|
||||
void updateFrom(DataPointContainer const& source);
|
||||
|
||||
private:
|
||||
tMap _dataPoints;
|
||||
};
|
||||
|
||||
} /* namespace JkBms */
|
||||
93
include/JkBmsSerialMessage.h
Normal file
@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "JkBmsDataPoints.h"
|
||||
|
||||
namespace JkBms {
|
||||
|
||||
class SerialMessage {
|
||||
public:
|
||||
using tData = std::vector<uint8_t>;
|
||||
|
||||
SerialMessage() = delete;
|
||||
|
||||
enum class Command : uint8_t {
|
||||
Activate = 0x01,
|
||||
Write = 0x02,
|
||||
Read = 0x03,
|
||||
Password = 0x05,
|
||||
ReadAll = 0x06
|
||||
};
|
||||
|
||||
Command getCommand() const { return static_cast<Command>(_raw[8]); }
|
||||
|
||||
enum class Source : uint8_t {
|
||||
BMS = 0x00,
|
||||
Bluetooth = 0x01,
|
||||
GPS = 0x02,
|
||||
Host = 0x03
|
||||
};
|
||||
Source getSource() const { return static_cast<Source>(_raw[9]); }
|
||||
|
||||
enum class Type : uint8_t {
|
||||
Command = 0x00,
|
||||
Response = 0x01,
|
||||
Unsolicited = 0x02
|
||||
};
|
||||
Type getType() const { return static_cast<Type>(_raw[10]); }
|
||||
|
||||
// this does *not* include the two byte start marker
|
||||
uint16_t getFrameLength() const { return get<uint16_t>(_raw.cbegin()+2); }
|
||||
|
||||
uint32_t getTerminalId() const { return get<uint32_t>(_raw.cbegin()+4); }
|
||||
|
||||
// there are 20 bytes of overhead. two of those are the start marker
|
||||
// bytes, which are *not* counted by the frame length.
|
||||
uint16_t getVariableFieldLength() const { return getFrameLength() - 18; }
|
||||
|
||||
// the upper byte of the 4-byte "record number" is reserved (for encryption)
|
||||
uint32_t getSequence() const { return get<uint32_t>(_raw.cend()-9) >> 8; }
|
||||
|
||||
bool isValid() const;
|
||||
|
||||
uint8_t const* data() { return _raw.data(); }
|
||||
size_t size() { return _raw.size(); }
|
||||
|
||||
protected:
|
||||
template <typename... Args>
|
||||
explicit SerialMessage(Args&&... args) : _raw(std::forward<Args>(args)...) { }
|
||||
|
||||
template<typename T, typename It> T get(It&& pos) const;
|
||||
template<typename It> bool getBool(It&& pos) const;
|
||||
template<typename It> int16_t getTemperature(It&& pos) const;
|
||||
template<typename It> std::string getString(It&& pos, size_t len, bool replaceZeroes = false) const;
|
||||
void processBatteryCurrent(tData::const_iterator& pos, uint8_t protocolVersion);
|
||||
template<typename T> void set(tData::iterator const& pos, T val);
|
||||
uint16_t calcChecksum() const;
|
||||
void updateChecksum();
|
||||
|
||||
tData _raw;
|
||||
JkBms::DataPointContainer _dp;
|
||||
|
||||
static constexpr uint16_t startMarker = 0x4e57;
|
||||
static constexpr uint8_t endMarker = 0x68;
|
||||
};
|
||||
|
||||
class SerialResponse : public SerialMessage {
|
||||
public:
|
||||
using tData = SerialMessage::tData;
|
||||
explicit SerialResponse(tData&& raw, uint8_t protocolVersion = -1);
|
||||
|
||||
DataPointContainer const& getDataPoints() const { return _dp; }
|
||||
};
|
||||
|
||||
class SerialCommand : public SerialMessage {
|
||||
public:
|
||||
using Command = SerialMessage::Command;
|
||||
explicit SerialCommand(Command cmd);
|
||||
};
|
||||
|
||||
} /* namespace JkBms */
|
||||
@ -2,12 +2,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <AsyncWebSocket.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <Stream.h>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
#include <Print.h>
|
||||
#include <freertos/task.h>
|
||||
#include <mutex>
|
||||
|
||||
#define BUFFER_SIZE 500
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <queue>
|
||||
|
||||
class MessageOutputClass : public Print {
|
||||
public:
|
||||
@ -21,13 +22,19 @@ private:
|
||||
|
||||
Task _loopTask;
|
||||
|
||||
using message_t = std::vector<uint8_t>;
|
||||
|
||||
// we keep a buffer for every task and only write complete lines to the
|
||||
// serial output and then move them to be pushed through the websocket.
|
||||
// this way we prevent mangling of messages from different contexts.
|
||||
std::unordered_map<TaskHandle_t, message_t> _task_messages;
|
||||
std::queue<message_t> _lines;
|
||||
|
||||
AsyncWebSocket* _ws = nullptr;
|
||||
char _buffer[BUFFER_SIZE];
|
||||
uint16_t _buff_pos = 0;
|
||||
uint32_t _lastSend = 0;
|
||||
bool _forceSend = false;
|
||||
|
||||
std::mutex _msgLock;
|
||||
|
||||
void serialWrite(message_t const& m);
|
||||
};
|
||||
|
||||
extern MessageOutputClass MessageOutput;
|
||||
22
include/MqttBattery.h
Normal file
@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "Battery.h"
|
||||
#include <espMqttClient.h>
|
||||
|
||||
class MqttBattery : public BatteryProvider {
|
||||
public:
|
||||
MqttBattery() = default;
|
||||
|
||||
bool init(bool verboseLogging) final;
|
||||
void deinit() final;
|
||||
void loop() final { return; } // this class is event-driven
|
||||
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
|
||||
|
||||
private:
|
||||
bool _verboseLogging = false;
|
||||
String _socTopic;
|
||||
std::shared_ptr<MqttBatteryStats> _stats = std::make_shared<MqttBatteryStats>();
|
||||
|
||||
void onMqttMessage(espMqttClientTypes::MessageProperties const& properties,
|
||||
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total);
|
||||
};
|
||||
44
include/MqttHandleHuawei.h
Normal file
@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "Configuration.h"
|
||||
#include <Huawei_can.h>
|
||||
#include <espMqttClient.h>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
#include <mutex>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
|
||||
class MqttHandleHuaweiClass {
|
||||
public:
|
||||
void init(Scheduler& scheduler);
|
||||
|
||||
private:
|
||||
void loop();
|
||||
|
||||
enum class Topic : unsigned {
|
||||
LimitOnlineVoltage,
|
||||
LimitOnlineCurrent,
|
||||
LimitOfflineVoltage,
|
||||
LimitOfflineCurrent,
|
||||
Mode
|
||||
};
|
||||
|
||||
void onMqttMessage(Topic t,
|
||||
const espMqttClientTypes::MessageProperties& properties,
|
||||
const char* topic, const uint8_t* payload, size_t len,
|
||||
size_t index, size_t total);
|
||||
|
||||
Task _loopTask;
|
||||
|
||||
uint32_t _lastPublishStats;
|
||||
uint32_t _lastPublish;
|
||||
|
||||
// MQTT callbacks to process updates on subscribed topics are executed in
|
||||
// the MQTT thread's context. we use this queue to switch processing the
|
||||
// user requests into the main loop's context (TaskScheduler context).
|
||||
mutable std::mutex _mqttMutex;
|
||||
std::deque<std::function<void()>> _mqttCallbacks;
|
||||
};
|
||||
|
||||
extern MqttHandleHuaweiClass MqttHandleHuawei;
|
||||
31
include/MqttHandlePowerLimiter.h
Normal file
@ -0,0 +1,31 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "Configuration.h"
|
||||
#include <espMqttClient.h>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
#include <mutex>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
|
||||
class MqttHandlePowerLimiterClass {
|
||||
public:
|
||||
void init(Scheduler& scheduler);
|
||||
|
||||
private:
|
||||
void loop();
|
||||
void onCmdMode(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total);
|
||||
|
||||
Task _loopTask;
|
||||
|
||||
uint32_t _lastPublishStats;
|
||||
uint32_t _lastPublish;
|
||||
|
||||
// MQTT callbacks to process updates on subscribed topics are executed in
|
||||
// the MQTT thread's context. we use this queue to switch processing the
|
||||
// user requests into the main loop's context (TaskScheduler context).
|
||||
mutable std::mutex _mqttMutex;
|
||||
std::deque<std::function<void()>> _mqttCallbacks;
|
||||
};
|
||||
|
||||
extern MqttHandlePowerLimiterClass MqttHandlePowerLimiter;
|
||||
27
include/MqttHandlePylontechHass.h
Normal file
@ -0,0 +1,27 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
|
||||
class MqttHandlePylontechHassClass {
|
||||
public:
|
||||
void init(Scheduler& scheduler);
|
||||
void publishConfig();
|
||||
void forceUpdate();
|
||||
|
||||
private:
|
||||
void loop();
|
||||
void publish(const String& subtopic, const String& payload);
|
||||
void publishBinarySensor(const char* caption, const char* icon, const char* subTopic, const char* payload_on, const char* payload_off);
|
||||
void publishSensor(const char* caption, const char* icon, const char* subTopic, const char* deviceClass = NULL, const char* stateClass = NULL, const char* unitOfMeasurement = NULL);
|
||||
void createDeviceInfo(JsonObject& object);
|
||||
|
||||
Task _loopTask;
|
||||
|
||||
bool _wasConnected = false;
|
||||
bool _updateForced = false;
|
||||
String serial = "0001"; // pseudo-serial, can be replaced in future with real serialnumber
|
||||
};
|
||||
|
||||
extern MqttHandlePylontechHassClass MqttHandlePylontechHass;
|
||||
36
include/MqttHandleVedirect.h
Normal file
@ -0,0 +1,36 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "VeDirectMpptController.h"
|
||||
#include "Configuration.h"
|
||||
#include <Arduino.h>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
|
||||
#ifndef VICTRON_PIN_RX
|
||||
#define VICTRON_PIN_RX 22
|
||||
#endif
|
||||
|
||||
#ifndef VICTRON_PIN_TX
|
||||
#define VICTRON_PIN_TX 21
|
||||
#endif
|
||||
|
||||
class MqttHandleVedirectClass {
|
||||
public:
|
||||
void init(Scheduler& scheduler);
|
||||
void forceUpdate();
|
||||
private:
|
||||
void loop();
|
||||
VeDirectMpptController::veMpptStruct _kvFrame{};
|
||||
|
||||
Task _loopTask;
|
||||
|
||||
// point of time in millis() when updated values will be published
|
||||
uint32_t _nextPublishUpdatesOnly = 0;
|
||||
|
||||
// point of time in millis() when all values will be published
|
||||
uint32_t _nextPublishFull = 1;
|
||||
|
||||
bool _PublishFull;
|
||||
};
|
||||
|
||||
extern MqttHandleVedirectClass MqttHandleVedirect;
|
||||
27
include/MqttHandleVedirectHass.h
Normal file
@ -0,0 +1,27 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include "VeDirectMpptController.h"
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
|
||||
class MqttHandleVedirectHassClass {
|
||||
public:
|
||||
void init(Scheduler& scheduler);
|
||||
void publishConfig();
|
||||
void forceUpdate();
|
||||
|
||||
private:
|
||||
void loop();
|
||||
void publish(const String& subtopic, const String& payload);
|
||||
void publishBinarySensor(const char* caption, const char* icon, const char* subTopic, const char* payload_on, const char* payload_off);
|
||||
void publishSensor(const char* caption, const char* icon, const char* subTopic, const char* deviceClass = NULL, const char* stateClass = NULL, const char* unitOfMeasurement = NULL);
|
||||
void createDeviceInfo(JsonObject& object);
|
||||
|
||||
Task _loopTask;
|
||||
|
||||
bool _wasConnected = false;
|
||||
bool _updateForced = false;
|
||||
};
|
||||
|
||||
extern MqttHandleVedirectHassClass MqttHandleVedirectHass;
|
||||
@ -11,6 +11,7 @@ class MqttSettingsClass {
|
||||
public:
|
||||
MqttSettingsClass();
|
||||
void init();
|
||||
void loop();
|
||||
void performReconnect();
|
||||
bool getConnected();
|
||||
void publish(const String& subtopic, const String& payload);
|
||||
@ -37,6 +38,7 @@ private:
|
||||
Ticker _mqttReconnectTimer;
|
||||
MqttSubscribeParser _mqttSubscribeParser;
|
||||
std::mutex _clientLock;
|
||||
bool _verboseLogging = true;
|
||||
};
|
||||
|
||||
extern MqttSettingsClass MqttSettings;
|
||||
@ -38,6 +38,18 @@ struct PinMapping_t {
|
||||
uint8_t display_clk;
|
||||
uint8_t display_cs;
|
||||
uint8_t display_reset;
|
||||
int8_t victron_tx;
|
||||
int8_t victron_rx;
|
||||
int8_t battery_rx;
|
||||
int8_t battery_rxen;
|
||||
int8_t battery_tx;
|
||||
int8_t battery_txen;
|
||||
int8_t huawei_miso;
|
||||
int8_t huawei_mosi;
|
||||
int8_t huawei_clk;
|
||||
int8_t huawei_irq;
|
||||
int8_t huawei_cs;
|
||||
int8_t huawei_power;
|
||||
int8_t led[PINMAPPING_LED_COUNT];
|
||||
};
|
||||
|
||||
@ -50,9 +62,10 @@ public:
|
||||
bool isValidNrf24Config() const;
|
||||
bool isValidCmt2300Config() const;
|
||||
bool isValidEthConfig() const;
|
||||
|
||||
bool isValidHuaweiConfig() const;
|
||||
|
||||
private:
|
||||
PinMapping_t _pinMapping;
|
||||
};
|
||||
|
||||
extern PinMappingClass PinMapping;
|
||||
extern PinMappingClass PinMapping;
|
||||
|
||||
107
include/PowerLimiter.h
Normal file
@ -0,0 +1,107 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "Configuration.h"
|
||||
#include <espMqttClient.h>
|
||||
#include <Arduino.h>
|
||||
#include <Hoymiles.h>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
#include <frozen/string.h>
|
||||
|
||||
#define PL_UI_STATE_INACTIVE 0
|
||||
#define PL_UI_STATE_CHARGING 1
|
||||
#define PL_UI_STATE_USE_SOLAR_ONLY 2
|
||||
#define PL_UI_STATE_USE_SOLAR_AND_BATTERY 3
|
||||
|
||||
#define PL_MODE_ENABLE_NORMAL_OP 0
|
||||
#define PL_MODE_FULL_DISABLE 1
|
||||
#define PL_MODE_SOLAR_PT_ONLY 2
|
||||
|
||||
typedef enum {
|
||||
EMPTY_WHEN_FULL= 0,
|
||||
EMPTY_AT_NIGHT
|
||||
} batDrainStrategy;
|
||||
|
||||
|
||||
class PowerLimiterClass {
|
||||
public:
|
||||
enum class Status : unsigned {
|
||||
Initializing,
|
||||
DisabledByConfig,
|
||||
DisabledByMqtt,
|
||||
WaitingForValidTimestamp,
|
||||
PowerMeterDisabled,
|
||||
PowerMeterTimeout,
|
||||
PowerMeterPending,
|
||||
InverterInvalid,
|
||||
InverterChanged,
|
||||
InverterOffline,
|
||||
InverterCommandsDisabled,
|
||||
InverterLimitPending,
|
||||
InverterPowerCmdPending,
|
||||
InverterDevInfoPending,
|
||||
InverterStatsPending,
|
||||
UnconditionalSolarPassthrough,
|
||||
NoVeDirect,
|
||||
Settling,
|
||||
Stable,
|
||||
};
|
||||
|
||||
void init(Scheduler& scheduler);
|
||||
uint8_t getPowerLimiterState();
|
||||
int32_t getLastRequestedPowerLimit();
|
||||
|
||||
enum class Mode : unsigned {
|
||||
Normal = 0,
|
||||
Disabled = 1,
|
||||
UnconditionalFullSolarPassthrough = 2
|
||||
};
|
||||
|
||||
void setMode(Mode m) { _mode = m; }
|
||||
Mode getMode() const { return _mode; }
|
||||
void calcNextInverterRestart();
|
||||
|
||||
private:
|
||||
void loop();
|
||||
|
||||
Task _loopTask;
|
||||
|
||||
int32_t _lastRequestedPowerLimit = 0;
|
||||
uint32_t _lastPowerLimitMillis = 0;
|
||||
uint32_t _shutdownTimeout = 0;
|
||||
Status _lastStatus = Status::Initializing;
|
||||
uint32_t _lastStatusPrinted = 0;
|
||||
uint32_t _lastCalculation = 0;
|
||||
static constexpr uint32_t _calculationBackoffMsDefault = 128;
|
||||
uint32_t _calculationBackoffMs = _calculationBackoffMsDefault;
|
||||
Mode _mode = Mode::Normal;
|
||||
std::shared_ptr<InverterAbstract> _inverter = nullptr;
|
||||
bool _batteryDischargeEnabled = false;
|
||||
uint32_t _nextInverterRestart = 0; // Values: 0->not calculated / 1->no restart configured / >1->time of next inverter restart in millis()
|
||||
uint32_t _nextCalculateCheck = 5000; // time in millis for next NTP check to calulate restart
|
||||
bool _fullSolarPassThroughEnabled = false;
|
||||
bool _verboseLogging = true;
|
||||
|
||||
frozen::string const& getStatusText(Status status);
|
||||
void announceStatus(Status status);
|
||||
bool shutdown(Status status);
|
||||
bool shutdown() { return shutdown(_lastStatus); }
|
||||
int32_t inverterPowerDcToAc(std::shared_ptr<InverterAbstract> inverter, int32_t dcPower);
|
||||
void unconditionalSolarPassthrough(std::shared_ptr<InverterAbstract> inverter);
|
||||
bool canUseDirectSolarPower();
|
||||
int32_t calcPowerLimit(std::shared_ptr<InverterAbstract> inverter, bool solarPowerEnabled, bool batteryDischargeEnabled);
|
||||
void commitPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t limit, bool enablePowerProduction);
|
||||
bool setNewPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t newPowerLimit);
|
||||
int32_t getSolarChargePower();
|
||||
float getLoadCorrectedVoltage();
|
||||
bool testThreshold(float socThreshold, float voltThreshold,
|
||||
std::function<bool(float, float)> compare);
|
||||
bool isStartThresholdReached();
|
||||
bool isStopThresholdReached();
|
||||
bool isBelowStopThreshold();
|
||||
bool useFullSolarPassthrough();
|
||||
};
|
||||
|
||||
extern PowerLimiterClass PowerLimiter;
|
||||
79
include/PowerMeter.h
Normal file
@ -0,0 +1,79 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "Configuration.h"
|
||||
#include <espMqttClient.h>
|
||||
#include <Arduino.h>
|
||||
#include <map>
|
||||
#include <list>
|
||||
#include "SDM.h"
|
||||
#include "sml.h"
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
|
||||
#ifndef SDM_RX_PIN
|
||||
#define SDM_RX_PIN 13
|
||||
#endif
|
||||
|
||||
#ifndef SDM_TX_PIN
|
||||
#define SDM_TX_PIN 32
|
||||
#endif
|
||||
|
||||
#ifndef SML_RX_PIN
|
||||
#define SML_RX_PIN 35
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
const unsigned char OBIS[6];
|
||||
void (*Fn)(double&);
|
||||
float* Arg;
|
||||
} OBISHandler;
|
||||
|
||||
class PowerMeterClass {
|
||||
public:
|
||||
enum SOURCE {
|
||||
SOURCE_MQTT = 0,
|
||||
SOURCE_SDM1PH = 1,
|
||||
SOURCE_SDM3PH = 2,
|
||||
SOURCE_HTTP = 3,
|
||||
SOURCE_SML = 4
|
||||
};
|
||||
void init(Scheduler& scheduler);
|
||||
float getPowerTotal(bool forceUpdate = true);
|
||||
uint32_t getLastPowerMeterUpdate();
|
||||
|
||||
private:
|
||||
void loop();
|
||||
void mqtt();
|
||||
|
||||
void onMqttMessage(const espMqttClientTypes::MessageProperties& properties,
|
||||
const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total);
|
||||
|
||||
Task _loopTask;
|
||||
|
||||
bool _verboseLogging = true;
|
||||
uint32_t _lastPowerMeterCheck;
|
||||
// Used in Power limiter for safety check
|
||||
uint32_t _lastPowerMeterUpdate;
|
||||
|
||||
float _powerMeter1Power = 0.0;
|
||||
float _powerMeter2Power = 0.0;
|
||||
float _powerMeter3Power = 0.0;
|
||||
float _powerMeter1Voltage = 0.0;
|
||||
float _powerMeter2Voltage = 0.0;
|
||||
float _powerMeter3Voltage = 0.0;
|
||||
float _powerMeterImport = 0.0;
|
||||
float _powerMeterExport = 0.0;
|
||||
|
||||
std::map<String, float*> _mqttSubscriptions;
|
||||
|
||||
void readPowerMeter();
|
||||
|
||||
bool smlReadLoop();
|
||||
const std::list<OBISHandler> smlHandlerList{
|
||||
{{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_powerMeter1Power},
|
||||
{{0x01, 0x00, 0x01, 0x08, 0x00, 0xff}, &smlOBISWh, &_powerMeterImport},
|
||||
{{0x01, 0x00, 0x02, 0x08, 0x00, 0xff}, &smlOBISWh, &_powerMeterExport}
|
||||
};
|
||||
};
|
||||
|
||||
extern PowerMeterClass PowerMeter;
|
||||
29
include/PylontechCanReceiver.h
Normal file
@ -0,0 +1,29 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "Configuration.h"
|
||||
#include "Battery.h"
|
||||
#include <espMqttClient.h>
|
||||
#include <driver/twai.h>
|
||||
#include <Arduino.h>
|
||||
#include <memory>
|
||||
|
||||
class PylontechCanReceiver : public BatteryProvider {
|
||||
public:
|
||||
bool init(bool verboseLogging) final;
|
||||
void deinit() final;
|
||||
void loop() final;
|
||||
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
|
||||
|
||||
private:
|
||||
uint16_t readUnsignedInt16(uint8_t *data);
|
||||
int16_t readSignedInt16(uint8_t *data);
|
||||
float scaleValue(int16_t value, float factor);
|
||||
bool getBit(uint8_t value, uint8_t bit);
|
||||
|
||||
void dummyData();
|
||||
|
||||
bool _verboseLogging = true;
|
||||
std::shared_ptr<PylontechBatteryStats> _stats =
|
||||
std::make_shared<PylontechBatteryStats>();
|
||||
};
|
||||
52
include/VictronMppt.h
Normal file
@ -0,0 +1,52 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <memory>
|
||||
|
||||
#include "VeDirectMpptController.h"
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
|
||||
class VictronMpptClass {
|
||||
public:
|
||||
VictronMpptClass() = default;
|
||||
~VictronMpptClass() = default;
|
||||
|
||||
void init(Scheduler& scheduler);
|
||||
void updateSettings();
|
||||
|
||||
bool isDataValid() const;
|
||||
|
||||
// returns the data age of all controllers,
|
||||
// i.e, the youngest data's age is returned.
|
||||
uint32_t getDataAgeMillis() const;
|
||||
|
||||
VeDirectMpptController::spData_t getData(size_t idx = 0) const;
|
||||
|
||||
// total output of all MPPT charge controllers in Watts
|
||||
int32_t getPowerOutputWatts() const;
|
||||
|
||||
// total panel input power of all MPPT charge controllers in Watts
|
||||
int32_t getPanelPowerWatts() const;
|
||||
|
||||
// sum of total yield of all MPPT charge controllers in kWh
|
||||
double getYieldTotal() const;
|
||||
|
||||
// sum of today's yield of all MPPT charge controllers in kWh
|
||||
double getYieldDay() const;
|
||||
|
||||
private:
|
||||
void loop();
|
||||
VictronMpptClass(VictronMpptClass const& other) = delete;
|
||||
VictronMpptClass(VictronMpptClass&& other) = delete;
|
||||
VictronMpptClass& operator=(VictronMpptClass const& other) = delete;
|
||||
VictronMpptClass& operator=(VictronMpptClass&& other) = delete;
|
||||
|
||||
Task _loopTask;
|
||||
|
||||
mutable std::mutex _mutex;
|
||||
using controller_t = std::unique_ptr<VeDirectMpptController>;
|
||||
std::vector<controller_t> _controllers;
|
||||
};
|
||||
|
||||
extern VictronMpptClass VictronMppt;
|
||||
17
include/VictronSmartShunt.h
Normal file
@ -0,0 +1,17 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "Battery.h"
|
||||
|
||||
class VictronSmartShunt : public BatteryProvider {
|
||||
public:
|
||||
bool init(bool verboseLogging) final;
|
||||
void deinit() final { }
|
||||
void loop() final;
|
||||
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
|
||||
|
||||
private:
|
||||
uint32_t _lastUpdate = 0;
|
||||
std::shared_ptr<VictronSmartShuntStats> _stats =
|
||||
std::make_shared<VictronSmartShuntStats>();
|
||||
};
|
||||
@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "WebApi_battery.h"
|
||||
#include "WebApi_config.h"
|
||||
#include "WebApi_device.h"
|
||||
#include "WebApi_devinfo.h"
|
||||
@ -16,12 +17,19 @@
|
||||
#include "WebApi_network.h"
|
||||
#include "WebApi_ntp.h"
|
||||
#include "WebApi_power.h"
|
||||
#include "WebApi_powermeter.h"
|
||||
#include "WebApi_powerlimiter.h"
|
||||
#include "WebApi_prometheus.h"
|
||||
#include "WebApi_security.h"
|
||||
#include "WebApi_sysstatus.h"
|
||||
#include "WebApi_webapp.h"
|
||||
#include "WebApi_ws_console.h"
|
||||
#include "WebApi_ws_live.h"
|
||||
#include "WebApi_ws_vedirect_live.h"
|
||||
#include "WebApi_vedirect.h"
|
||||
#include "WebApi_ws_Huawei.h"
|
||||
#include "WebApi_Huawei.h"
|
||||
#include "WebApi_ws_battery.h"
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
|
||||
@ -44,6 +52,7 @@ private:
|
||||
|
||||
AsyncWebServer _server;
|
||||
|
||||
WebApiBatteryClass _webApiBattery;
|
||||
WebApiConfigClass _webApiConfig;
|
||||
WebApiDeviceClass _webApiDevice;
|
||||
WebApiDevInfoClass _webApiDevInfo;
|
||||
@ -58,12 +67,19 @@ private:
|
||||
WebApiNetworkClass _webApiNetwork;
|
||||
WebApiNtpClass _webApiNtp;
|
||||
WebApiPowerClass _webApiPower;
|
||||
WebApiPowerMeterClass _webApiPowerMeter;
|
||||
WebApiPowerLimiterClass _webApiPowerLimiter;
|
||||
WebApiPrometheusClass _webApiPrometheus;
|
||||
WebApiSecurityClass _webApiSecurity;
|
||||
WebApiSysstatusClass _webApiSysstatus;
|
||||
WebApiWebappClass _webApiWebapp;
|
||||
WebApiWsConsoleClass _webApiWsConsole;
|
||||
WebApiWsLiveClass _webApiWsLive;
|
||||
WebApiWsVedirectLiveClass _webApiWsVedirectLive;
|
||||
WebApiVedirectClass _webApiVedirect;
|
||||
WebApiHuaweiClass _webApiHuaweiClass;
|
||||
WebApiWsHuaweiLiveClass _webApiWsHuaweiLive;
|
||||
WebApiWsBatteryLiveClass _webApiWsBatteryLive;
|
||||
};
|
||||
|
||||
extern WebApiClass WebApi;
|
||||
19
include/WebApi_Huawei.h
Normal file
@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <AsyncJson.h>
|
||||
|
||||
class WebApiHuaweiClass {
|
||||
public:
|
||||
void init(AsyncWebServer& server);
|
||||
void loop();
|
||||
void getJsonData(JsonVariant& root);
|
||||
private:
|
||||
void onStatus(AsyncWebServerRequest* request);
|
||||
void onAdminGet(AsyncWebServerRequest* request);
|
||||
void onAdminPost(AsyncWebServerRequest* request);
|
||||
void onPost(AsyncWebServerRequest* request);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
};
|
||||
18
include/WebApi_battery.h
Normal file
@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
|
||||
class WebApiBatteryClass {
|
||||
public:
|
||||
void init(AsyncWebServer& server);
|
||||
void loop();
|
||||
|
||||
private:
|
||||
void onStatus(AsyncWebServerRequest* request);
|
||||
void onAdminGet(AsyncWebServerRequest* request);
|
||||
void onAdminPost(AsyncWebServerRequest* request);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
};
|
||||
18
include/WebApi_powerlimiter.h
Normal file
@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
|
||||
class WebApiPowerLimiterClass {
|
||||
public:
|
||||
void init(AsyncWebServer& server);
|
||||
void loop();
|
||||
|
||||
private:
|
||||
void onStatus(AsyncWebServerRequest* request);
|
||||
void onAdminGet(AsyncWebServerRequest* request);
|
||||
void onAdminPost(AsyncWebServerRequest* request);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
};
|
||||
19
include/WebApi_powermeter.h
Normal file
@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
|
||||
class WebApiPowerMeterClass {
|
||||
public:
|
||||
void init(AsyncWebServer& server);
|
||||
void loop();
|
||||
|
||||
private:
|
||||
void onStatus(AsyncWebServerRequest* request);
|
||||
void onAdminGet(AsyncWebServerRequest* request);
|
||||
void onAdminPost(AsyncWebServerRequest* request);
|
||||
void onTestHttpRequest(AsyncWebServerRequest* request);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
};
|
||||
18
include/WebApi_vedirect.h
Normal file
@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
|
||||
class WebApiVedirectClass {
|
||||
public:
|
||||
void init(AsyncWebServer& server);
|
||||
void loop();
|
||||
|
||||
private:
|
||||
void onVedirectStatus(AsyncWebServerRequest* request);
|
||||
void onVedirectAdminGet(AsyncWebServerRequest* request);
|
||||
void onVedirectAdminPost(AsyncWebServerRequest* request);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
};
|
||||
26
include/WebApi_ws_Huawei.h
Normal file
@ -0,0 +1,26 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "ArduinoJson.h"
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <mutex>
|
||||
|
||||
class WebApiWsHuaweiLiveClass {
|
||||
public:
|
||||
WebApiWsHuaweiLiveClass();
|
||||
void init(AsyncWebServer& server);
|
||||
void loop();
|
||||
|
||||
private:
|
||||
void generateJsonResponse(JsonVariant& root);
|
||||
void onLivedataStatus(AsyncWebServerRequest* request);
|
||||
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
AsyncWebSocket _ws;
|
||||
|
||||
uint32_t _lastWsCleanup = 0;
|
||||
uint32_t _lastUpdateCheck = 0;
|
||||
|
||||
std::mutex _mutex;
|
||||
};
|
||||
27
include/WebApi_ws_battery.h
Normal file
@ -0,0 +1,27 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "ArduinoJson.h"
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <mutex>
|
||||
|
||||
class WebApiWsBatteryLiveClass {
|
||||
public:
|
||||
WebApiWsBatteryLiveClass();
|
||||
void init(AsyncWebServer& server);
|
||||
void loop();
|
||||
|
||||
private:
|
||||
void generateJsonResponse(JsonVariant& root);
|
||||
void onLivedataStatus(AsyncWebServerRequest* request);
|
||||
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
AsyncWebSocket _ws;
|
||||
|
||||
uint32_t _lastWsCleanup = 0;
|
||||
uint32_t _lastUpdateCheck = 0;
|
||||
static constexpr uint16_t _responseSize = 1024 + 512;
|
||||
|
||||
std::mutex _mutex;
|
||||
};
|
||||
29
include/WebApi_ws_vedirect_live.h
Normal file
@ -0,0 +1,29 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "ArduinoJson.h"
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <VeDirectMpptController.h>
|
||||
#include <mutex>
|
||||
|
||||
class WebApiWsVedirectLiveClass {
|
||||
public:
|
||||
WebApiWsVedirectLiveClass();
|
||||
void init(AsyncWebServer& server);
|
||||
void loop();
|
||||
|
||||
private:
|
||||
void generateJsonResponse(JsonVariant& root);
|
||||
void onLivedataStatus(AsyncWebServerRequest* request);
|
||||
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
AsyncWebSocket _ws;
|
||||
|
||||
uint32_t _lastWsPublish = 0;
|
||||
uint32_t _lastWsCleanup = 0;
|
||||
uint32_t _dataAgeMillis = 0;
|
||||
static constexpr uint16_t _responseSize = 1024 + 128;
|
||||
|
||||
std::mutex _mutex;
|
||||
};
|
||||
@ -104,6 +104,104 @@
|
||||
|
||||
#define REACHABLE_THRESHOLD 2U
|
||||
|
||||
#define MAX_INVERTER_LIMIT 2250
|
||||
#define VEDIRECT_ENABLED false
|
||||
#define VEDIRECT_VERBOSE_LOGGING false
|
||||
#define VEDIRECT_UPDATESONLY true
|
||||
|
||||
#define POWERMETER_ENABLED false
|
||||
#define POWERMETER_INTERVAL 10
|
||||
#define POWERMETER_SOURCE 2
|
||||
#define POWERMETER_SDMBAUDRATE 9600
|
||||
#define POWERMETER_SDMADDRESS 1
|
||||
|
||||
|
||||
#define POWERLIMITER_ENABLED false
|
||||
#define POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED true
|
||||
#define POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES 3
|
||||
#define POWERLIMITER_BATTERY_DRAIN_STRATEGY 0
|
||||
#define POWERLIMITER_INTERVAL 10
|
||||
#define POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER true
|
||||
#define POWERLIMITER_INVERTER_ID 0
|
||||
#define POWERLIMITER_INVERTER_CHANNEL_ID 0
|
||||
#define POWERLIMITER_TARGET_POWER_CONSUMPTION 0
|
||||
#define POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS 0
|
||||
#define POWERLIMITER_LOWER_POWER_LIMIT 10
|
||||
#define POWERLIMITER_UPPER_POWER_LIMIT 800
|
||||
#define POWERLIMITER_BATTERY_SOC_START_THRESHOLD 80
|
||||
#define POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD 20
|
||||
#define POWERLIMITER_VOLTAGE_START_THRESHOLD 50.0
|
||||
#define POWERLIMITER_VOLTAGE_STOP_THRESHOLD 49.0
|
||||
#define POWERLIMITER_VOLTAGE_LOAD_CORRECTION_FACTOR 0.001
|
||||
#define POWERLIMITER_RESTART_HOUR -1
|
||||
#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_SOC 100
|
||||
#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_START_VOLTAGE 100.0
|
||||
#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_STOP_VOLTAGE 100.0
|
||||
|
||||
#define BATTERY_ENABLED false
|
||||
#define BATTERY_PROVIDER 0 // Pylontech CAN receiver
|
||||
#define BATTERY_JKBMS_INTERFACE 0
|
||||
#define BATTERY_JKBMS_POLLING_INTERVAL 5
|
||||
|
||||
#define HUAWEI_ENABLED false
|
||||
#define HUAWEI_CAN_CONTROLLER_FREQUENCY 8000000UL
|
||||
#define HUAWEI_AUTO_POWER_VOLTAGE_LIMIT 42.0
|
||||
#define HUAWEI_AUTO_POWER_ENABLE_VOLTAGE_LIMIT 42.0
|
||||
#define HUAWEI_AUTO_POWER_LOWER_POWER_LIMIT 150
|
||||
#define HUAWEI_AUTO_POWER_UPPER_POWER_LIMIT 2000
|
||||
|
||||
#define VERBOSE_LOGGING true
|
||||
|
||||
#define LED_BRIGHTNESS 100U
|
||||
|
||||
#define MAX_INVERTER_LIMIT 2250
|
||||
#define VEDIRECT_ENABLED false
|
||||
#define VEDIRECT_VERBOSE_LOGGING false
|
||||
#define VEDIRECT_UPDATESONLY true
|
||||
|
||||
#define POWERMETER_ENABLED false
|
||||
#define POWERMETER_INTERVAL 10
|
||||
#define POWERMETER_SOURCE 2
|
||||
#define POWERMETER_SDMBAUDRATE 9600
|
||||
#define POWERMETER_SDMADDRESS 1
|
||||
|
||||
|
||||
#define POWERLIMITER_ENABLED false
|
||||
#define POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED true
|
||||
#define POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES 3
|
||||
#define POWERLIMITER_BATTERY_DRAIN_STRATEGY 0
|
||||
#define POWERLIMITER_INTERVAL 10
|
||||
#define POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER true
|
||||
#define POWERLIMITER_INVERTER_ID 0
|
||||
#define POWERLIMITER_INVERTER_CHANNEL_ID 0
|
||||
#define POWERLIMITER_TARGET_POWER_CONSUMPTION 0
|
||||
#define POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS 0
|
||||
#define POWERLIMITER_LOWER_POWER_LIMIT 10
|
||||
#define POWERLIMITER_UPPER_POWER_LIMIT 800
|
||||
#define POWERLIMITER_BATTERY_SOC_START_THRESHOLD 80
|
||||
#define POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD 20
|
||||
#define POWERLIMITER_VOLTAGE_START_THRESHOLD 50.0
|
||||
#define POWERLIMITER_VOLTAGE_STOP_THRESHOLD 49.0
|
||||
#define POWERLIMITER_VOLTAGE_LOAD_CORRECTION_FACTOR 0.001
|
||||
#define POWERLIMITER_RESTART_HOUR -1
|
||||
#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_SOC 100
|
||||
#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_START_VOLTAGE 100.0
|
||||
#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_STOP_VOLTAGE 100.0
|
||||
|
||||
#define BATTERY_ENABLED false
|
||||
#define BATTERY_PROVIDER 0 // Pylontech CAN receiver
|
||||
#define BATTERY_JKBMS_INTERFACE 0
|
||||
#define BATTERY_JKBMS_POLLING_INTERVAL 5
|
||||
|
||||
#define HUAWEI_ENABLED false
|
||||
#define HUAWEI_CAN_CONTROLLER_FREQUENCY 8000000UL
|
||||
#define HUAWEI_AUTO_POWER_VOLTAGE_LIMIT 42.0
|
||||
#define HUAWEI_AUTO_POWER_ENABLE_VOLTAGE_LIMIT 42.0
|
||||
#define HUAWEI_AUTO_POWER_LOWER_POWER_LIMIT 150
|
||||
#define HUAWEI_AUTO_POWER_UPPER_POWER_LIMIT 2000
|
||||
|
||||
#define VERBOSE_LOGGING true
|
||||
|
||||
#define LED_BRIGHTNESS 100U
|
||||
|
||||
#define MAX_INVERTER_LIMIT 2250
|
||||
|
||||
BIN
lib/.DS_Store
vendored
Normal file
BIN
lib/Hoymiles/.DS_Store
vendored
Normal file
@ -263,6 +263,11 @@ void HoymilesClass::setPollInterval(const uint32_t interval)
|
||||
_pollInterval = interval;
|
||||
}
|
||||
|
||||
void HoymilesClass::setVerboseLogging(bool verboseLogging)
|
||||
{
|
||||
_verboseLogging = verboseLogging;
|
||||
}
|
||||
|
||||
void HoymilesClass::setMessageOutput(Print* output)
|
||||
{
|
||||
_messageOutput = output;
|
||||
@ -271,4 +276,19 @@ void HoymilesClass::setMessageOutput(Print* output)
|
||||
Print* HoymilesClass::getMessageOutput()
|
||||
{
|
||||
return _messageOutput;
|
||||
}
|
||||
}
|
||||
|
||||
class Silent : public Print {
|
||||
public:
|
||||
size_t write(uint8_t c) final { return 0; }
|
||||
};
|
||||
|
||||
Silent Dummy;
|
||||
|
||||
Print* HoymilesClass::getVerboseMessageOutput()
|
||||
{
|
||||
if (_verboseLogging) {
|
||||
return _messageOutput;
|
||||
}
|
||||
return &Dummy;
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ public:
|
||||
|
||||
void setMessageOutput(Print* output);
|
||||
Print* getMessageOutput();
|
||||
Print* getVerboseMessageOutput();
|
||||
|
||||
std::shared_ptr<InverterAbstract> addInverter(const char* name, const uint64_t serial);
|
||||
std::shared_ptr<InverterAbstract> getInverterByPos(const uint8_t pos);
|
||||
@ -35,6 +36,7 @@ public:
|
||||
|
||||
uint32_t PollInterval() const;
|
||||
void setPollInterval(const uint32_t interval);
|
||||
void setVerboseLogging(bool verboseLogging);
|
||||
|
||||
bool isAllRadioIdle() const;
|
||||
|
||||
@ -46,6 +48,7 @@ private:
|
||||
std::mutex _mutex;
|
||||
|
||||
uint32_t _pollInterval = 0;
|
||||
bool _verboseLogging = true;
|
||||
uint32_t _lastPoll = 0;
|
||||
|
||||
Print* _messageOutput = &Serial;
|
||||
|
||||
@ -54,7 +54,7 @@ void HoymilesRadio::sendLastPacketAgain()
|
||||
void HoymilesRadio::handleReceivedPackage()
|
||||
{
|
||||
if (_busyFlag && _rxTimeout.occured()) {
|
||||
Hoymiles.getMessageOutput()->println("RX Period End");
|
||||
Hoymiles.getVerboseMessageOutput()->println("RX Period End");
|
||||
std::shared_ptr<InverterAbstract> inv = Hoymiles.getInverterBySerial(_commandQueue.front().get()->getTargetAddress());
|
||||
|
||||
if (nullptr != inv) {
|
||||
@ -117,10 +117,10 @@ void HoymilesRadio::handleReceivedPackage()
|
||||
void HoymilesRadio::dumpBuf(const uint8_t buf[], const uint8_t len, const bool appendNewline)
|
||||
{
|
||||
for (uint8_t i = 0; i < len; i++) {
|
||||
Hoymiles.getMessageOutput()->printf("%02X ", buf[i]);
|
||||
Hoymiles.getVerboseMessageOutput()->printf("%02X ", buf[i]);
|
||||
}
|
||||
if (appendNewline) {
|
||||
Hoymiles.getMessageOutput()->println("");
|
||||
Hoymiles.getVerboseMessageOutput()->println("");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -126,7 +126,7 @@ void HoymilesRadio_CMT::loop()
|
||||
}
|
||||
|
||||
if (_packetReceived) {
|
||||
Hoymiles.getMessageOutput()->println("Interrupt received");
|
||||
Hoymiles.getVerboseMessageOutput()->println("Interrupt received");
|
||||
while (_radio->available()) {
|
||||
if (!(_rxBuffer.size() > FRAGMENT_BUFFER_SIZE)) {
|
||||
fragment_t f;
|
||||
@ -165,9 +165,9 @@ void HoymilesRadio_CMT::loop()
|
||||
|
||||
if (nullptr != inv) {
|
||||
// Save packet in inverter rx buffer
|
||||
Hoymiles.getMessageOutput()->printf("RX %.2f MHz --> ", getFrequencyFromChannel(f.channel) / 1000000.0);
|
||||
Hoymiles.getVerboseMessageOutput()->printf("RX %.2f MHz --> ", getFrequencyFromChannel(f.channel) / 1000000.0);
|
||||
dumpBuf(f.fragment, f.len, false);
|
||||
Hoymiles.getMessageOutput()->printf("| %d dBm\r\n", f.rssi);
|
||||
Hoymiles.getVerboseMessageOutput()->printf("| %d dBm\r\n", f.rssi);
|
||||
|
||||
inv->addRxFragment(f.fragment, f.len);
|
||||
} else {
|
||||
@ -271,9 +271,9 @@ void HoymilesRadio_CMT::sendEsbPacket(CommandAbstract& cmd)
|
||||
cmtSwitchDtuFreq(getInvBootFrequency());
|
||||
}
|
||||
|
||||
Hoymiles.getMessageOutput()->printf("TX %s %.2f MHz --> ",
|
||||
Hoymiles.getVerboseMessageOutput()->printf("TX %s %.2f MHz --> ",
|
||||
cmd.getCommandName().c_str(), getFrequencyFromChannel(_radio->getChannel()) / 1000000.0);
|
||||
cmd.dumpDataPayload(Hoymiles.getMessageOutput());
|
||||
cmd.dumpDataPayload(Hoymiles.getVerboseMessageOutput());
|
||||
|
||||
if (!_radio->write(cmd.getDataPayload(), cmd.getDataSize())) {
|
||||
Hoymiles.getMessageOutput()->println("TX SPI Timeout");
|
||||
|
||||
@ -48,7 +48,7 @@ void HoymilesRadio_NRF::loop()
|
||||
}
|
||||
|
||||
if (_packetReceived) {
|
||||
Hoymiles.getMessageOutput()->println("Interrupt received");
|
||||
Hoymiles.getVerboseMessageOutput()->println("Interrupt received");
|
||||
while (_radio->available()) {
|
||||
if (!(_rxBuffer.size() > FRAGMENT_BUFFER_SIZE)) {
|
||||
fragment_t f;
|
||||
@ -76,9 +76,9 @@ void HoymilesRadio_NRF::loop()
|
||||
|
||||
if (nullptr != inv) {
|
||||
// Save packet in inverter rx buffer
|
||||
Hoymiles.getMessageOutput()->printf("RX Channel: %d --> ", f.channel);
|
||||
Hoymiles.getVerboseMessageOutput()->printf("RX Channel: %d --> ", f.channel);
|
||||
dumpBuf(f.fragment, f.len, false);
|
||||
Hoymiles.getMessageOutput()->printf("| %d dBm\r\n", f.rssi);
|
||||
Hoymiles.getVerboseMessageOutput()->printf("| %d dBm\r\n", f.rssi);
|
||||
|
||||
inv->addRxFragment(f.fragment, f.len);
|
||||
} else {
|
||||
@ -183,9 +183,9 @@ void HoymilesRadio_NRF::sendEsbPacket(CommandAbstract& cmd)
|
||||
openWritingPipe(s);
|
||||
_radio->setRetries(3, 15);
|
||||
|
||||
Hoymiles.getMessageOutput()->printf("TX %s Channel: %d --> ",
|
||||
Hoymiles.getVerboseMessageOutput()->printf("TX %s Channel: %d --> ",
|
||||
cmd.getCommandName().c_str(), _radio->getChannel());
|
||||
cmd.dumpDataPayload(Hoymiles.getMessageOutput());
|
||||
cmd.dumpDataPayload(Hoymiles.getVerboseMessageOutput());
|
||||
_radio->write(cmd.getDataPayload(), cmd.getDataSize());
|
||||
|
||||
_radio->setRetries(0, 0);
|
||||
|
||||
@ -127,6 +127,10 @@ bool HM_Abstract::sendActivePowerControlRequest(float limit, const PowerLimitCon
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CMD_PENDING == SystemConfigPara()->getLastLimitCommandSuccess()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (type == PowerLimitControlType::RelativNonPersistent || type == PowerLimitControlType::RelativPersistent) {
|
||||
limit = min<float>(100, limit);
|
||||
}
|
||||
@ -154,6 +158,10 @@ bool HM_Abstract::sendPowerControlRequest(const bool turnOn)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CMD_PENDING == PowerCommand()->getLastPowerCommandSuccess()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (turnOn) {
|
||||
_powerState = 1;
|
||||
} else {
|
||||
|
||||
405
lib/SMLParser/sml.cpp
Normal file
@ -0,0 +1,405 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "sml.h"
|
||||
#include "smlCrcTable.h"
|
||||
|
||||
#ifdef SML_DEBUG
|
||||
char logBuff[200];
|
||||
|
||||
#ifdef SML_NATIVE
|
||||
#define SML_LOG(...) \
|
||||
do { \
|
||||
printf(__VA_ARGS__); \
|
||||
} while (0)
|
||||
#define SML_TREELOG(level, ...) \
|
||||
do { \
|
||||
printf("%.*s", level, " "); \
|
||||
printf(__VA_ARGS__); \
|
||||
} while (0)
|
||||
#elif ARDUINO
|
||||
#include <Arduino.h>
|
||||
#define SML_LOG(...) \
|
||||
do { \
|
||||
sprintf(logBuff, __VA_ARGS__); \
|
||||
Serial.print(logBuff); \
|
||||
} while (0)
|
||||
#define SML_TREELOG(level, ...) \
|
||||
do { \
|
||||
sprintf(logBuff, __VA_ARGS__); \
|
||||
Serial.print(logBuff); \
|
||||
} while (0)
|
||||
#endif
|
||||
|
||||
#else
|
||||
#define SML_LOG(...) \
|
||||
do { \
|
||||
} while (0)
|
||||
#define SML_TREELOG(level, ...) \
|
||||
do { \
|
||||
} while (0)
|
||||
#endif
|
||||
|
||||
#define MAX_LIST_SIZE 80
|
||||
#define MAX_TREE_SIZE 10
|
||||
|
||||
static sml_states_t currentState = SML_START;
|
||||
static char nodes[MAX_TREE_SIZE];
|
||||
static unsigned char currentLevel = 0;
|
||||
static unsigned short crc = 0xFFFF;
|
||||
static signed char sc;
|
||||
static unsigned short crcMine = 0xFFFF;
|
||||
static unsigned short crcReceived = 0x0000;
|
||||
static unsigned char len = 4;
|
||||
static unsigned char listBuffer[MAX_LIST_SIZE]; /* keeps a list
|
||||
as length + state + data */
|
||||
static unsigned char listPos = 0;
|
||||
|
||||
void crc16(unsigned char &byte)
|
||||
{
|
||||
#ifdef ARDUINO
|
||||
crc =
|
||||
pgm_read_word_near(&smlCrcTable[(byte ^ crc) & 0xff]) ^ (crc >> 8 & 0xff);
|
||||
#else
|
||||
crc = smlCrcTable[(byte ^ crc) & 0xff] ^ (crc >> 8 & 0xff);
|
||||
#endif
|
||||
}
|
||||
|
||||
void setState(sml_states_t state, int byteLen)
|
||||
{
|
||||
currentState = state;
|
||||
len = byteLen;
|
||||
}
|
||||
|
||||
void pushListBuffer(unsigned char byte)
|
||||
{
|
||||
if (listPos < MAX_LIST_SIZE) {
|
||||
listBuffer[listPos++] = byte;
|
||||
}
|
||||
}
|
||||
|
||||
void reduceList()
|
||||
{
|
||||
if (currentLevel <= MAX_TREE_SIZE && nodes[currentLevel] > 0)
|
||||
nodes[currentLevel]--;
|
||||
}
|
||||
|
||||
void smlNewList(unsigned char size)
|
||||
{
|
||||
reduceList();
|
||||
if (currentLevel < MAX_TREE_SIZE)
|
||||
currentLevel++;
|
||||
nodes[currentLevel] = size;
|
||||
SML_TREELOG(currentLevel, "LISTSTART on level %i with %i nodes\n",
|
||||
currentLevel, size);
|
||||
setState(SML_LISTSTART, size);
|
||||
// @todo workaround for lists inside obis lists
|
||||
if (size > 5) {
|
||||
listPos = 0;
|
||||
memset(listBuffer, '\0', MAX_LIST_SIZE);
|
||||
}
|
||||
else {
|
||||
pushListBuffer(size);
|
||||
pushListBuffer(currentState);
|
||||
}
|
||||
}
|
||||
|
||||
void checkMagicByte(unsigned char &byte)
|
||||
{
|
||||
unsigned int size = 0;
|
||||
while (currentLevel > 0 && nodes[currentLevel] == 0) {
|
||||
/* go back in tree if no nodes remaining */
|
||||
SML_TREELOG(currentLevel, "back to previous list\n");
|
||||
currentLevel--;
|
||||
}
|
||||
if (byte > 0x70 && byte <= 0x7F) {
|
||||
/* new list */
|
||||
size = byte & 0x0F;
|
||||
smlNewList(size);
|
||||
}
|
||||
else if (byte >= 0x01 && byte <= 0x6F && nodes[currentLevel] > 0) {
|
||||
if (byte == 0x01) {
|
||||
/* no data, get next */
|
||||
SML_TREELOG(currentLevel, " Data %i (empty)\n", nodes[currentLevel]);
|
||||
pushListBuffer(0);
|
||||
pushListBuffer(currentState);
|
||||
if (nodes[currentLevel] == 1) {
|
||||
setState(SML_LISTEND, 1);
|
||||
SML_TREELOG(currentLevel, "LISTEND\n");
|
||||
}
|
||||
else {
|
||||
setState(SML_NEXT, 1);
|
||||
}
|
||||
}
|
||||
else {
|
||||
size = (byte & 0x0F) - 1;
|
||||
setState(SML_DATA, size);
|
||||
if ((byte & 0xF0) == 0x50) {
|
||||
setState(SML_DATA_SIGNED_INT, size);
|
||||
}
|
||||
else if ((byte & 0xF0) == 0x60) {
|
||||
setState(SML_DATA_UNSIGNED_INT, size);
|
||||
}
|
||||
else if ((byte & 0xF0) == 0x00) {
|
||||
setState(SML_DATA_OCTET_STRING, size);
|
||||
}
|
||||
SML_TREELOG(currentLevel,
|
||||
" Data %i (length = %i%s): ", nodes[currentLevel], size,
|
||||
(currentState == SML_DATA_SIGNED_INT) ? ", signed int"
|
||||
: (currentState == SML_DATA_UNSIGNED_INT) ? ", unsigned int"
|
||||
: (currentState == SML_DATA_OCTET_STRING) ? ", octet string"
|
||||
: "");
|
||||
pushListBuffer(size);
|
||||
pushListBuffer(currentState);
|
||||
}
|
||||
reduceList();
|
||||
}
|
||||
else if (byte == 0x00) {
|
||||
/* end of block */
|
||||
reduceList();
|
||||
SML_TREELOG(currentLevel, "End of block at level %i\n", currentLevel);
|
||||
if (currentLevel == 0) {
|
||||
setState(SML_NEXT, 1);
|
||||
}
|
||||
else {
|
||||
setState(SML_BLOCKEND, 1);
|
||||
}
|
||||
}
|
||||
else if (byte & 0x80) {
|
||||
// MSB bit is set, another TL byte will follow
|
||||
if (byte >= 0x80 && byte <= 0x8F) {
|
||||
// Datatype Octet String
|
||||
setState(SML_HDATA, (byte & 0x0F) << 4);
|
||||
}
|
||||
else if (byte >= 0xF0 /*&& byte <= 0xFF*/) {
|
||||
/* Datatype List of ...*/
|
||||
setState(SML_LISTEXTENDED, (byte & 0x0F) << 4);
|
||||
}
|
||||
}
|
||||
else if (byte == 0x1B && currentLevel == 0) {
|
||||
/* end sequence */
|
||||
setState(SML_END, 3);
|
||||
}
|
||||
else {
|
||||
/* Unexpected Byte */
|
||||
SML_TREELOG(currentLevel,
|
||||
"UNEXPECTED magicbyte >%02X< at currentLevel %i\n", byte,
|
||||
currentLevel);
|
||||
setState(SML_UNEXPECTED, 4);
|
||||
}
|
||||
}
|
||||
|
||||
sml_states_t smlState(unsigned char ¤tByte)
|
||||
{
|
||||
unsigned char size;
|
||||
if (len > 0)
|
||||
len--;
|
||||
crc16(currentByte);
|
||||
switch (currentState) {
|
||||
case SML_UNEXPECTED:
|
||||
case SML_CHECKSUM_ERROR:
|
||||
case SML_FINAL:
|
||||
case SML_START:
|
||||
currentState = SML_START;
|
||||
currentLevel = 0; // Reset current level at the begin of a new transmission
|
||||
// to prevent problems
|
||||
if (currentByte != 0x1b)
|
||||
setState(SML_UNEXPECTED, 4);
|
||||
if (len == 0) {
|
||||
SML_TREELOG(0, "START\n");
|
||||
/* completely clean any garbage from crc checksum */
|
||||
crc = 0xFFFF;
|
||||
currentByte = 0x1b;
|
||||
crc16(currentByte);
|
||||
crc16(currentByte);
|
||||
crc16(currentByte);
|
||||
crc16(currentByte);
|
||||
setState(SML_VERSION, 4);
|
||||
}
|
||||
break;
|
||||
case SML_VERSION:
|
||||
if (currentByte != 0x01)
|
||||
setState(SML_UNEXPECTED, 4);
|
||||
if (len == 0) {
|
||||
setState(SML_BLOCKSTART, 1);
|
||||
}
|
||||
break;
|
||||
case SML_END:
|
||||
if (currentByte != 0x1b) {
|
||||
SML_LOG("UNEXPECTED char >%02X< at SML_END\n", currentByte);
|
||||
setState(SML_UNEXPECTED, 4);
|
||||
}
|
||||
if (len == 0) {
|
||||
setState(SML_CHECKSUM, 4);
|
||||
}
|
||||
break;
|
||||
case SML_CHECKSUM:
|
||||
// SML_LOG("CHECK: %02X\n", currentByte);
|
||||
if (len == 2) {
|
||||
crcMine = crc ^ 0xFFFF;
|
||||
}
|
||||
if (len == 1) {
|
||||
crcReceived += currentByte;
|
||||
}
|
||||
if (len == 0) {
|
||||
crcReceived = crcReceived | (currentByte << 8);
|
||||
SML_LOG("Received checksum: %02X\n", crcReceived);
|
||||
SML_LOG("Calculated checksum: %02X\n", crcMine);
|
||||
if (crcMine == crcReceived) {
|
||||
setState(SML_FINAL, 4);
|
||||
}
|
||||
else {
|
||||
setState(SML_CHECKSUM_ERROR, 4);
|
||||
}
|
||||
crc = 0xFFFF;
|
||||
crcReceived = 0x000; /* reset CRC */
|
||||
}
|
||||
break;
|
||||
case SML_HDATA:
|
||||
size = len + currentByte - 1;
|
||||
setState(SML_DATA, size);
|
||||
pushListBuffer(size);
|
||||
pushListBuffer(currentState);
|
||||
SML_TREELOG(currentLevel, " Data (length = %i): ", size);
|
||||
break;
|
||||
case SML_LISTEXTENDED:
|
||||
size = len + (currentByte & 0x0F);
|
||||
SML_TREELOG(currentLevel, "Extended List with Size=%i\n", size);
|
||||
smlNewList(size);
|
||||
break;
|
||||
case SML_DATA:
|
||||
case SML_DATA_SIGNED_INT:
|
||||
case SML_DATA_UNSIGNED_INT:
|
||||
case SML_DATA_OCTET_STRING:
|
||||
SML_LOG("%02X ", currentByte);
|
||||
pushListBuffer(currentByte);
|
||||
if (nodes[currentLevel] == 0 && len == 0) {
|
||||
SML_LOG("\n");
|
||||
SML_TREELOG(currentLevel, "LISTEND on level %i\n", currentLevel);
|
||||
currentState = SML_LISTEND;
|
||||
}
|
||||
else if (len == 0) {
|
||||
currentState = SML_DATAEND;
|
||||
SML_LOG("\n");
|
||||
}
|
||||
break;
|
||||
case SML_DATAEND:
|
||||
case SML_NEXT:
|
||||
case SML_LISTSTART:
|
||||
case SML_LISTEND:
|
||||
case SML_BLOCKSTART:
|
||||
case SML_BLOCKEND:
|
||||
checkMagicByte(currentByte);
|
||||
break;
|
||||
}
|
||||
return currentState;
|
||||
}
|
||||
|
||||
bool smlOBISCheck(const unsigned char *obis)
|
||||
{
|
||||
return (memcmp(obis, &listBuffer[2], 6) == 0);
|
||||
}
|
||||
|
||||
void smlOBISManufacturer(unsigned char *str, int maxSize)
|
||||
{
|
||||
int i = 0, pos = 0, size = 0;
|
||||
while (i < listPos) {
|
||||
size = (int)listBuffer[i];
|
||||
i++;
|
||||
pos++;
|
||||
if (pos == 6) {
|
||||
/* get manufacturer at position 6 in list */
|
||||
size = (size > maxSize - 1) ? maxSize : size;
|
||||
memcpy(str, &listBuffer[i + 1], size);
|
||||
str[size + 1] = 0;
|
||||
}
|
||||
i += size + 1;
|
||||
}
|
||||
}
|
||||
|
||||
void smlPow(double &val, signed char &scaler)
|
||||
{
|
||||
if (scaler < 0) {
|
||||
while (scaler++) {
|
||||
val /= 10;
|
||||
}
|
||||
}
|
||||
else {
|
||||
while (scaler--) {
|
||||
val *= 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void smlOBISByUnit(long long int &val, signed char &scaler, sml_units_t unit)
|
||||
{
|
||||
unsigned char i = 0, pos = 0, size = 0, y = 0, skip = 0;
|
||||
sml_states_t type;
|
||||
val = -1; /* unknown or error */
|
||||
while (i < listPos) {
|
||||
pos++;
|
||||
size = (int)listBuffer[i++];
|
||||
type = (sml_states_t)listBuffer[i++];
|
||||
if (type == SML_LISTSTART && size > 0) {
|
||||
// skip a list inside an obis list
|
||||
skip = size;
|
||||
while (skip > 0) {
|
||||
size = (int)listBuffer[i++];
|
||||
type = (sml_states_t)listBuffer[i++];
|
||||
i += size;
|
||||
skip--;
|
||||
}
|
||||
size = 0;
|
||||
}
|
||||
if (pos == 4 && listBuffer[i] != unit) {
|
||||
/* return unknown (-1) if unit does not match */
|
||||
return;
|
||||
}
|
||||
if (pos == 5) {
|
||||
scaler = listBuffer[i];
|
||||
}
|
||||
if (pos == 6) {
|
||||
y = size;
|
||||
// initialize 64bit signed integer based on MSB from received value
|
||||
val =
|
||||
(type == SML_DATA_SIGNED_INT && (listBuffer[i] & (1 << 7))) ? ~0 : 0;
|
||||
for (y = 0; y < size; y++) {
|
||||
// left shift received bytes to 64 bit signed integer
|
||||
val = (val << 8) | listBuffer[i + y];
|
||||
}
|
||||
}
|
||||
i += size;
|
||||
}
|
||||
}
|
||||
|
||||
void smlOBISWh(double &wh)
|
||||
{
|
||||
long long int val;
|
||||
smlOBISByUnit(val, sc, SML_WATT_HOUR);
|
||||
wh = val;
|
||||
smlPow(wh, sc);
|
||||
}
|
||||
|
||||
void smlOBISW(double &w)
|
||||
{
|
||||
long long int val;
|
||||
smlOBISByUnit(val, sc, SML_WATT);
|
||||
w = val;
|
||||
smlPow(w, sc);
|
||||
}
|
||||
|
||||
void smlOBISVolt(double &v)
|
||||
{
|
||||
long long int val;
|
||||
smlOBISByUnit(val, sc, SML_VOLT);
|
||||
v = val;
|
||||
smlPow(v, sc);
|
||||
}
|
||||
|
||||
void smlOBISAmpere(double &a)
|
||||
{
|
||||
long long int val;
|
||||
smlOBISByUnit(val, sc, SML_AMPERE);
|
||||
a = val;
|
||||
smlPow(a, sc);
|
||||
}
|
||||
106
lib/SMLParser/sml.h
Normal file
@ -0,0 +1,106 @@
|
||||
#ifndef SML_H
|
||||
#define SML_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef enum {
|
||||
SML_START,
|
||||
SML_END,
|
||||
SML_VERSION,
|
||||
SML_NEXT,
|
||||
SML_LISTSTART,
|
||||
SML_LISTEND,
|
||||
SML_LISTEXTENDED,
|
||||
SML_DATA,
|
||||
SML_HDATA,
|
||||
SML_DATAEND,
|
||||
SML_BLOCKSTART,
|
||||
SML_BLOCKEND,
|
||||
SML_CHECKSUM,
|
||||
SML_CHECKSUM_ERROR, /* calculated checksum does not match */
|
||||
SML_UNEXPECTED, /* unexpected byte received */
|
||||
SML_FINAL, /* final state, checksum OK */
|
||||
SML_DATA_SIGNED_INT,
|
||||
SML_DATA_UNSIGNED_INT,
|
||||
SML_DATA_OCTET_STRING,
|
||||
} sml_states_t;
|
||||
|
||||
typedef enum {
|
||||
SML_YEAR = 1,
|
||||
SML_MONTH = 2,
|
||||
SML_WEEK = 3,
|
||||
SML_DAY = 4,
|
||||
SML_HOUR = 5,
|
||||
SML_MIN = 6,
|
||||
SML_SECOND = 7,
|
||||
SML_DEGREE = 8,
|
||||
SML_DEGREE_CELSIUS = 9,
|
||||
SML_CURRENCY = 10,
|
||||
SML_METRE = 11,
|
||||
SML_METRE_PER_SECOND = 12,
|
||||
SML_CUBIC_METRE = 13,
|
||||
SML_CUBIC_METRE_CORRECTED = 14,
|
||||
SML_CUBIC_METRE_PER_HOUR = 15,
|
||||
SML_CUBIC_METRE_PER_HOUR_CORRECTED = 16,
|
||||
SML_CUBIC_METRE_PER_DAY = 17,
|
||||
SML_CUBIC_METRE_PER_DAY_CORRECTED = 18,
|
||||
SML_LITRE = 19,
|
||||
SML_KILOGRAM = 20,
|
||||
SML_NEWTON = 21,
|
||||
SML_NEWTONMETER = 22,
|
||||
SML_PASCAL = 23,
|
||||
SML_BAR = 24,
|
||||
SML_JOULE = 25,
|
||||
SML_JOULE_PER_HOUR = 26,
|
||||
SML_WATT = 27,
|
||||
SML_VOLT_AMPERE = 28,
|
||||
SML_VAR = 29,
|
||||
SML_WATT_HOUR = 30,
|
||||
SML_VOLT_AMPERE_HOUR = 31,
|
||||
SML_VAR_HOUR = 32,
|
||||
SML_AMPERE = 33,
|
||||
SML_COULOMB = 34,
|
||||
SML_VOLT = 35,
|
||||
SML_VOLT_PER_METRE = 36,
|
||||
SML_FARAD = 37,
|
||||
SML_OHM = 38,
|
||||
SML_OHM_METRE = 39,
|
||||
SML_WEBER = 40,
|
||||
SML_TESLA = 41,
|
||||
SML_AMPERE_PER_METRE = 42,
|
||||
SML_HENRY = 43,
|
||||
SML_HERTZ = 44,
|
||||
SML_ACTIVE_ENERGY_METER_CONSTANT_OR_PULSE_VALUE = 45,
|
||||
SML_REACTIVE_ENERGY_METER_CONSTANT_OR_PULSE_VALUE = 46,
|
||||
SML_APPARENT_ENERGY_METER_CONSTANT_OR_PULSE_VALUE = 47,
|
||||
SML_VOLT_SQUARED_HOURS = 48,
|
||||
SML_AMPERE_SQUARED_HOURS = 49,
|
||||
SML_KILOGRAM_PER_SECOND = 50,
|
||||
SML_KELVIN = 52,
|
||||
SML_VOLT_SQUARED_HOUR_METER_CONSTANT_OR_PULSE_VALUE = 53,
|
||||
SML_AMPERE_SQUARED_HOUR_METER_CONSTANT_OR_PULSE_VALUE = 54,
|
||||
SML_METER_CONSTANT_OR_PULSE_VALUE = 55,
|
||||
SML_PERCENTAGE = 56,
|
||||
SML_AMPERE_HOUR = 57,
|
||||
SML_ENERGY_PER_VOLUME = 60,
|
||||
SML_CALORIFIC_VALUE = 61,
|
||||
SML_MOLE_PERCENT = 62,
|
||||
SML_MASS_DENSITY = 63,
|
||||
SML_PASCAL_SECOND = 64,
|
||||
SML_RESERVED = 253,
|
||||
SML_OTHER_UNIT = 254,
|
||||
SML_COUNT = 255
|
||||
} sml_units_t;
|
||||
|
||||
sml_states_t smlState(unsigned char &byte);
|
||||
bool smlOBISCheck(const unsigned char *obis);
|
||||
void smlOBISManufacturer(unsigned char *str, int maxSize);
|
||||
void smlOBISByUnit(long long int &wh, signed char &scaler, sml_units_t unit);
|
||||
|
||||
// Be aware that double on Arduino UNO is just 32 bit
|
||||
void smlOBISWh(double &wh);
|
||||
void smlOBISW(double &w);
|
||||
void smlOBISVolt(double &v);
|
||||
void smlOBISAmpere(double &a);
|
||||
|
||||
#endif
|
||||
42
lib/SMLParser/smlCrcTable.h
Normal file
@ -0,0 +1,42 @@
|
||||
#ifndef SML_CRC_TABLE_H
|
||||
#define SML_CRC_TABLE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef ARDUINO
|
||||
#include <Arduino.h>
|
||||
static const uint16_t smlCrcTable[256] PROGMEM =
|
||||
#else
|
||||
static const uint16_t smlCrcTable[256] =
|
||||
#endif
|
||||
{0x0000, 0x1189, 0x2312, 0x329B, 0x4624, 0x57AD, 0x6536, 0x74BF, 0x8C48,
|
||||
0x9DC1, 0xAF5A, 0xBED3, 0xCA6C, 0xDBE5, 0xE97E, 0xF8F7, 0x1081, 0x0108,
|
||||
0x3393, 0x221A, 0x56A5, 0x472C, 0x75B7, 0x643E, 0x9CC9, 0x8D40, 0xBFDB,
|
||||
0xAE52, 0xDAED, 0xCB64, 0xF9FF, 0xE876, 0x2102, 0x308B, 0x0210, 0x1399,
|
||||
0x6726, 0x76AF, 0x4434, 0x55BD, 0xAD4A, 0xBCC3, 0x8E58, 0x9FD1, 0xEB6E,
|
||||
0xFAE7, 0xC87C, 0xD9F5, 0x3183, 0x200A, 0x1291, 0x0318, 0x77A7, 0x662E,
|
||||
0x54B5, 0x453C, 0xBDCB, 0xAC42, 0x9ED9, 0x8F50, 0xFBEF, 0xEA66, 0xD8FD,
|
||||
0xC974, 0x4204, 0x538D, 0x6116, 0x709F, 0x0420, 0x15A9, 0x2732, 0x36BB,
|
||||
0xCE4C, 0xDFC5, 0xED5E, 0xFCD7, 0x8868, 0x99E1, 0xAB7A, 0xBAF3, 0x5285,
|
||||
0x430C, 0x7197, 0x601E, 0x14A1, 0x0528, 0x37B3, 0x263A, 0xDECD, 0xCF44,
|
||||
0xFDDF, 0xEC56, 0x98E9, 0x8960, 0xBBFB, 0xAA72, 0x6306, 0x728F, 0x4014,
|
||||
0x519D, 0x2522, 0x34AB, 0x0630, 0x17B9, 0xEF4E, 0xFEC7, 0xCC5C, 0xDDD5,
|
||||
0xA96A, 0xB8E3, 0x8A78, 0x9BF1, 0x7387, 0x620E, 0x5095, 0x411C, 0x35A3,
|
||||
0x242A, 0x16B1, 0x0738, 0xFFCF, 0xEE46, 0xDCDD, 0xCD54, 0xB9EB, 0xA862,
|
||||
0x9AF9, 0x8B70, 0x8408, 0x9581, 0xA71A, 0xB693, 0xC22C, 0xD3A5, 0xE13E,
|
||||
0xF0B7, 0x0840, 0x19C9, 0x2B52, 0x3ADB, 0x4E64, 0x5FED, 0x6D76, 0x7CFF,
|
||||
0x9489, 0x8500, 0xB79B, 0xA612, 0xD2AD, 0xC324, 0xF1BF, 0xE036, 0x18C1,
|
||||
0x0948, 0x3BD3, 0x2A5A, 0x5EE5, 0x4F6C, 0x7DF7, 0x6C7E, 0xA50A, 0xB483,
|
||||
0x8618, 0x9791, 0xE32E, 0xF2A7, 0xC03C, 0xD1B5, 0x2942, 0x38CB, 0x0A50,
|
||||
0x1BD9, 0x6F66, 0x7EEF, 0x4C74, 0x5DFD, 0xB58B, 0xA402, 0x9699, 0x8710,
|
||||
0xF3AF, 0xE226, 0xD0BD, 0xC134, 0x39C3, 0x284A, 0x1AD1, 0x0B58, 0x7FE7,
|
||||
0x6E6E, 0x5CF5, 0x4D7C, 0xC60C, 0xD785, 0xE51E, 0xF497, 0x8028, 0x91A1,
|
||||
0xA33A, 0xB2B3, 0x4A44, 0x5BCD, 0x6956, 0x78DF, 0x0C60, 0x1DE9, 0x2F72,
|
||||
0x3EFB, 0xD68D, 0xC704, 0xF59F, 0xE416, 0x90A9, 0x8120, 0xB3BB, 0xA232,
|
||||
0x5AC5, 0x4B4C, 0x79D7, 0x685E, 0x1CE1, 0x0D68, 0x3FF3, 0x2E7A, 0xE70E,
|
||||
0xF687, 0xC41C, 0xD595, 0xA12A, 0xB0A3, 0x8238, 0x93B1, 0x6B46, 0x7ACF,
|
||||
0x4854, 0x59DD, 0x2D62, 0x3CEB, 0x0E70, 0x1FF9, 0xF78F, 0xE606, 0xD49D,
|
||||
0xC514, 0xB1AB, 0xA022, 0x92B9, 0x8330, 0x7BC7, 0x6A4E, 0x58D5, 0x495C,
|
||||
0x3DE3, 0x2C6A, 0x1EF1, 0x0F78};
|
||||
|
||||
#endif
|
||||
254
lib/SdmEnergyMeter/SDM.cpp
Normal file
@ -0,0 +1,254 @@
|
||||
/* Library for reading SDM 72/120/220/230/630 Modbus Energy meters.
|
||||
* Reading via Hardware or Software Serial library & rs232<->rs485 converter
|
||||
* 2016-2022 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core)
|
||||
* crc calculation by Jaime García (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino/)
|
||||
*/
|
||||
//------------------------------------------------------------------------------
|
||||
#include "SDM.h"
|
||||
//------------------------------------------------------------------------------
|
||||
#if defined ( USE_HARDWARESERIAL )
|
||||
#if defined ( ESP8266 )
|
||||
SDM::SDM(HardwareSerial& serial, long baud, int dere_pin, int config, bool swapuart) : sdmSer(serial) {
|
||||
this->_baud = baud;
|
||||
this->_dere_pin = dere_pin;
|
||||
this->_config = config;
|
||||
this->_swapuart = swapuart;
|
||||
}
|
||||
#elif defined ( ESP32 )
|
||||
SDM::SDM(HardwareSerial& serial, long baud, int dere_pin, int config, int8_t rx_pin, int8_t tx_pin) : sdmSer(serial) {
|
||||
this->_baud = baud;
|
||||
this->_dere_pin = dere_pin;
|
||||
this->_config = config;
|
||||
this->_rx_pin = rx_pin;
|
||||
this->_tx_pin = tx_pin;
|
||||
}
|
||||
#else
|
||||
SDM::SDM(HardwareSerial& serial, long baud, int dere_pin, int config) : sdmSer(serial) {
|
||||
this->_baud = baud;
|
||||
this->_dere_pin = dere_pin;
|
||||
this->_config = config;
|
||||
}
|
||||
#endif
|
||||
#else
|
||||
#if defined ( ESP8266 ) || defined ( ESP32 )
|
||||
SDM::SDM(SoftwareSerial& serial, long baud, int dere_pin, int config, int8_t rx_pin, int8_t tx_pin) : sdmSer(serial) {
|
||||
this->_baud = baud;
|
||||
this->_dere_pin = dere_pin;
|
||||
this->_config = config;
|
||||
this->_rx_pin = rx_pin;
|
||||
this->_tx_pin = tx_pin;
|
||||
}
|
||||
#else
|
||||
SDM::SDM(SoftwareSerial& serial, long baud, int dere_pin) : sdmSer(serial) {
|
||||
this->_baud = baud;
|
||||
this->_dere_pin = dere_pin;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
SDM::~SDM() {
|
||||
}
|
||||
|
||||
void SDM::begin(void) {
|
||||
#if defined ( USE_HARDWARESERIAL )
|
||||
#if defined ( ESP8266 )
|
||||
sdmSer.begin(_baud, (SerialConfig)_config);
|
||||
#elif defined ( ESP32 )
|
||||
sdmSer.begin(_baud, _config, _rx_pin, _tx_pin);
|
||||
#else
|
||||
sdmSer.begin(_baud, _config);
|
||||
#endif
|
||||
#else
|
||||
#if defined ( ESP8266 ) || defined ( ESP32 )
|
||||
sdmSer.begin(_baud, (SoftwareSerialConfig)_config, _rx_pin, _tx_pin);
|
||||
#else
|
||||
sdmSer.begin(_baud);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined ( USE_HARDWARESERIAL ) && defined ( ESP8266 )
|
||||
if (_swapuart)
|
||||
sdmSer.swap();
|
||||
#endif
|
||||
if (_dere_pin != NOT_A_PIN) {
|
||||
pinMode(_dere_pin, OUTPUT); //set output pin mode for DE/RE pin when used (for control MAX485)
|
||||
}
|
||||
dereSet(LOW); //set init state to receive from SDM -> DE Disable, /RE Enable (for control MAX485)
|
||||
}
|
||||
|
||||
float SDM::readVal(uint16_t reg, uint8_t node) {
|
||||
uint16_t temp;
|
||||
unsigned long resptime;
|
||||
uint8_t sdmarr[FRAMESIZE] = {node, SDM_B_02, 0, 0, SDM_B_05, SDM_B_06, 0, 0, 0};
|
||||
float res = NAN;
|
||||
uint16_t readErr = SDM_ERR_NO_ERROR;
|
||||
|
||||
sdmarr[2] = highByte(reg);
|
||||
sdmarr[3] = lowByte(reg);
|
||||
|
||||
temp = calculateCRC(sdmarr, FRAMESIZE - 3); //calculate out crc only from first 6 bytes
|
||||
|
||||
sdmarr[6] = lowByte(temp);
|
||||
sdmarr[7] = highByte(temp);
|
||||
|
||||
#if !defined ( USE_HARDWARESERIAL )
|
||||
sdmSer.listen(); //enable softserial rx interrupt
|
||||
#endif
|
||||
|
||||
flush(); //read serial if any old data is available
|
||||
|
||||
dereSet(HIGH); //transmit to SDM -> DE Enable, /RE Disable (for control MAX485)
|
||||
|
||||
delay(2); //fix for issue (nan reading) by sjfaustino: https://github.com/reaper7/SDM_Energy_Meter/issues/7#issuecomment-272111524
|
||||
|
||||
sdmSer.write(sdmarr, FRAMESIZE - 1); //send 8 bytes
|
||||
|
||||
sdmSer.flush(); //clear out tx buffer
|
||||
|
||||
dereSet(LOW); //receive from SDM -> DE Disable, /RE Enable (for control MAX485)
|
||||
|
||||
resptime = millis();
|
||||
|
||||
while (sdmSer.available() < FRAMESIZE) {
|
||||
if (millis() - resptime > msturnaround) {
|
||||
readErr = SDM_ERR_TIMEOUT; //err debug (4)
|
||||
break;
|
||||
}
|
||||
yield();
|
||||
}
|
||||
|
||||
if (readErr == SDM_ERR_NO_ERROR) { //if no timeout...
|
||||
|
||||
if (sdmSer.available() >= FRAMESIZE) {
|
||||
|
||||
for(int n=0; n<FRAMESIZE; n++) {
|
||||
sdmarr[n] = sdmSer.read();
|
||||
}
|
||||
|
||||
if (sdmarr[0] == node && sdmarr[1] == SDM_B_02 && sdmarr[2] == SDM_REPLY_BYTE_COUNT) {
|
||||
|
||||
if ((calculateCRC(sdmarr, FRAMESIZE - 2)) == ((sdmarr[8] << 8) | sdmarr[7])) { //calculate crc from first 7 bytes and compare with received crc (bytes 7 & 8)
|
||||
((uint8_t*)&res)[3]= sdmarr[3];
|
||||
((uint8_t*)&res)[2]= sdmarr[4];
|
||||
((uint8_t*)&res)[1]= sdmarr[5];
|
||||
((uint8_t*)&res)[0]= sdmarr[6];
|
||||
} else {
|
||||
readErr = SDM_ERR_CRC_ERROR; //err debug (1)
|
||||
}
|
||||
|
||||
} else {
|
||||
readErr = SDM_ERR_WRONG_BYTES; //err debug (2)
|
||||
}
|
||||
|
||||
} else {
|
||||
readErr = SDM_ERR_NOT_ENOUGHT_BYTES; //err debug (3)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
flush(mstimeout); //read serial if any old data is available and wait for RESPONSE_TIMEOUT (in ms)
|
||||
|
||||
if (sdmSer.available()) //if serial rx buffer (after RESPONSE_TIMEOUT) still contains data then something spam rs485, check node(s) or increase RESPONSE_TIMEOUT
|
||||
readErr = SDM_ERR_TIMEOUT; //err debug (4) but returned value may be correct
|
||||
|
||||
if (readErr != SDM_ERR_NO_ERROR) { //if error then copy temp error value to global val and increment global error counter
|
||||
readingerrcode = readErr;
|
||||
readingerrcount++;
|
||||
} else {
|
||||
++readingsuccesscount;
|
||||
}
|
||||
|
||||
#if !defined ( USE_HARDWARESERIAL )
|
||||
sdmSer.stopListening(); //disable softserial rx interrupt
|
||||
#endif
|
||||
|
||||
return (res);
|
||||
}
|
||||
|
||||
uint16_t SDM::getErrCode(bool _clear) {
|
||||
uint16_t _tmp = readingerrcode;
|
||||
if (_clear == true)
|
||||
clearErrCode();
|
||||
return (_tmp);
|
||||
}
|
||||
|
||||
uint32_t SDM::getErrCount(bool _clear) {
|
||||
uint32_t _tmp = readingerrcount;
|
||||
if (_clear == true)
|
||||
clearErrCount();
|
||||
return (_tmp);
|
||||
}
|
||||
|
||||
uint32_t SDM::getSuccCount(bool _clear) {
|
||||
uint32_t _tmp = readingsuccesscount;
|
||||
if (_clear == true)
|
||||
clearSuccCount();
|
||||
return (_tmp);
|
||||
}
|
||||
|
||||
void SDM::clearErrCode() {
|
||||
readingerrcode = SDM_ERR_NO_ERROR;
|
||||
}
|
||||
|
||||
void SDM::clearErrCount() {
|
||||
readingerrcount = 0;
|
||||
}
|
||||
|
||||
void SDM::clearSuccCount() {
|
||||
readingsuccesscount = 0;
|
||||
}
|
||||
|
||||
void SDM::setMsTurnaround(uint16_t _msturnaround) {
|
||||
if (_msturnaround < SDM_MIN_DELAY)
|
||||
msturnaround = SDM_MIN_DELAY;
|
||||
else if (_msturnaround > SDM_MAX_DELAY)
|
||||
msturnaround = SDM_MAX_DELAY;
|
||||
else
|
||||
msturnaround = _msturnaround;
|
||||
}
|
||||
|
||||
void SDM::setMsTimeout(uint16_t _mstimeout) {
|
||||
if (_mstimeout < SDM_MIN_DELAY)
|
||||
mstimeout = SDM_MIN_DELAY;
|
||||
else if (_mstimeout > SDM_MAX_DELAY)
|
||||
mstimeout = SDM_MAX_DELAY;
|
||||
else
|
||||
mstimeout = _mstimeout;
|
||||
}
|
||||
|
||||
uint16_t SDM::getMsTurnaround() {
|
||||
return (msturnaround);
|
||||
}
|
||||
|
||||
uint16_t SDM::getMsTimeout() {
|
||||
return (mstimeout);
|
||||
}
|
||||
|
||||
uint16_t SDM::calculateCRC(uint8_t *array, uint8_t len) {
|
||||
uint16_t _crc, _flag;
|
||||
_crc = 0xFFFF;
|
||||
for (uint8_t i = 0; i < len; i++) {
|
||||
_crc ^= (uint16_t)array[i];
|
||||
for (uint8_t j = 8; j; j--) {
|
||||
_flag = _crc & 0x0001;
|
||||
_crc >>= 1;
|
||||
if (_flag)
|
||||
_crc ^= 0xA001;
|
||||
}
|
||||
}
|
||||
return _crc;
|
||||
}
|
||||
|
||||
void SDM::flush(unsigned long _flushtime) {
|
||||
unsigned long flushstart = millis();
|
||||
while (sdmSer.available() || (millis() - flushstart < _flushtime)) {
|
||||
if (sdmSer.available()) //read serial if any old data is available
|
||||
sdmSer.read();
|
||||
delay(1);
|
||||
}
|
||||
}
|
||||
|
||||
void SDM::dereSet(bool _state) {
|
||||
if (_dere_pin != NOT_A_PIN)
|
||||
digitalWrite(_dere_pin, _state); //receive from SDM -> DE Disable, /RE Enable (for control MAX485)
|
||||
}
|
||||
299
lib/SdmEnergyMeter/SDM.h
Normal file
@ -0,0 +1,299 @@
|
||||
/* Library for reading SDM 72/120/220/230/630 Modbus Energy meters.
|
||||
* Reading via Hardware or Software Serial library & rs232<->rs485 converter
|
||||
* 2016-2022 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core)
|
||||
* crc calculation by Jaime García (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino/)
|
||||
*/
|
||||
//------------------------------------------------------------------------------
|
||||
#ifndef SDM_h
|
||||
#define SDM_h
|
||||
//------------------------------------------------------------------------------
|
||||
#include <Arduino.h>
|
||||
#include <SDM_Config_User.h>
|
||||
#if defined ( USE_HARDWARESERIAL )
|
||||
#include <HardwareSerial.h>
|
||||
#else
|
||||
#include <SoftwareSerial.h>
|
||||
#endif
|
||||
//------------------------------------------------------------------------------
|
||||
//DEFAULT CONFIG (DO NOT CHANGE ANYTHING!!! for changes use SDM_Config_User.h):
|
||||
//------------------------------------------------------------------------------
|
||||
#if !defined ( SDM_UART_BAUD )
|
||||
#define SDM_UART_BAUD 4800 // default baudrate
|
||||
#endif
|
||||
|
||||
#if !defined ( DERE_PIN )
|
||||
#define DERE_PIN NOT_A_PIN // default digital pin for control MAX485 DE/RE lines (connect DE & /RE together to this pin)
|
||||
#endif
|
||||
|
||||
#if defined ( USE_HARDWARESERIAL )
|
||||
|
||||
#if !defined ( SDM_UART_CONFIG )
|
||||
#define SDM_UART_CONFIG SERIAL_8N1 // default hardware uart config
|
||||
#endif
|
||||
|
||||
#if defined ( ESP8266 ) && !defined ( SWAPHWSERIAL )
|
||||
#define SWAPHWSERIAL 0 // (only esp8266) when hwserial used, then swap uart pins from 3/1 to 13/15 (default not swap)
|
||||
#endif
|
||||
|
||||
#if defined ( ESP32 )
|
||||
#if !defined ( SDM_RX_PIN )
|
||||
#define SDM_RX_PIN -1 // use default rx pin for selected port
|
||||
#endif
|
||||
#if !defined ( SDM_TX_PIN )
|
||||
#define SDM_TX_PIN -1 // use default tx pin for selected port
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#else
|
||||
|
||||
#if defined ( ESP8266 ) || defined ( ESP32 )
|
||||
#if !defined ( SDM_UART_CONFIG )
|
||||
#define SDM_UART_CONFIG SWSERIAL_8N1 // default softwareware uart config for esp8266/esp32
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// #if !defined ( SDM_RX_PIN ) || !defined ( SDM_TX_PIN )
|
||||
// #error "SDM_RX_PIN and SDM_TX_PIN must be defined in SDM_Config_User.h for Software Serial option)"
|
||||
// #endif
|
||||
|
||||
#if !defined ( SDM_RX_PIN )
|
||||
#define SDM_RX_PIN -1
|
||||
#endif
|
||||
#if !defined ( SDM_TX_PIN )
|
||||
#define SDM_TX_PIN -1
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
#if !defined ( WAITING_TURNAROUND_DELAY )
|
||||
#define WAITING_TURNAROUND_DELAY 200 // time in ms to wait for process current request
|
||||
#endif
|
||||
|
||||
#if !defined ( RESPONSE_TIMEOUT )
|
||||
#define RESPONSE_TIMEOUT 500 // time in ms to wait for return response from all devices before next request
|
||||
#endif
|
||||
|
||||
#if !defined ( SDM_MIN_DELAY )
|
||||
#define SDM_MIN_DELAY 20 // minimum value (in ms) for WAITING_TURNAROUND_DELAY and RESPONSE_TIMEOUT
|
||||
#endif
|
||||
|
||||
#if !defined ( SDM_MAX_DELAY )
|
||||
#define SDM_MAX_DELAY 5000 // maximum value (in ms) for WAITING_TURNAROUND_DELAY and RESPONSE_TIMEOUT
|
||||
#endif
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
#define SDM_ERR_NO_ERROR 0 // no error
|
||||
#define SDM_ERR_CRC_ERROR 1 // crc error
|
||||
#define SDM_ERR_WRONG_BYTES 2 // bytes b0,b1 or b2 wrong
|
||||
#define SDM_ERR_NOT_ENOUGHT_BYTES 3 // not enough bytes from sdm
|
||||
#define SDM_ERR_TIMEOUT 4 // timeout
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
#define FRAMESIZE 9 // size of out/in array
|
||||
#define SDM_REPLY_BYTE_COUNT 0x04 // number of bytes with data
|
||||
|
||||
#define SDM_B_01 0x01 // BYTE 1 -> slave address (default value 1 read from node 1)
|
||||
#define SDM_B_02 0x04 // BYTE 2 -> function code (default value 0x04 read from 3X input registers)
|
||||
#define SDM_B_05 0x00 // BYTE 5
|
||||
#define SDM_B_06 0x02 // BYTE 6
|
||||
// BYTES 3 & 4 (BELOW)
|
||||
|
||||
//---------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
// REGISTERS LIST FOR SDM DEVICES |
|
||||
//---------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
// REGISTER NAME REGISTER ADDRESS UNIT | SDM630 | SDM230 | SDM220 | SDM120CT| SDM120 | SDM72D | SDM72 V2|
|
||||
//---------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
#define SDM_PHASE_1_VOLTAGE 0x0000 // V | 1 | 1 | 1 | 1 | 1 | | 1 |
|
||||
#define SDM_PHASE_2_VOLTAGE 0x0002 // V | 1 | | | | | | 1 |
|
||||
#define SDM_PHASE_3_VOLTAGE 0x0004 // V | 1 | | | | | | 1 |
|
||||
#define SDM_PHASE_1_CURRENT 0x0006 // A | 1 | 1 | 1 | 1 | 1 | | 1 |
|
||||
#define SDM_PHASE_2_CURRENT 0x0008 // A | 1 | | | | | | 1 |
|
||||
#define SDM_PHASE_3_CURRENT 0x000A // A | 1 | | | | | | 1 |
|
||||
#define SDM_PHASE_1_POWER 0x000C // W | 1 | 1 | 1 | 1 | 1 | | 1 |
|
||||
#define SDM_PHASE_2_POWER 0x000E // W | 1 | | | | | | 1 |
|
||||
#define SDM_PHASE_3_POWER 0x0010 // W | 1 | | | | | | 1 |
|
||||
#define SDM_PHASE_1_APPARENT_POWER 0x0012 // VA | 1 | 1 | 1 | 1 | 1 | | 1 |
|
||||
#define SDM_PHASE_2_APPARENT_POWER 0x0014 // VA | 1 | | | | | | 1 |
|
||||
#define SDM_PHASE_3_APPARENT_POWER 0x0016 // VA | 1 | | | | | | 1 |
|
||||
#define SDM_PHASE_1_REACTIVE_POWER 0x0018 // VAr | 1 | 1 | 1 | 1 | 1 | | 1 |
|
||||
#define SDM_PHASE_2_REACTIVE_POWER 0x001A // VAr | 1 | | | | | | 1 |
|
||||
#define SDM_PHASE_3_REACTIVE_POWER 0x001C // VAr | 1 | | | | | | 1 |
|
||||
#define SDM_PHASE_1_POWER_FACTOR 0x001E // | 1 | 1 | 1 | 1 | 1 | | 1 |
|
||||
#define SDM_PHASE_2_POWER_FACTOR 0x0020 // | 1 | | | | | | 1 |
|
||||
#define SDM_PHASE_3_POWER_FACTOR 0x0022 // | 1 | | | | | | 1 |
|
||||
#define SDM_PHASE_1_ANGLE 0x0024 // Degrees | 1 | 1 | 1 | 1 | | | |
|
||||
#define SDM_PHASE_2_ANGLE 0x0026 // Degrees | 1 | | | | | | |
|
||||
#define SDM_PHASE_3_ANGLE 0x0028 // Degrees | 1 | | | | | | |
|
||||
#define SDM_AVERAGE_L_TO_N_VOLTS 0x002A // V | 1 | | | | | | 1 |
|
||||
#define SDM_AVERAGE_LINE_CURRENT 0x002E // A | 1 | | | | | | 1 |
|
||||
#define SDM_SUM_LINE_CURRENT 0x0030 // A | 1 | | | | | | 1 |
|
||||
#define SDM_TOTAL_SYSTEM_POWER 0x0034 // W | 1 | | | | | 1 | 1 |
|
||||
#define SDM_TOTAL_SYSTEM_APPARENT_POWER 0x0038 // VA | 1 | | | | | | 1 |
|
||||
#define SDM_TOTAL_SYSTEM_REACTIVE_POWER 0x003C // VAr | 1 | | | | | | 1 |
|
||||
#define SDM_TOTAL_SYSTEM_POWER_FACTOR 0x003E // | 1 | | | | | | 1 |
|
||||
#define SDM_TOTAL_SYSTEM_PHASE_ANGLE 0x0042 // Degrees | 1 | | | | | | |
|
||||
#define SDM_FREQUENCY 0x0046 // Hz | 1 | 1 | 1 | 1 | 1 | | 1 |
|
||||
#define SDM_IMPORT_ACTIVE_ENERGY 0x0048 // kWh/MWh | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
|
||||
#define SDM_EXPORT_ACTIVE_ENERGY 0x004A // kWh/MWh | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
|
||||
#define SDM_IMPORT_REACTIVE_ENERGY 0x004C // kVArh/MVArh | 1 | 1 | 1 | 1 | 1 | | |
|
||||
#define SDM_EXPORT_REACTIVE_ENERGY 0x004E // kVArh/MVArh | 1 | 1 | 1 | 1 | 1 | | |
|
||||
#define SDM_VAH_SINCE_LAST_RESET 0x0050 // kVAh/MVAh | 1 | | | | | | |
|
||||
#define SDM_AH_SINCE_LAST_RESET 0x0052 // Ah/kAh | 1 | | | | | | |
|
||||
#define SDM_TOTAL_SYSTEM_POWER_DEMAND 0x0054 // W | 1 | 1 | | | | | |
|
||||
#define SDM_MAXIMUM_TOTAL_SYSTEM_POWER_DEMAND 0x0056 // W | 1 | 1 | | | | | |
|
||||
#define SDM_CURRENT_SYSTEM_POSITIVE_POWER_DEMAND 0x0058 // W | | 1 | | | | | |
|
||||
#define SDM_MAXIMUM_SYSTEM_POSITIVE_POWER_DEMAND 0x005A // W | | 1 | | | | | |
|
||||
#define SDM_CURRENT_SYSTEM_REVERSE_POWER_DEMAND 0x005C // W | | 1 | | | | | |
|
||||
#define SDM_MAXIMUM_SYSTEM_REVERSE_POWER_DEMAND 0x005E // W | | 1 | | | | | |
|
||||
#define SDM_TOTAL_SYSTEM_VA_DEMAND 0x0064 // VA | 1 | | | | | | |
|
||||
#define SDM_MAXIMUM_TOTAL_SYSTEM_VA_DEMAND 0x0066 // VA | 1 | | | | | | |
|
||||
#define SDM_NEUTRAL_CURRENT_DEMAND 0x0068 // A | 1 | | | | | | |
|
||||
#define SDM_MAXIMUM_NEUTRAL_CURRENT 0x006A // A | 1 | | | | | | |
|
||||
#define SDM_LINE_1_TO_LINE_2_VOLTS 0x00C8 // V | 1 | | | | | | 1 |
|
||||
#define SDM_LINE_2_TO_LINE_3_VOLTS 0x00CA // V | 1 | | | | | | 1 |
|
||||
#define SDM_LINE_3_TO_LINE_1_VOLTS 0x00CC // V | 1 | | | | | | 1 |
|
||||
#define SDM_AVERAGE_LINE_TO_LINE_VOLTS 0x00CE // V | 1 | | | | | | 1 |
|
||||
#define SDM_NEUTRAL_CURRENT 0x00E0 // A | 1 | | | | | | 1 |
|
||||
#define SDM_PHASE_1_LN_VOLTS_THD 0x00EA // % | 1 | | | | | | |
|
||||
#define SDM_PHASE_2_LN_VOLTS_THD 0x00EC // % | 1 | | | | | | |
|
||||
#define SDM_PHASE_3_LN_VOLTS_THD 0x00EE // % | 1 | | | | | | |
|
||||
#define SDM_PHASE_1_CURRENT_THD 0x00F0 // % | 1 | | | | | | |
|
||||
#define SDM_PHASE_2_CURRENT_THD 0x00F2 // % | 1 | | | | | | |
|
||||
#define SDM_PHASE_3_CURRENT_THD 0x00F4 // % | 1 | | | | | | |
|
||||
#define SDM_AVERAGE_LINE_TO_NEUTRAL_VOLTS_THD 0x00F8 // % | 1 | | | | | | |
|
||||
#define SDM_AVERAGE_LINE_CURRENT_THD 0x00FA // % | 1 | | | | | | |
|
||||
#define SDM_TOTAL_SYSTEM_POWER_FACTOR_INV 0x00FE // | 1 | | | | | | |
|
||||
#define SDM_PHASE_1_CURRENT_DEMAND 0x0102 // A | 1 | 1 | | | | | |
|
||||
#define SDM_PHASE_2_CURRENT_DEMAND 0x0104 // A | 1 | | | | | | |
|
||||
#define SDM_PHASE_3_CURRENT_DEMAND 0x0106 // A | 1 | | | | | | |
|
||||
#define SDM_MAXIMUM_PHASE_1_CURRENT_DEMAND 0x0108 // A | 1 | 1 | | | | | |
|
||||
#define SDM_MAXIMUM_PHASE_2_CURRENT_DEMAND 0x010A // A | 1 | | | | | | |
|
||||
#define SDM_MAXIMUM_PHASE_3_CURRENT_DEMAND 0x010C // A | 1 | | | | | | |
|
||||
#define SDM_LINE_1_TO_LINE_2_VOLTS_THD 0x014E // % | 1 | | | | | | |
|
||||
#define SDM_LINE_2_TO_LINE_3_VOLTS_THD 0x0150 // % | 1 | | | | | | |
|
||||
#define SDM_LINE_3_TO_LINE_1_VOLTS_THD 0x0152 // % | 1 | | | | | | |
|
||||
#define SDM_AVERAGE_LINE_TO_LINE_VOLTS_THD 0x0154 // % | 1 | | | | | | |
|
||||
#define SDM_TOTAL_ACTIVE_ENERGY 0x0156 // kWh | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
|
||||
#define SDM_TOTAL_REACTIVE_ENERGY 0x0158 // kVArh | 1 | 1 | 1 | 1 | 1 | | 1 |
|
||||
#define SDM_L1_IMPORT_ACTIVE_ENERGY 0x015A // kWh | 1 | | | | | | |
|
||||
#define SDM_L2_IMPORT_ACTIVE_ENERGY 0x015C // kWh | 1 | | | | | | |
|
||||
#define SDM_L3_IMPORT_ACTIVE_ENERGY 0x015E // kWh | 1 | | | | | | |
|
||||
#define SDM_L1_EXPORT_ACTIVE_ENERGY 0x0160 // kWh | 1 | | | | | | |
|
||||
#define SDM_L2_EXPORT_ACTIVE_ENERGY 0x0162 // kWh | 1 | | | | | | |
|
||||
#define SDM_L3_EXPORT_ACTIVE_ENERGY 0x0164 // kWh | 1 | | | | | | |
|
||||
#define SDM_L1_TOTAL_ACTIVE_ENERGY 0x0166 // kWh | 1 | | | | | | |
|
||||
#define SDM_L2_TOTAL_ACTIVE_ENERGY 0x0168 // kWh | 1 | | | | | | |
|
||||
#define SDM_L3_TOTAL_ACTIVE_ENERGY 0x016a // kWh | 1 | | | | | | |
|
||||
#define SDM_L1_IMPORT_REACTIVE_ENERGY 0x016C // kVArh | 1 | | | | | | |
|
||||
#define SDM_L2_IMPORT_REACTIVE_ENERGY 0x016E // kVArh | 1 | | | | | | |
|
||||
#define SDM_L3_IMPORT_REACTIVE_ENERGY 0x0170 // kVArh | 1 | | | | | | |
|
||||
#define SDM_L1_EXPORT_REACTIVE_ENERGY 0x0172 // kVArh | 1 | | | | | | |
|
||||
#define SDM_L2_EXPORT_REACTIVE_ENERGY 0x0174 // kVArh | 1 | | | | | | |
|
||||
#define SDM_L3_EXPORT_REACTIVE_ENERGY 0x0176 // kVArh | 1 | | | | | | |
|
||||
#define SDM_L1_TOTAL_REACTIVE_ENERGY 0x0178 // kVArh | 1 | | | | | | |
|
||||
#define SDM_L2_TOTAL_REACTIVE_ENERGY 0x017A // kVArh | 1 | | | | | | |
|
||||
#define SDM_L3_TOTAL_REACTIVE_ENERGY 0x017C // kVArh | 1 | | | | | | |
|
||||
#define SDM_CURRENT_RESETTABLE_TOTAL_ACTIVE_ENERGY 0x0180 // kWh | | 1 | | | | 1 | 1 |
|
||||
#define SDM_CURRENT_RESETTABLE_TOTAL_REACTIVE_ENERGY 0x0182 // kVArh | | 1 | | | | | |
|
||||
#define SDM_CURRENT_RESETTABLE_IMPORT_ENERGY 0x0184 // kWh | | | | | | 1 | 1 |
|
||||
#define SDM_CURRENT_RESETTABLE_EXPORT_ENERGY 0x0186 // kWh | | | | | | 1 | 1 |
|
||||
#define SDM_NET_KWH 0x018C // kWh | | | | | | | 1 |
|
||||
#define SDM_IMPORT_POWER 0x0500 // W | | | | | | 1 | 1 |
|
||||
#define SDM_EXPORT_POWER 0x0502 // W | | | | | | 1 | 1 |
|
||||
//---------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
// REGISTERS LIST FOR DDM DEVICE |
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
// REGISTER NAME REGISTER ADDRESS UNIT | DDM18SD |
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
#define DDM_PHASE_1_VOLTAGE 0x0000 // V | 1 |
|
||||
#define DDM_PHASE_1_CURRENT 0x0008 // A | 1 |
|
||||
#define DDM_PHASE_1_POWER 0x0012 // W | 1 |
|
||||
#define DDM_PHASE_1_REACTIVE_POWER 0x001A // VAr | 1 |
|
||||
#define DDM_PHASE_1_POWER_FACTOR 0x002A // | 1 |
|
||||
#define DDM_FREQUENCY 0x0036 // Hz | 1 |
|
||||
#define DDM_IMPORT_ACTIVE_ENERGY 0x0100 // kWh | 1 |
|
||||
#define DDM_IMPORT_REACTIVE_ENERGY 0x0400 // kVArh | 1 |
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
// REGISTERS LIST FOR DEVNAME DEVICE |
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
// REGISTER NAME REGISTER ADDRESS UNIT | DEVNAME |
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
//#define DEVNAME_VOLTAGE 0x0000 // V | 1 |
|
||||
//#define DEVNAME_CURRENT 0x0002 // A | 1 |
|
||||
//#define DEVNAME_POWER 0x0004 // W | 1 |
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
|
||||
//-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
class SDM {
|
||||
public:
|
||||
#if defined ( USE_HARDWARESERIAL ) // hardware serial
|
||||
#if defined ( ESP8266 ) // on esp8266
|
||||
SDM(HardwareSerial& serial, long baud = SDM_UART_BAUD, int dere_pin = DERE_PIN, int config = SDM_UART_CONFIG, bool swapuart = SWAPHWSERIAL);
|
||||
#elif defined ( ESP32 ) // on esp32
|
||||
SDM(HardwareSerial& serial, long baud = SDM_UART_BAUD, int dere_pin = DERE_PIN, int config = SDM_UART_CONFIG, int8_t rx_pin = SDM_RX_PIN, int8_t tx_pin = SDM_TX_PIN);
|
||||
#else // on avr
|
||||
SDM(HardwareSerial& serial, long baud = SDM_UART_BAUD, int dere_pin = DERE_PIN, int config = SDM_UART_CONFIG);
|
||||
#endif
|
||||
#else // software serial
|
||||
#if defined ( ESP8266 ) || defined ( ESP32 ) // on esp8266/esp32
|
||||
SDM(SoftwareSerial& serial, long baud = SDM_UART_BAUD, int dere_pin = DERE_PIN, int config = SDM_UART_CONFIG, int8_t rx_pin = SDM_RX_PIN, int8_t tx_pin = SDM_TX_PIN);
|
||||
#else // on avr
|
||||
SDM(SoftwareSerial& serial, long baud = SDM_UART_BAUD, int dere_pin = DERE_PIN);
|
||||
#endif
|
||||
#endif
|
||||
virtual ~SDM();
|
||||
|
||||
void begin(void);
|
||||
float readVal(uint16_t reg, uint8_t node = SDM_B_01); // read value from register = reg and from deviceId = node
|
||||
uint16_t getErrCode(bool _clear = false); // return last errorcode (optional clear this value, default flase)
|
||||
uint32_t getErrCount(bool _clear = false); // return total errors count (optional clear this value, default flase)
|
||||
uint32_t getSuccCount(bool _clear = false); // return total success count (optional clear this value, default false)
|
||||
void clearErrCode(); // clear last errorcode
|
||||
void clearErrCount(); // clear total errors count
|
||||
void clearSuccCount(); // clear total success count
|
||||
void setMsTurnaround(uint16_t _msturnaround = WAITING_TURNAROUND_DELAY); // set new value for WAITING_TURNAROUND_DELAY (ms), min=SDM_MIN_DELAY, max=SDM_MAX_DELAY
|
||||
void setMsTimeout(uint16_t _mstimeout = RESPONSE_TIMEOUT); // set new value for RESPONSE_TIMEOUT (ms), min=SDM_MIN_DELAY, max=SDM_MAX_DELAY
|
||||
uint16_t getMsTurnaround(); // get current value of WAITING_TURNAROUND_DELAY (ms)
|
||||
uint16_t getMsTimeout(); // get current value of RESPONSE_TIMEOUT (ms)
|
||||
|
||||
private:
|
||||
#if defined ( USE_HARDWARESERIAL )
|
||||
HardwareSerial& sdmSer;
|
||||
#else
|
||||
SoftwareSerial& sdmSer;
|
||||
#endif
|
||||
|
||||
#if defined ( USE_HARDWARESERIAL )
|
||||
int _config = SDM_UART_CONFIG;
|
||||
#if defined ( ESP8266 )
|
||||
bool _swapuart = SWAPHWSERIAL;
|
||||
#elif defined ( ESP32 )
|
||||
int8_t _rx_pin = -1;
|
||||
int8_t _tx_pin = -1;
|
||||
#endif
|
||||
#else
|
||||
#if defined ( ESP8266 ) || defined ( ESP32 )
|
||||
int _config = SDM_UART_CONFIG;
|
||||
#endif
|
||||
int8_t _rx_pin = -1;
|
||||
int8_t _tx_pin = -1;
|
||||
#endif
|
||||
long _baud = SDM_UART_BAUD;
|
||||
int _dere_pin = DERE_PIN;
|
||||
uint16_t readingerrcode = SDM_ERR_NO_ERROR; // 4 = timeout; 3 = not enough bytes; 2 = number of bytes OK but bytes b0,b1 or b2 wrong, 1 = crc error
|
||||
uint16_t msturnaround = WAITING_TURNAROUND_DELAY;
|
||||
uint16_t mstimeout = RESPONSE_TIMEOUT;
|
||||
uint32_t readingerrcount = 0; // total errors counter
|
||||
uint32_t readingsuccesscount = 0; // total success counter
|
||||
uint16_t calculateCRC(uint8_t *array, uint8_t len);
|
||||
void flush(unsigned long _flushtime = 0); // read serial if any old data is available or for a given time in ms
|
||||
void dereSet(bool _state = LOW); // for control MAX485 DE/RE pins, LOW receive from SDM, HIGH transmit to SDM
|
||||
};
|
||||
#endif // SDM_h
|
||||
93
lib/SdmEnergyMeter/SDM_Config_User.h
Normal file
@ -0,0 +1,93 @@
|
||||
/* Library for reading SDM 72/120/220/230/630 Modbus Energy meters.
|
||||
* Reading via Hardware or Software Serial library & rs232<->rs485 converter
|
||||
* 2016-2022 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core)
|
||||
* crc calculation by Jaime García (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino/)
|
||||
*/
|
||||
|
||||
/*
|
||||
* USER CONFIG:
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
* define or undefine USE_HARDWARESERIAL (uncomment only one or none)
|
||||
*/
|
||||
//#undef USE_HARDWARESERIAL
|
||||
#define USE_HARDWARESERIAL
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
* define user baudrate
|
||||
*/
|
||||
#define SDM_UART_BAUD 9600
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
* define user SDM_RX_PIN and SDM_TX_PIN for esp/avr Software Serial option
|
||||
* or ESP32 with Hardware Serial if default core pins are not suitable
|
||||
*/
|
||||
#if defined ( USE_HARDWARESERIAL )
|
||||
#if defined ( ESP32 )
|
||||
#define SDM_RX_PIN 13
|
||||
#define SDM_TX_PIN 32
|
||||
#endif
|
||||
#else
|
||||
#if defined ( ESP8266 ) || defined ( ESP32 )
|
||||
#define SDM_RX_PIN 13
|
||||
#define SDM_TX_PIN 15
|
||||
#else
|
||||
#define SDM_RX_PIN 10
|
||||
#define SDM_TX_PIN 11
|
||||
#endif
|
||||
#endif
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
* define user DERE_PIN for control MAX485 DE/RE lines (connect DE & /RE together to this pin)
|
||||
*/
|
||||
//#define DERE_PIN NOT_A_PIN
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
#if defined ( USE_HARDWARESERIAL )
|
||||
|
||||
/*
|
||||
* define user SDM_UART_CONFIG for hardware serial
|
||||
*/
|
||||
//#define SDM_UART_CONFIG SERIAL_8N1
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
* define user SWAPHWSERIAL, if true(1) then swap uart pins from 3/1 to 13/15 (only ESP8266)
|
||||
*/
|
||||
//#define SWAPHWSERIAL 0
|
||||
|
||||
#else
|
||||
|
||||
/*
|
||||
* define user SDM_UART_CONFIG for software serial
|
||||
*/
|
||||
//#define SDM_UART_CONFIG SWSERIAL_8N1
|
||||
|
||||
#endif
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
* define user WAITING_TURNAROUND_DELAY time in ms to wait for process current request
|
||||
*/
|
||||
//#define WAITING_TURNAROUND_DELAY 200
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
* define user RESPONSE_TIMEOUT time in ms to wait for return response from all devices before next request
|
||||
*/
|
||||
//#define RESPONSE_TIMEOUT 500
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
385
lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp
Normal file
@ -0,0 +1,385 @@
|
||||
/* framehandler.cpp
|
||||
*
|
||||
* Arduino library to read from Victron devices using VE.Direct protocol.
|
||||
* Derived from Victron framehandler reference implementation.
|
||||
*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2019 Victron Energy BV
|
||||
* Portions Copyright (C) 2020 Chris Terwilliger
|
||||
* https://github.com/cterwilliger/VeDirectFrameHandler
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* 2020.05.05 - 0.2 - initial release
|
||||
* 2020.06.21 - 0.2 - add MIT license, no code changes
|
||||
* 2020.08.20 - 0.3 - corrected #include reference
|
||||
*
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "VeDirectFrameHandler.h"
|
||||
|
||||
// The name of the record that contains the checksum.
|
||||
static constexpr char checksumTagName[] = "CHECKSUM";
|
||||
|
||||
// state machine
|
||||
enum States {
|
||||
IDLE = 1,
|
||||
RECORD_BEGIN = 2,
|
||||
RECORD_NAME = 3,
|
||||
RECORD_VALUE = 4,
|
||||
CHECKSUM = 5,
|
||||
RECORD_HEX = 6
|
||||
};
|
||||
|
||||
|
||||
|
||||
class Silent : public Print {
|
||||
public:
|
||||
size_t write(uint8_t c) final { return 0; }
|
||||
};
|
||||
|
||||
static Silent MessageOutputDummy;
|
||||
|
||||
VeDirectFrameHandler::VeDirectFrameHandler() :
|
||||
_msgOut(&MessageOutputDummy),
|
||||
_lastUpdate(0),
|
||||
_state(IDLE),
|
||||
_checksum(0),
|
||||
_textPointer(0),
|
||||
_hexSize(0),
|
||||
_name(""),
|
||||
_value(""),
|
||||
_debugIn(0),
|
||||
_lastByteMillis(0)
|
||||
{
|
||||
}
|
||||
|
||||
void VeDirectFrameHandler::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort)
|
||||
{
|
||||
_vedirectSerial = std::make_unique<HardwareSerial>(hwSerialPort);
|
||||
_vedirectSerial->begin(19200, SERIAL_8N1, rx, tx);
|
||||
_vedirectSerial->flush();
|
||||
_msgOut = msgOut;
|
||||
_verboseLogging = verboseLogging;
|
||||
_debugIn = 0;
|
||||
}
|
||||
|
||||
void VeDirectFrameHandler::dumpDebugBuffer() {
|
||||
_msgOut->printf("[VE.Direct] serial input (%d Bytes):", _debugIn);
|
||||
for (int i = 0; i < _debugIn; ++i) {
|
||||
if (i % 16 == 0) {
|
||||
_msgOut->printf("\r\n[VE.Direct]");
|
||||
}
|
||||
_msgOut->printf(" %02x", _debugBuffer[i]);
|
||||
}
|
||||
_msgOut->println("");
|
||||
_debugIn = 0;
|
||||
}
|
||||
|
||||
void VeDirectFrameHandler::loop()
|
||||
{
|
||||
while ( _vedirectSerial->available()) {
|
||||
rxData(_vedirectSerial->read());
|
||||
_lastByteMillis = millis();
|
||||
}
|
||||
|
||||
// there will never be a large gap between two bytes of the same frame.
|
||||
// if such a large gap is observed, reset the state machine so it tries
|
||||
// to decode a new frame once more data arrives.
|
||||
if (IDLE != _state && _lastByteMillis + 500 < millis()) {
|
||||
_msgOut->printf("[VE.Direct] Resetting state machine (was %d) after timeout\r\n", _state);
|
||||
if (_verboseLogging) { dumpDebugBuffer(); }
|
||||
_checksum = 0;
|
||||
_state = IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* rxData
|
||||
* This function is called by loop() which passes a byte of serial data
|
||||
* Based on Victron's example code. But using String and Map instead of pointer and arrays
|
||||
*/
|
||||
void VeDirectFrameHandler::rxData(uint8_t inbyte)
|
||||
{
|
||||
if (_verboseLogging) {
|
||||
_debugBuffer[_debugIn] = inbyte;
|
||||
_debugIn = (_debugIn + 1) % _debugBuffer.size();
|
||||
if (0 == _debugIn) {
|
||||
_msgOut->println("[VE.Direct] ERROR: debug buffer overrun!");
|
||||
}
|
||||
}
|
||||
|
||||
if ( (inbyte == ':') && (_state != CHECKSUM) ) {
|
||||
_prevState = _state; //hex frame can interrupt TEXT
|
||||
_state = RECORD_HEX;
|
||||
_hexSize = 0;
|
||||
}
|
||||
if (_state != RECORD_HEX) {
|
||||
_checksum += inbyte;
|
||||
}
|
||||
inbyte = toupper(inbyte);
|
||||
|
||||
switch(_state) {
|
||||
case IDLE:
|
||||
/* wait for \n of the start of an record */
|
||||
switch(inbyte) {
|
||||
case '\n':
|
||||
_state = RECORD_BEGIN;
|
||||
break;
|
||||
case '\r': /* Skip */
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case RECORD_BEGIN:
|
||||
_textPointer = _name;
|
||||
*_textPointer++ = inbyte;
|
||||
_state = RECORD_NAME;
|
||||
break;
|
||||
case RECORD_NAME:
|
||||
// The record name is being received, terminated by a \t
|
||||
switch(inbyte) {
|
||||
case '\t':
|
||||
// the Checksum record indicates a EOR
|
||||
if ( _textPointer < (_name + sizeof(_name)) ) {
|
||||
*_textPointer = 0; /* Zero terminate */
|
||||
if (strcmp(_name, checksumTagName) == 0) {
|
||||
_state = CHECKSUM;
|
||||
break;
|
||||
}
|
||||
}
|
||||
_textPointer = _value; /* Reset value pointer */
|
||||
_state = RECORD_VALUE;
|
||||
break;
|
||||
case '#': /* Ignore # from serial number*/
|
||||
break;
|
||||
default:
|
||||
// add byte to name, but do no overflow
|
||||
if ( _textPointer < (_name + sizeof(_name)) )
|
||||
*_textPointer++ = inbyte;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case RECORD_VALUE:
|
||||
// The record value is being received. The \r indicates a new record.
|
||||
switch(inbyte) {
|
||||
case '\n':
|
||||
if ( _textPointer < (_value + sizeof(_value)) ) {
|
||||
*_textPointer = 0; // make zero ended
|
||||
textRxEvent(_name, _value);
|
||||
}
|
||||
_state = RECORD_BEGIN;
|
||||
break;
|
||||
case '\r': /* Skip */
|
||||
break;
|
||||
default:
|
||||
// add byte to value, but do no overflow
|
||||
if ( _textPointer < (_value + sizeof(_value)) )
|
||||
*_textPointer++ = inbyte;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case CHECKSUM:
|
||||
{
|
||||
bool valid = _checksum == 0;
|
||||
if (!valid) {
|
||||
_msgOut->printf("[VE.Direct] checksum 0x%02x != 0, invalid frame\r\n", _checksum);
|
||||
}
|
||||
if (_verboseLogging) { dumpDebugBuffer(); }
|
||||
_checksum = 0;
|
||||
_state = IDLE;
|
||||
if (valid) { frameValidEvent(); }
|
||||
break;
|
||||
}
|
||||
case RECORD_HEX:
|
||||
_state = hexRxEvent(inbyte);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* textRxEvent
|
||||
* This function is called every time a new name/value is successfully parsed. It writes the values to the temporary buffer.
|
||||
*/
|
||||
bool VeDirectFrameHandler::textRxEvent(std::string const& who, char* name, char* value, veStruct& frame) {
|
||||
if (_verboseLogging) {
|
||||
_msgOut->printf("[Victron %s] Text Event %s: Value: %s\r\n",
|
||||
who.c_str(), name, value );
|
||||
}
|
||||
|
||||
if (strcmp(name, "PID") == 0) {
|
||||
frame.PID = strtol(value, nullptr, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(name, "SER") == 0) {
|
||||
strcpy(frame.SER, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(name, "FW") == 0) {
|
||||
strcpy(frame.FW, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(name, "V") == 0) {
|
||||
frame.V = round(atof(value) / 10.0) / 100.0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(name, "I") == 0) {
|
||||
frame.I = round(atof(value) / 10.0) / 100.0;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* hexRxEvent
|
||||
* This function records hex answers or async messages
|
||||
*/
|
||||
int VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) {
|
||||
int ret=RECORD_HEX; // default - continue recording until end of frame
|
||||
|
||||
switch (inbyte) {
|
||||
case '\n':
|
||||
// restore previous state
|
||||
ret=_prevState;
|
||||
break;
|
||||
|
||||
default:
|
||||
_hexSize++;
|
||||
if (_hexSize>=VE_MAX_HEX_LEN) { // oops -buffer overflow - something went wrong, we abort
|
||||
_msgOut->println("[VE.Direct] hexRx buffer overflow - aborting read");
|
||||
_hexSize=0;
|
||||
ret=IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool VeDirectFrameHandler::isDataValid(veStruct const& frame) const {
|
||||
if (_lastUpdate == 0) {
|
||||
return false;
|
||||
}
|
||||
if (strlen(frame.SER) == 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t VeDirectFrameHandler::getLastUpdate() const
|
||||
{
|
||||
return _lastUpdate;
|
||||
}
|
||||
|
||||
/*
|
||||
* getPidAsString
|
||||
* This function returns the product id (PID) as readable text.
|
||||
*/
|
||||
frozen::string const& VeDirectFrameHandler::veStruct::getPidAsString() const
|
||||
{
|
||||
static constexpr frozen::map<uint16_t, frozen::string, 77> values = {
|
||||
{ 0x0300, "BlueSolar MPPT 70|15" },
|
||||
{ 0xA040, "BlueSolar MPPT 75|50" },
|
||||
{ 0xA041, "BlueSolar MPPT 150|35" },
|
||||
{ 0xA042, "BlueSolar MPPT 75|15" },
|
||||
{ 0xA043, "BlueSolar MPPT 100|15" },
|
||||
{ 0xA044, "BlueSolar MPPT 100|30" },
|
||||
{ 0xA045, "BlueSolar MPPT 100|50" },
|
||||
{ 0xA046, "BlueSolar MPPT 100|70" },
|
||||
{ 0xA047, "BlueSolar MPPT 150|100" },
|
||||
{ 0xA049, "BlueSolar MPPT 100|50 rev2" },
|
||||
{ 0xA04A, "BlueSolar MPPT 100|30 rev2" },
|
||||
{ 0xA04B, "BlueSolar MPPT 150|35 rev2" },
|
||||
{ 0xA04C, "BlueSolar MPPT 75|10" },
|
||||
{ 0xA04D, "BlueSolar MPPT 150|45" },
|
||||
{ 0xA04E, "BlueSolar MPPT 150|60" },
|
||||
{ 0xA04F, "BlueSolar MPPT 150|85" },
|
||||
{ 0xA050, "SmartSolar MPPT 250|100" },
|
||||
{ 0xA051, "SmartSolar MPPT 150|100" },
|
||||
{ 0xA052, "SmartSolar MPPT 150|85" },
|
||||
{ 0xA053, "SmartSolar MPPT 75|15" },
|
||||
{ 0xA054, "SmartSolar MPPT 75|10" },
|
||||
{ 0xA055, "SmartSolar MPPT 100|15" },
|
||||
{ 0xA056, "SmartSolar MPPT 100|30" },
|
||||
{ 0xA057, "SmartSolar MPPT 100|50" },
|
||||
{ 0xA058, "SmartSolar MPPT 150|35" },
|
||||
{ 0xA059, "SmartSolar MPPT 150|10 rev2" },
|
||||
{ 0xA05A, "SmartSolar MPPT 150|85 rev2" },
|
||||
{ 0xA05B, "SmartSolar MPPT 250|70" },
|
||||
{ 0xA05C, "SmartSolar MPPT 250|85" },
|
||||
{ 0xA05D, "SmartSolar MPPT 250|60" },
|
||||
{ 0xA05E, "SmartSolar MPPT 250|45" },
|
||||
{ 0xA05F, "SmartSolar MPPT 100|20" },
|
||||
{ 0xA060, "SmartSolar MPPT 100|20 48V" },
|
||||
{ 0xA061, "SmartSolar MPPT 150|45" },
|
||||
{ 0xA062, "SmartSolar MPPT 150|60" },
|
||||
{ 0xA063, "SmartSolar MPPT 150|70" },
|
||||
{ 0xA064, "SmartSolar MPPT 250|85 rev2" },
|
||||
{ 0xA065, "SmartSolar MPPT 250|100 rev2" },
|
||||
{ 0xA066, "BlueSolar MPPT 100|20" },
|
||||
{ 0xA067, "BlueSolar MPPT 100|20 48V" },
|
||||
{ 0xA068, "SmartSolar MPPT 250|60 rev2" },
|
||||
{ 0xA069, "SmartSolar MPPT 250|70 rev2" },
|
||||
{ 0xA06A, "SmartSolar MPPT 150|45 rev2" },
|
||||
{ 0xA06B, "SmartSolar MPPT 150|60 rev2" },
|
||||
{ 0xA06C, "SmartSolar MPPT 150|70 rev2" },
|
||||
{ 0xA06D, "SmartSolar MPPT 150|85 rev3" },
|
||||
{ 0xA06E, "SmartSolar MPPT 150|100 rev3" },
|
||||
{ 0xA06F, "BlueSolar MPPT 150|45 rev2" },
|
||||
{ 0xA070, "BlueSolar MPPT 150|60 rev2" },
|
||||
{ 0xA071, "BlueSolar MPPT 150|70 rev2" },
|
||||
{ 0xA102, "SmartSolar MPPT VE.Can 150|70" },
|
||||
{ 0xA103, "SmartSolar MPPT VE.Can 150|45" },
|
||||
{ 0xA104, "SmartSolar MPPT VE.Can 150|60" },
|
||||
{ 0xA105, "SmartSolar MPPT VE.Can 150|85" },
|
||||
{ 0xA106, "SmartSolar MPPT VE.Can 150|100" },
|
||||
{ 0xA107, "SmartSolar MPPT VE.Can 250|45" },
|
||||
{ 0xA108, "SmartSolar MPPT VE.Can 250|60" },
|
||||
{ 0xA109, "SmartSolar MPPT VE.Can 250|80" },
|
||||
{ 0xA10A, "SmartSolar MPPT VE.Can 250|85" },
|
||||
{ 0xA10B, "SmartSolar MPPT VE.Can 250|100" },
|
||||
{ 0xA10C, "SmartSolar MPPT VE.Can 150|70 rev2" },
|
||||
{ 0xA10D, "SmartSolar MPPT VE.Can 150|85 rev2" },
|
||||
{ 0xA10E, "SmartSolar MPPT VE.Can 150|100 rev2" },
|
||||
{ 0xA10F, "BlueSolar MPPT VE.Can 150|100" },
|
||||
{ 0xA110, "SmartSolar MPPT RS 450|100" },
|
||||
{ 0xA112, "BlueSolar MPPT VE.Can 250|70" },
|
||||
{ 0xA113, "BlueSolar MPPT VE.Can 250|100" },
|
||||
{ 0xA114, "SmartSolar MPPT VE.Can 250|70 rev2" },
|
||||
{ 0xA115, "SmartSolar MPPT VE.Can 250|100 rev2" },
|
||||
{ 0xA116, "SmartSolar MPPT VE.Can 250|85 rev2" },
|
||||
{ 0xA381, "BMV-712 Smart" },
|
||||
{ 0xA382, "BMV-710H Smart" },
|
||||
{ 0xA383, "BMV-712 Smart Rev2" },
|
||||
{ 0xA389, "SmartShunt 500A/50mV" },
|
||||
{ 0xA38A, "SmartShunt 1000A/50mV" },
|
||||
{ 0xA38B, "SmartShunt 2000A/50mV" },
|
||||
{ 0xA3F0, "SmartShunt 2000A/50mV" }
|
||||
};
|
||||
|
||||
return getAsString(values, PID);
|
||||
}
|
||||
79
lib/VeDirectFrameHandler/VeDirectFrameHandler.h
Normal file
@ -0,0 +1,79 @@
|
||||
/* frameHandler.h
|
||||
*
|
||||
* Arduino library to read from Victron devices using VE.Direct protocol.
|
||||
* Derived from Victron framehandler reference implementation.
|
||||
*
|
||||
* 2020.05.05 - 0.2 - initial release
|
||||
* 2021.02.23 - 0.3 - change frameLen to 22 per VE.Direct Protocol version 3.30
|
||||
* 2022.08.20 - 0.4 - changes for OpenDTU
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <array>
|
||||
#include <frozen/string.h>
|
||||
#include <frozen/map.h>
|
||||
#include <memory>
|
||||
|
||||
#define VE_MAX_VALUE_LEN 33 // VE.Direct Protocol: max value size is 33 including /0
|
||||
#define VE_MAX_HEX_LEN 100 // Maximum size of hex frame - max payload 34 byte (=68 char) + safe buffer
|
||||
|
||||
class VeDirectFrameHandler {
|
||||
public:
|
||||
VeDirectFrameHandler();
|
||||
virtual void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort);
|
||||
void loop(); // main loop to read ve.direct data
|
||||
uint32_t getLastUpdate() const; // timestamp of last successful frame read
|
||||
|
||||
protected:
|
||||
bool _verboseLogging;
|
||||
Print* _msgOut;
|
||||
uint32_t _lastUpdate;
|
||||
|
||||
typedef struct {
|
||||
uint16_t PID = 0; // product id
|
||||
char SER[VE_MAX_VALUE_LEN]; // serial number
|
||||
char FW[VE_MAX_VALUE_LEN]; // firmware release number
|
||||
double V = 0; // battery voltage in V
|
||||
double I = 0; // battery current in A
|
||||
double E = 0; // efficiency in percent (calculated, moving average)
|
||||
|
||||
frozen::string const& getPidAsString() const; // product ID as string
|
||||
} veStruct;
|
||||
|
||||
bool textRxEvent(std::string const& who, char* name, char* value, veStruct& frame);
|
||||
bool isDataValid(veStruct const& frame) const; // return true if data valid and not outdated
|
||||
|
||||
template<typename T, size_t L>
|
||||
static frozen::string const& getAsString(frozen::map<T, frozen::string, L> const& values, T val)
|
||||
{
|
||||
auto pos = values.find(val);
|
||||
if (pos == values.end()) {
|
||||
static constexpr frozen::string dummy("???");
|
||||
return dummy;
|
||||
}
|
||||
return pos->second;
|
||||
}
|
||||
|
||||
private:
|
||||
void setLastUpdate(); // set timestampt after successful frame read
|
||||
void dumpDebugBuffer();
|
||||
void rxData(uint8_t inbyte); // byte of serial data
|
||||
virtual void textRxEvent(char *, char *) = 0;
|
||||
virtual void frameValidEvent() = 0;
|
||||
int hexRxEvent(uint8_t);
|
||||
|
||||
std::unique_ptr<HardwareSerial> _vedirectSerial;
|
||||
int _state; // current state
|
||||
int _prevState; // previous state
|
||||
uint8_t _checksum; // checksum value
|
||||
char * _textPointer; // pointer to the private buffer we're writing to, name or value
|
||||
int _hexSize; // length of hex buffer
|
||||
char _name[VE_MAX_VALUE_LEN]; // buffer for the field name
|
||||
char _value[VE_MAX_VALUE_LEN]; // buffer for the field value
|
||||
std::array<uint8_t, 512> _debugBuffer;
|
||||
unsigned _debugIn;
|
||||
uint32_t _lastByteMillis;
|
||||
};
|
||||
176
lib/VeDirectFrameHandler/VeDirectMpptController.cpp
Normal file
@ -0,0 +1,176 @@
|
||||
#include <Arduino.h>
|
||||
#include "VeDirectMpptController.h"
|
||||
|
||||
void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging)
|
||||
{
|
||||
VeDirectFrameHandler::init(rx, tx, msgOut, verboseLogging, 1);
|
||||
_spData = std::make_shared<veMpptStruct>();
|
||||
if (_verboseLogging) { _msgOut->println("Finished init MPPTController"); }
|
||||
}
|
||||
|
||||
bool VeDirectMpptController::isDataValid() const {
|
||||
return VeDirectFrameHandler::isDataValid(*_spData);
|
||||
}
|
||||
|
||||
void VeDirectMpptController::textRxEvent(char* name, char* value)
|
||||
{
|
||||
if (VeDirectFrameHandler::textRxEvent("MPPT", name, value, _tmpFrame)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(name, "LOAD") == 0) {
|
||||
if (strcmp(value, "ON") == 0)
|
||||
_tmpFrame.LOAD = true;
|
||||
else
|
||||
_tmpFrame.LOAD = false;
|
||||
}
|
||||
else if (strcmp(name, "CS") == 0) {
|
||||
_tmpFrame.CS = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "ERR") == 0) {
|
||||
_tmpFrame.ERR = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "OR") == 0) {
|
||||
_tmpFrame.OR = strtol(value, nullptr, 0);
|
||||
}
|
||||
else if (strcmp(name, "MPPT") == 0) {
|
||||
_tmpFrame.MPPT = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "HSDS") == 0) {
|
||||
_tmpFrame.HSDS = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "VPV") == 0) {
|
||||
_tmpFrame.VPV = round(atof(value) / 10.0) / 100.0;
|
||||
}
|
||||
else if (strcmp(name, "PPV") == 0) {
|
||||
_tmpFrame.PPV = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "H19") == 0) {
|
||||
_tmpFrame.H19 = atof(value) / 100.0;
|
||||
}
|
||||
else if (strcmp(name, "H20") == 0) {
|
||||
_tmpFrame.H20 = atof(value) / 100.0;
|
||||
}
|
||||
else if (strcmp(name, "H21") == 0) {
|
||||
_tmpFrame.H21 = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "H22") == 0) {
|
||||
_tmpFrame.H22 = atof(value) / 100.0;
|
||||
}
|
||||
else if (strcmp(name, "H23") == 0) {
|
||||
_tmpFrame.H23 = atoi(value);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* frameValidEvent
|
||||
* This function is called at the end of the received frame.
|
||||
*/
|
||||
void VeDirectMpptController::frameValidEvent() {
|
||||
_tmpFrame.P = _tmpFrame.V * _tmpFrame.I;
|
||||
|
||||
_tmpFrame.IPV = 0;
|
||||
if (_tmpFrame.VPV > 0) {
|
||||
_tmpFrame.IPV = _tmpFrame.PPV / _tmpFrame.VPV;
|
||||
}
|
||||
|
||||
_tmpFrame.E = 0;
|
||||
if ( _tmpFrame.PPV > 0) {
|
||||
_efficiency.addNumber(static_cast<double>(_tmpFrame.P * 100) / _tmpFrame.PPV);
|
||||
_tmpFrame.E = _efficiency.getAverage();
|
||||
}
|
||||
|
||||
_spData = std::make_shared<veMpptStruct>(_tmpFrame);
|
||||
_tmpFrame = {};
|
||||
_lastUpdate = millis();
|
||||
}
|
||||
|
||||
/*
|
||||
* getCsAsString
|
||||
* This function returns the state of operations (CS) as readable text.
|
||||
*/
|
||||
frozen::string const& VeDirectMpptController::veMpptStruct::getCsAsString() const
|
||||
{
|
||||
static constexpr frozen::map<uint8_t, frozen::string, 9> values = {
|
||||
{ 0, "OFF" },
|
||||
{ 2, "Fault" },
|
||||
{ 3, "Bulk" },
|
||||
{ 4, "Absorbtion" },
|
||||
{ 5, "Float" },
|
||||
{ 7, "Equalize (manual)" },
|
||||
{ 245, "Starting-up" },
|
||||
{ 247, "Auto equalize / Recondition" },
|
||||
{ 252, "External Control" }
|
||||
};
|
||||
|
||||
return getAsString(values, CS);
|
||||
}
|
||||
|
||||
/*
|
||||
* getMpptAsString
|
||||
* This function returns the state of MPPT (MPPT) as readable text.
|
||||
*/
|
||||
frozen::string const& VeDirectMpptController::veMpptStruct::getMpptAsString() const
|
||||
{
|
||||
static constexpr frozen::map<uint8_t, frozen::string, 3> values = {
|
||||
{ 0, "OFF" },
|
||||
{ 1, "Voltage or current limited" },
|
||||
{ 2, "MPP Tracker active" }
|
||||
};
|
||||
|
||||
return getAsString(values, MPPT);
|
||||
}
|
||||
|
||||
/*
|
||||
* getErrAsString
|
||||
* This function returns error state (ERR) as readable text.
|
||||
*/
|
||||
frozen::string const& VeDirectMpptController::veMpptStruct::getErrAsString() const
|
||||
{
|
||||
static constexpr frozen::map<uint8_t, frozen::string, 20> values = {
|
||||
{ 0, "No error" },
|
||||
{ 2, "Battery voltage too high" },
|
||||
{ 17, "Charger temperature too high" },
|
||||
{ 18, "Charger over current" },
|
||||
{ 19, "Charger current reversed" },
|
||||
{ 20, "Bulk time limit exceeded" },
|
||||
{ 21, "Current sensor issue(sensor bias/sensor broken)" },
|
||||
{ 26, "Terminals overheated" },
|
||||
{ 28, "Converter issue (dual converter models only)" },
|
||||
{ 33, "Input voltage too high (solar panel)" },
|
||||
{ 34, "Input current too high (solar panel)" },
|
||||
{ 38, "Input shutdown (due to excessive battery voltage)" },
|
||||
{ 39, "Input shutdown (due to current flow during off mode)" },
|
||||
{ 40, "Input" },
|
||||
{ 65, "Lost communication with one of devices" },
|
||||
{ 67, "Synchronisedcharging device configuration issue" },
|
||||
{ 68, "BMS connection lost" },
|
||||
{ 116, "Factory calibration data lost" },
|
||||
{ 117, "Invalid/incompatible firmware" },
|
||||
{ 118, "User settings invalid" }
|
||||
};
|
||||
|
||||
return getAsString(values, ERR);
|
||||
}
|
||||
|
||||
/*
|
||||
* getOrAsString
|
||||
* This function returns the off reason (OR) as readable text.
|
||||
*/
|
||||
frozen::string const& VeDirectMpptController::veMpptStruct::getOrAsString() const
|
||||
{
|
||||
static constexpr frozen::map<uint32_t, frozen::string, 10> values = {
|
||||
{ 0x00000000, "Not off" },
|
||||
{ 0x00000001, "No input power" },
|
||||
{ 0x00000002, "Switched off (power switch)" },
|
||||
{ 0x00000004, "Switched off (device moderegister)" },
|
||||
{ 0x00000008, "Remote input" },
|
||||
{ 0x00000010, "Protection active" },
|
||||
{ 0x00000020, "Paygo" },
|
||||
{ 0x00000040, "BMS" },
|
||||
{ 0x00000080, "Engine shutdown detection" },
|
||||
{ 0x00000100, "Analysing input voltage" }
|
||||
};
|
||||
|
||||
return getAsString(values, OR);
|
||||
}
|
||||
77
lib/VeDirectFrameHandler/VeDirectMpptController.h
Normal file
@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "VeDirectFrameHandler.h"
|
||||
|
||||
template<typename T, size_t WINDOW_SIZE>
|
||||
class MovingAverage {
|
||||
public:
|
||||
MovingAverage()
|
||||
: _sum(0)
|
||||
, _index(0)
|
||||
, _count(0) { }
|
||||
|
||||
void addNumber(T num) {
|
||||
if (_count < WINDOW_SIZE) {
|
||||
_count++;
|
||||
} else {
|
||||
_sum -= _window[_index];
|
||||
}
|
||||
|
||||
_window[_index] = num;
|
||||
_sum += num;
|
||||
_index = (_index + 1) % WINDOW_SIZE;
|
||||
}
|
||||
|
||||
double getAverage() const {
|
||||
if (_count == 0) { return 0.0; }
|
||||
return static_cast<double>(_sum) / _count;
|
||||
}
|
||||
|
||||
private:
|
||||
std::array<T, WINDOW_SIZE> _window;
|
||||
T _sum;
|
||||
size_t _index;
|
||||
size_t _count;
|
||||
};
|
||||
|
||||
class VeDirectMpptController : public VeDirectFrameHandler {
|
||||
public:
|
||||
VeDirectMpptController() = default;
|
||||
|
||||
void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging);
|
||||
bool isDataValid() const; // return true if data valid and not outdated
|
||||
|
||||
struct veMpptStruct : veStruct {
|
||||
uint8_t MPPT; // state of MPP tracker
|
||||
int32_t PPV; // panel power in W
|
||||
int32_t P; // battery output power in W (calculated)
|
||||
double VPV; // panel voltage in V
|
||||
double IPV; // panel current in A (calculated)
|
||||
bool LOAD; // virtual load output state (on if battery voltage reaches upper limit, off if battery reaches lower limit)
|
||||
uint8_t CS; // current state of operation e. g. OFF or Bulk
|
||||
uint8_t ERR; // error code
|
||||
uint32_t OR; // off reason
|
||||
uint32_t HSDS; // day sequence number 1...365
|
||||
double H19; // yield total kWh
|
||||
double H20; // yield today kWh
|
||||
int32_t H21; // maximum power today W
|
||||
double H22; // yield yesterday kWh
|
||||
int32_t H23; // maximum power yesterday W
|
||||
|
||||
frozen::string const& getMpptAsString() const; // state of mppt as string
|
||||
frozen::string const& getCsAsString() const; // current state as string
|
||||
frozen::string const& getErrAsString() const; // error state as string
|
||||
frozen::string const& getOrAsString() const; // off reason as string
|
||||
};
|
||||
|
||||
using spData_t = std::shared_ptr<veMpptStruct const>;
|
||||
spData_t getData() const { return _spData; }
|
||||
|
||||
private:
|
||||
void textRxEvent(char* name, char* value) final;
|
||||
void frameValidEvent() final;
|
||||
spData_t _spData = nullptr;
|
||||
veMpptStruct _tmpFrame{}; // private struct for received name and value pairs
|
||||
MovingAverage<double, 5> _efficiency;
|
||||
};
|
||||
112
lib/VeDirectFrameHandler/VeDirectShuntController.cpp
Normal file
@ -0,0 +1,112 @@
|
||||
#include <Arduino.h>
|
||||
#include "VeDirectShuntController.h"
|
||||
|
||||
VeDirectShuntController VeDirectShunt;
|
||||
|
||||
VeDirectShuntController::VeDirectShuntController()
|
||||
{
|
||||
}
|
||||
|
||||
void VeDirectShuntController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging)
|
||||
{
|
||||
VeDirectFrameHandler::init(rx, tx, msgOut, verboseLogging, 2);
|
||||
if (_verboseLogging) {
|
||||
_msgOut->println("Finished init ShuntController");
|
||||
}
|
||||
}
|
||||
|
||||
void VeDirectShuntController::textRxEvent(char* name, char* value)
|
||||
{
|
||||
if (VeDirectFrameHandler::textRxEvent("SmartShunt", name, value, _tmpFrame)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(name, "T") == 0) {
|
||||
_tmpFrame.T = atoi(value);
|
||||
_tmpFrame.tempPresent = true;
|
||||
}
|
||||
else if (strcmp(name, "P") == 0) {
|
||||
_tmpFrame.P = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "CE") == 0) {
|
||||
_tmpFrame.CE = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "SOC") == 0) {
|
||||
_tmpFrame.SOC = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "TTG") == 0) {
|
||||
_tmpFrame.TTG = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "ALARM") == 0) {
|
||||
_tmpFrame.ALARM = (strcmp(value, "ON") == 0);
|
||||
}
|
||||
else if (strcmp(name, "H1") == 0) {
|
||||
_tmpFrame.H1 = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "H2") == 0) {
|
||||
_tmpFrame.H2 = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "H3") == 0) {
|
||||
_tmpFrame.H3 = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "H4") == 0) {
|
||||
_tmpFrame.H4 = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "H5") == 0) {
|
||||
_tmpFrame.H5 = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "H6") == 0) {
|
||||
_tmpFrame.H6 = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "H7") == 0) {
|
||||
_tmpFrame.H7 = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "H8") == 0) {
|
||||
_tmpFrame.H8 = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "H9") == 0) {
|
||||
_tmpFrame.H9 = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "H10") == 0) {
|
||||
_tmpFrame.H10 = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "H11") == 0) {
|
||||
_tmpFrame.H11 = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "H12") == 0) {
|
||||
_tmpFrame.H12 = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "H13") == 0) {
|
||||
_tmpFrame.H13 = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "H14") == 0) {
|
||||
_tmpFrame.H14 = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "H15") == 0) {
|
||||
_tmpFrame.H15 = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "H16") == 0) {
|
||||
_tmpFrame.H16 = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "H17") == 0) {
|
||||
_tmpFrame.H17 = atoi(value);
|
||||
}
|
||||
else if (strcmp(name, "H18") == 0) {
|
||||
_tmpFrame.H18 = atoi(value);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* frameValidEvent
|
||||
* This function is called at the end of the received frame.
|
||||
*/
|
||||
void VeDirectShuntController::frameValidEvent() {
|
||||
// other than in the MPPT controller, the SmartShunt seems to split all data
|
||||
// into two seperate messagesas. Thus we update veFrame only every second message
|
||||
// after a value for PID has been received
|
||||
if (_tmpFrame.PID == 0) { return; }
|
||||
|
||||
veFrame = _tmpFrame;
|
||||
_tmpFrame = {};
|
||||
_lastUpdate = millis();
|
||||
}
|
||||
49
lib/VeDirectFrameHandler/VeDirectShuntController.h
Normal file
@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "VeDirectFrameHandler.h"
|
||||
|
||||
class VeDirectShuntController : public VeDirectFrameHandler {
|
||||
public:
|
||||
VeDirectShuntController();
|
||||
|
||||
void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging);
|
||||
|
||||
struct veShuntStruct : veStruct {
|
||||
int32_t T; // Battery temperature
|
||||
bool tempPresent = false; // Battery temperature sensor is attached to the shunt
|
||||
int32_t P; // Instantaneous power
|
||||
int32_t CE; // Consumed Amp Hours
|
||||
int32_t SOC; // State-of-charge
|
||||
uint32_t TTG; // Time-to-go
|
||||
bool ALARM; // Alarm condition active
|
||||
uint32_t AR; // Alarm Reason
|
||||
int32_t H1; // Depth of the deepest discharge
|
||||
int32_t H2; // Depth of the last discharge
|
||||
int32_t H3; // Depth of the average discharge
|
||||
int32_t H4; // Number of charge cycles
|
||||
int32_t H5; // Number of full discharges
|
||||
int32_t H6; // Cumulative Amp Hours drawn
|
||||
int32_t H7; // Minimum main (battery) voltage
|
||||
int32_t H8; // Maximum main (battery) voltage
|
||||
int32_t H9; // Number of seconds since last full charge
|
||||
int32_t H10; // Number of automatic synchronizations
|
||||
int32_t H11; // Number of low main voltage alarms
|
||||
int32_t H12; // Number of high main voltage alarms
|
||||
int32_t H13; // Number of low auxiliary voltage alarms
|
||||
int32_t H14; // Number of high auxiliary voltage alarms
|
||||
int32_t H15; // Minimum auxiliary (battery) voltage
|
||||
int32_t H16; // Maximum auxiliary (battery) voltage
|
||||
int32_t H17; // Amount of discharged energy
|
||||
int32_t H18; // Amount of charged energy
|
||||
};
|
||||
|
||||
veShuntStruct veFrame{};
|
||||
|
||||
private:
|
||||
void textRxEvent(char * name, char * value) final;
|
||||
void frameValidEvent() final;
|
||||
veShuntStruct _tmpFrame{}; // private struct for received name and value pairs
|
||||
};
|
||||
|
||||
extern VeDirectShuntController VeDirectShunt;
|
||||
@ -15,13 +15,22 @@ if missing_pkgs:
|
||||
|
||||
from dulwich import porcelain
|
||||
|
||||
|
||||
def get_firmware_specifier_build_flag():
|
||||
try:
|
||||
build_version = porcelain.describe('.') # '.' refers to the repository root dir
|
||||
except:
|
||||
except Exception as err:
|
||||
print(f"Unexpected {err=}, {type(err)=}")
|
||||
build_version = "g0000000"
|
||||
build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version + "\\\""
|
||||
print ("Firmware Revision: " + build_version)
|
||||
try:
|
||||
branch_name = porcelain.active_branch('.').decode('utf-8') # '.' refers to the repository root dir
|
||||
except Exception as err:
|
||||
print(f"Unexpected {err=}, {type(err)=}")
|
||||
branch_name = "master"
|
||||
build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version + "\\\" "
|
||||
build_flag += "-D AUTO_GIT_BRANCH=\\\"" + branch_name + "\\\""
|
||||
print("Firmware Revision: " + build_version)
|
||||
print("Firmware build on branch: " + branch_name)
|
||||
return (build_flag)
|
||||
|
||||
env.Append(
|
||||
|
||||
492
platformio.ini
@ -1,239 +1,253 @@
|
||||
; PlatformIO Project Configuration File
|
||||
;
|
||||
; Build options: build flags, source filter
|
||||
; Upload options: custom upload port, speed and extra flags
|
||||
; Library options: dependencies, extra library storages
|
||||
; Advanced options: extra scripting
|
||||
;
|
||||
; Please visit documentation for the other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[platformio]
|
||||
default_envs = generic_esp32
|
||||
extra_configs =
|
||||
platformio_override.ini
|
||||
|
||||
[env]
|
||||
; Make sure to NOT add any spaces in the custom_ci_action property
|
||||
; (also the position in the file is important)
|
||||
custom_ci_action = generic,generic_esp32,generic_esp32s3,generic_esp32s3_usb
|
||||
|
||||
framework = arduino
|
||||
platform = espressif32@6.5.0
|
||||
|
||||
build_flags =
|
||||
-DPIOENV=\"$PIOENV\"
|
||||
-D_TASK_STD_FUNCTION=1
|
||||
-D_TASK_THREAD_SAFE=1
|
||||
-Wall -Wextra -Wunused -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference
|
||||
; Have to remove -Werror because of
|
||||
; https://github.com/espressif/arduino-esp32/issues/9044 and
|
||||
; https://github.com/espressif/arduino-esp32/issues/9045
|
||||
; -Werror
|
||||
-std=c++17
|
||||
-std=gnu++17
|
||||
build_unflags =
|
||||
-std=gnu++11
|
||||
|
||||
lib_deps =
|
||||
https://github.com/yubox-node-org/ESPAsyncWebServer
|
||||
bblanchon/ArduinoJson @ ^6.21.4
|
||||
https://github.com/bertmelis/espMqttClient.git#v1.5.0
|
||||
nrf24/RF24 @ ^1.4.8
|
||||
olikraus/U8g2 @ ^2.35.9
|
||||
buelowp/sunset @ ^1.1.7
|
||||
https://github.com/arkhipenko/TaskScheduler#testing
|
||||
|
||||
extra_scripts =
|
||||
pre:pio-scripts/auto_firmware_version.py
|
||||
pre:pio-scripts/patch_apply.py
|
||||
post:pio-scripts/create_factory_bin.py
|
||||
|
||||
board_build.partitions = partitions_custom.csv
|
||||
board_build.filesystem = littlefs
|
||||
board_build.embed_files =
|
||||
webapp_dist/index.html.gz
|
||||
webapp_dist/zones.json.gz
|
||||
webapp_dist/favicon.ico
|
||||
webapp_dist/favicon.png
|
||||
webapp_dist/js/app.js.gz
|
||||
webapp_dist/site.webmanifest
|
||||
|
||||
custom_patches =
|
||||
|
||||
monitor_filters = esp32_exception_decoder, time, log2file, colorize
|
||||
monitor_speed = 115200
|
||||
upload_protocol = esptool
|
||||
|
||||
; Specify port in platformio_override.ini. Comment out (add ; in front of line) to use auto detection.
|
||||
; monitor_port = COM4
|
||||
; upload_port = COM4
|
||||
|
||||
|
||||
[env:generic_esp32]
|
||||
board = esp32dev
|
||||
build_flags = ${env.build_flags}
|
||||
|
||||
|
||||
[env:generic_esp32c3]
|
||||
board = esp32-c3-devkitc-02
|
||||
custom_patches = ${env.custom_patches},esp32c3
|
||||
build_flags = ${env.build_flags}
|
||||
|
||||
|
||||
[env:generic_esp32c3_usb]
|
||||
board = esp32-c3-devkitc-02
|
||||
custom_patches = ${env.custom_patches},esp32c3
|
||||
build_flags = ${env.build_flags}
|
||||
-DARDUINO_USB_MODE=1
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
|
||||
|
||||
[env:generic_esp32s3]
|
||||
board = esp32-s3-devkitc-1
|
||||
build_flags = ${env.build_flags}
|
||||
|
||||
|
||||
[env:generic_esp32s3_usb]
|
||||
board = esp32-s3-devkitc-1
|
||||
upload_protocol = esp-builtin
|
||||
build_flags = ${env.build_flags}
|
||||
-DARDUINO_USB_MODE=1
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
|
||||
|
||||
[env:generic]
|
||||
board = esp32dev
|
||||
build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=19
|
||||
-DHOYMILES_PIN_MOSI=23
|
||||
-DHOYMILES_PIN_SCLK=18
|
||||
-DHOYMILES_PIN_IRQ=16
|
||||
-DHOYMILES_PIN_CE=4
|
||||
-DHOYMILES_PIN_CS=5
|
||||
|
||||
|
||||
[env:olimex_esp32_poe]
|
||||
; https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware
|
||||
board = esp32-poe
|
||||
build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=15
|
||||
-DHOYMILES_PIN_MOSI=2
|
||||
-DHOYMILES_PIN_SCLK=14
|
||||
-DHOYMILES_PIN_IRQ=13
|
||||
-DHOYMILES_PIN_CE=16
|
||||
-DHOYMILES_PIN_CS=5
|
||||
-DOPENDTU_ETHERNET
|
||||
|
||||
|
||||
[env:olimex_esp32_evb]
|
||||
; https://www.olimex.com/Products/IoT/ESP32/ESP32-EVB/open-source-hardware
|
||||
board = esp32-evb
|
||||
build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=15
|
||||
-DHOYMILES_PIN_MOSI=2
|
||||
-DHOYMILES_PIN_SCLK=14
|
||||
-DHOYMILES_PIN_IRQ=13
|
||||
-DHOYMILES_PIN_CE=16
|
||||
-DHOYMILES_PIN_CS=17
|
||||
-DOPENDTU_ETHERNET
|
||||
|
||||
|
||||
[env:d1_mini_esp32]
|
||||
board = wemos_d1_mini32
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=19
|
||||
-DHOYMILES_PIN_MOSI=23
|
||||
-DHOYMILES_PIN_SCLK=18
|
||||
-DHOYMILES_PIN_IRQ=16
|
||||
-DHOYMILES_PIN_CE=17
|
||||
-DHOYMILES_PIN_CS=5
|
||||
|
||||
|
||||
[env:wt32_eth01]
|
||||
; http://www.wireless-tag.com/portfolio/wt32-eth01/
|
||||
board = wt32-eth01
|
||||
build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=4
|
||||
-DHOYMILES_PIN_MOSI=2
|
||||
-DHOYMILES_PIN_SCLK=32
|
||||
-DHOYMILES_PIN_IRQ=33
|
||||
-DHOYMILES_PIN_CE=14
|
||||
-DHOYMILES_PIN_CS=15
|
||||
-DOPENDTU_ETHERNET
|
||||
|
||||
|
||||
[env:esp_s3_12k_kit]
|
||||
; https://www.waveshare.com/wiki/NodeMCU-ESP-S3-12K-Kit
|
||||
board = esp32-s3-devkitc-1
|
||||
build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=16
|
||||
-DHOYMILES_PIN_MOSI=17
|
||||
-DHOYMILES_PIN_SCLK=18
|
||||
-DHOYMILES_PIN_IRQ=3
|
||||
-DHOYMILES_PIN_CE=4
|
||||
-DHOYMILES_PIN_CS=5
|
||||
|
||||
|
||||
[env:lolin32_lite]
|
||||
; https://www.makershop.de/plattformen/esp8266/wemos-lolin32/
|
||||
; https://www.az-delivery.de/products/esp32-lolin-lolin32
|
||||
board = lolin32_lite
|
||||
build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=19
|
||||
-DHOYMILES_PIN_MOSI=23
|
||||
-DHOYMILES_PIN_SCLK=18
|
||||
-DHOYMILES_PIN_IRQ=16
|
||||
-DHOYMILES_PIN_CE=17
|
||||
-DHOYMILES_PIN_CS=5
|
||||
|
||||
[env:lolin_s2_mini]
|
||||
board = lolin_s2_mini
|
||||
build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=13
|
||||
-DHOYMILES_PIN_MOSI=11
|
||||
-DHOYMILES_PIN_SCLK=12
|
||||
-DHOYMILES_PIN_CS=10
|
||||
-DHOYMILES_PIN_IRQ=4
|
||||
-DHOYMILES_PIN_CE=5
|
||||
|
||||
|
||||
[env:opendtufusionv1]
|
||||
board = esp32-s3-devkitc-1
|
||||
upload_protocol = esp-builtin
|
||||
debug_tool = esp-builtin
|
||||
debug_speed = 12000
|
||||
build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=48
|
||||
-DHOYMILES_PIN_MOSI=35
|
||||
-DHOYMILES_PIN_SCLK=36
|
||||
-DHOYMILES_PIN_IRQ=47
|
||||
-DHOYMILES_PIN_CE=38
|
||||
-DHOYMILES_PIN_CS=37
|
||||
-DLED0=17
|
||||
-DLED1=18
|
||||
-DARDUINO_USB_MODE=1
|
||||
|
||||
[env:opendtufusionv2]
|
||||
board = esp32-s3-devkitc-1
|
||||
upload_protocol = esp-builtin
|
||||
debug_tool = esp-builtin
|
||||
debug_speed = 12000
|
||||
build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=48
|
||||
-DHOYMILES_PIN_MOSI=35
|
||||
-DHOYMILES_PIN_SCLK=36
|
||||
-DHOYMILES_PIN_IRQ=47
|
||||
-DHOYMILES_PIN_CE=38
|
||||
-DHOYMILES_PIN_CS=37
|
||||
-DLED0=17
|
||||
-DLED1=18
|
||||
-DCMT_CLK=6
|
||||
-DCMT_CS=4
|
||||
-DCMT_FCS=21
|
||||
-DCMT_GPIO2=3
|
||||
-DCMT_GPIO3=8
|
||||
-DCMT_SDIO=5
|
||||
-DARDUINO_USB_MODE=1
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
; PlatformIO Project Configuration File
|
||||
;
|
||||
; Build options: build flags, source filter
|
||||
; Upload options: custom upload port, speed and extra flags
|
||||
; Library options: dependencies, extra library storages
|
||||
; Advanced options: extra scripting
|
||||
;
|
||||
; Please visit documentation for the other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[platformio]
|
||||
default_envs = generic_esp32
|
||||
extra_configs =
|
||||
platformio_override.ini
|
||||
|
||||
[env]
|
||||
; Make sure to NOT add any spaces in the custom_ci_action property
|
||||
; (also the position in the file is important)
|
||||
custom_ci_action = generic,generic_esp32,generic_esp32s3,generic_esp32s3_usb
|
||||
|
||||
framework = arduino
|
||||
platform = espressif32@6.5.0
|
||||
|
||||
build_flags =
|
||||
-DPIOENV=\"$PIOENV\"
|
||||
-D_TASK_STD_FUNCTION=1
|
||||
-D_TASK_THREAD_SAFE=1
|
||||
-Wall -Wextra -Wunused -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference
|
||||
; Have to remove -Werror because of
|
||||
; https://github.com/espressif/arduino-esp32/issues/9044 and
|
||||
; https://github.com/espressif/arduino-esp32/issues/9045
|
||||
; -Werror
|
||||
-std=c++17
|
||||
-std=gnu++17
|
||||
build_unflags =
|
||||
-std=gnu++11
|
||||
|
||||
lib_deps =
|
||||
https://github.com/yubox-node-org/ESPAsyncWebServer
|
||||
bblanchon/ArduinoJson @ ^6.21.4
|
||||
https://github.com/bertmelis/espMqttClient.git#v1.5.0
|
||||
nrf24/RF24 @ ^1.4.8
|
||||
olikraus/U8g2 @ ^2.35.9
|
||||
buelowp/sunset @ ^1.1.7
|
||||
https://github.com/arkhipenko/TaskScheduler#testing
|
||||
https://github.com/arkhipenko/TaskScheduler#testing
|
||||
https://github.com/coryjfowler/MCP_CAN_lib
|
||||
plerup/EspSoftwareSerial @ ^8.0.1
|
||||
https://github.com/dok-net/ghostl @ ^1.0.1
|
||||
rweather/Crypto@^0.4.0
|
||||
|
||||
extra_scripts =
|
||||
pre:pio-scripts/auto_firmware_version.py
|
||||
pre:pio-scripts/patch_apply.py
|
||||
post:pio-scripts/create_factory_bin.py
|
||||
|
||||
board_build.partitions = partitions_custom.csv
|
||||
board_build.filesystem = littlefs
|
||||
board_build.embed_files =
|
||||
webapp_dist/index.html.gz
|
||||
webapp_dist/zones.json.gz
|
||||
webapp_dist/favicon.ico
|
||||
webapp_dist/favicon.png
|
||||
webapp_dist/js/app.js.gz
|
||||
webapp_dist/site.webmanifest
|
||||
|
||||
custom_patches =
|
||||
|
||||
monitor_filters = esp32_exception_decoder, time, log2file, colorize
|
||||
monitor_speed = 115200
|
||||
upload_protocol = esptool
|
||||
|
||||
; Specify port in platformio_override.ini. Comment out (add ; in front of line) to use auto detection.
|
||||
; monitor_port = COM4
|
||||
; upload_port = COM4
|
||||
|
||||
|
||||
[env:generic_esp32]
|
||||
board = esp32dev
|
||||
build_flags = ${env.build_flags}
|
||||
|
||||
|
||||
[env:generic_esp32c3]
|
||||
board = esp32-c3-devkitc-02
|
||||
custom_patches = ${env.custom_patches},esp32c3
|
||||
build_flags = ${env.build_flags}
|
||||
|
||||
|
||||
[env:generic_esp32c3_usb]
|
||||
board = esp32-c3-devkitc-02
|
||||
custom_patches = ${env.custom_patches},esp32c3
|
||||
build_flags = ${env.build_flags}
|
||||
-DARDUINO_USB_MODE=1
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
|
||||
|
||||
[env:generic_esp32s3]
|
||||
board = esp32-s3-devkitc-1
|
||||
build_flags = ${env.build_flags}
|
||||
|
||||
|
||||
[env:generic_esp32s3_usb]
|
||||
board = esp32-s3-devkitc-1
|
||||
upload_protocol = esp-builtin
|
||||
build_flags = ${env.build_flags}
|
||||
-DARDUINO_USB_MODE=1
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
|
||||
|
||||
[env:generic]
|
||||
board = esp32dev
|
||||
build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=19
|
||||
-DHOYMILES_PIN_MOSI=23
|
||||
-DHOYMILES_PIN_SCLK=18
|
||||
-DHOYMILES_PIN_IRQ=16
|
||||
-DHOYMILES_PIN_CE=4
|
||||
-DHOYMILES_PIN_CS=5
|
||||
|
||||
|
||||
[env:olimex_esp32_poe]
|
||||
; https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware
|
||||
board = esp32-poe
|
||||
build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=15
|
||||
-DHOYMILES_PIN_MOSI=2
|
||||
-DHOYMILES_PIN_SCLK=14
|
||||
-DHOYMILES_PIN_IRQ=13
|
||||
-DHOYMILES_PIN_CE=16
|
||||
-DHOYMILES_PIN_CS=5
|
||||
-DOPENDTU_ETHERNET
|
||||
|
||||
|
||||
[env:olimex_esp32_evb]
|
||||
; https://www.olimex.com/Products/IoT/ESP32/ESP32-EVB/open-source-hardware
|
||||
board = esp32-evb
|
||||
build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=15
|
||||
-DHOYMILES_PIN_MOSI=2
|
||||
-DHOYMILES_PIN_SCLK=14
|
||||
-DHOYMILES_PIN_IRQ=13
|
||||
-DHOYMILES_PIN_CE=16
|
||||
-DHOYMILES_PIN_CS=17
|
||||
-DOPENDTU_ETHERNET
|
||||
|
||||
|
||||
[env:d1_mini_esp32]
|
||||
board = wemos_d1_mini32
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=19
|
||||
-DHOYMILES_PIN_MOSI=23
|
||||
-DHOYMILES_PIN_SCLK=18
|
||||
-DHOYMILES_PIN_IRQ=16
|
||||
-DHOYMILES_PIN_CE=17
|
||||
-DHOYMILES_PIN_CS=5
|
||||
-DVICTRON_PIN_TX=21
|
||||
-DVICTRON_PIN_RX=22
|
||||
-DPYLONTECH_PIN_RX=27
|
||||
-DPYLONTECH_PIN_TX=14
|
||||
-DHUAWEI_PIN_MISO=12
|
||||
-DHUAWEI_PIN_MOSI=13
|
||||
-DHUAWEI_PIN_SCLK=26
|
||||
-DHUAWEI_PIN_IRQ=25
|
||||
-DHUAWEI_PIN_CS=15
|
||||
-DHUAWEI_PIN_POWER=33
|
||||
|
||||
[env:wt32_eth01]
|
||||
; http://www.wireless-tag.com/portfolio/wt32-eth01/
|
||||
board = wt32-eth01
|
||||
build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=4
|
||||
-DHOYMILES_PIN_MOSI=2
|
||||
-DHOYMILES_PIN_SCLK=32
|
||||
-DHOYMILES_PIN_IRQ=33
|
||||
-DHOYMILES_PIN_CE=14
|
||||
-DHOYMILES_PIN_CS=15
|
||||
-DOPENDTU_ETHERNET
|
||||
|
||||
|
||||
[env:esp_s3_12k_kit]
|
||||
; https://www.waveshare.com/wiki/NodeMCU-ESP-S3-12K-Kit
|
||||
board = esp32-s3-devkitc-1
|
||||
build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=16
|
||||
-DHOYMILES_PIN_MOSI=17
|
||||
-DHOYMILES_PIN_SCLK=18
|
||||
-DHOYMILES_PIN_IRQ=3
|
||||
-DHOYMILES_PIN_CE=4
|
||||
-DHOYMILES_PIN_CS=5
|
||||
|
||||
|
||||
[env:lolin32_lite]
|
||||
; https://www.makershop.de/plattformen/esp8266/wemos-lolin32/
|
||||
; https://www.az-delivery.de/products/esp32-lolin-lolin32
|
||||
board = lolin32_lite
|
||||
build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=19
|
||||
-DHOYMILES_PIN_MOSI=23
|
||||
-DHOYMILES_PIN_SCLK=18
|
||||
-DHOYMILES_PIN_IRQ=16
|
||||
-DHOYMILES_PIN_CE=17
|
||||
-DHOYMILES_PIN_CS=5
|
||||
|
||||
[env:lolin_s2_mini]
|
||||
board = lolin_s2_mini
|
||||
build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=13
|
||||
-DHOYMILES_PIN_MOSI=11
|
||||
-DHOYMILES_PIN_SCLK=12
|
||||
-DHOYMILES_PIN_CS=10
|
||||
-DHOYMILES_PIN_IRQ=4
|
||||
-DHOYMILES_PIN_CE=5
|
||||
|
||||
|
||||
[env:opendtufusionv1]
|
||||
board = esp32-s3-devkitc-1
|
||||
upload_protocol = esp-builtin
|
||||
debug_tool = esp-builtin
|
||||
debug_speed = 12000
|
||||
build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=48
|
||||
-DHOYMILES_PIN_MOSI=35
|
||||
-DHOYMILES_PIN_SCLK=36
|
||||
-DHOYMILES_PIN_IRQ=47
|
||||
-DHOYMILES_PIN_CE=38
|
||||
-DHOYMILES_PIN_CS=37
|
||||
-DLED0=17
|
||||
-DLED1=18
|
||||
-DARDUINO_USB_MODE=1
|
||||
|
||||
[env:opendtufusionv2]
|
||||
board = esp32-s3-devkitc-1
|
||||
upload_protocol = esp-builtin
|
||||
debug_tool = esp-builtin
|
||||
debug_speed = 12000
|
||||
build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=48
|
||||
-DHOYMILES_PIN_MOSI=35
|
||||
-DHOYMILES_PIN_SCLK=36
|
||||
-DHOYMILES_PIN_IRQ=47
|
||||
-DHOYMILES_PIN_CE=38
|
||||
-DHOYMILES_PIN_CS=37
|
||||
-DLED0=17
|
||||
-DLED1=18
|
||||
-DCMT_CLK=6
|
||||
-DCMT_CS=4
|
||||
-DCMT_FCS=21
|
||||
-DCMT_GPIO2=3
|
||||
-DCMT_GPIO3=8
|
||||
-DCMT_SDIO=5
|
||||
-DARDUINO_USB_MODE=1
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
|
||||
@ -30,5 +30,12 @@
|
||||
; -DHOYMILES_PIN_IRQ=4
|
||||
; -DHOYMILES_PIN_CE=5
|
||||
; -DHOYMILES_PIN_CS=6
|
||||
; -DVICTRON_PIN_TX=21
|
||||
; -DVICTRON_PIN_RX=22
|
||||
; -DHUAWEI_PIN_MISO=12
|
||||
; -DHUAWEI_PIN_MOSI=13
|
||||
; -DHUAWEI_PIN_SCLK=26
|
||||
; -DHUAWEI_PIN_IRQ=25
|
||||
; -DHUAWEI_PIN_CS=15
|
||||
;monitor_port = /dev/ttyACM0
|
||||
;upload_port = /dev/ttyACM0
|
||||
|
||||
89
src/Battery.cpp
Normal file
@ -0,0 +1,89 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include "Battery.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "PylontechCanReceiver.h"
|
||||
#include "JkBmsController.h"
|
||||
#include "VictronSmartShunt.h"
|
||||
#include "MqttBattery.h"
|
||||
|
||||
BatteryClass Battery;
|
||||
|
||||
std::shared_ptr<BatteryStats const> BatteryClass::getStats() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
|
||||
if (!_upProvider) {
|
||||
static auto sspDummyStats = std::make_shared<BatteryStats>();
|
||||
return sspDummyStats;
|
||||
}
|
||||
|
||||
return _upProvider->getStats();
|
||||
}
|
||||
|
||||
void BatteryClass::init(Scheduler& scheduler)
|
||||
{
|
||||
scheduler.addTask(_loopTask);
|
||||
_loopTask.setCallback(std::bind(&BatteryClass::loop, this));
|
||||
_loopTask.setIterations(TASK_FOREVER);
|
||||
_loopTask.enable();
|
||||
|
||||
this->updateSettings();
|
||||
}
|
||||
|
||||
void BatteryClass::updateSettings()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
|
||||
if (_upProvider) {
|
||||
_upProvider->deinit();
|
||||
_upProvider = nullptr;
|
||||
}
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
if (!config.Battery.Enabled) { return; }
|
||||
|
||||
bool verboseLogging = config.Battery.VerboseLogging;
|
||||
|
||||
switch (config.Battery.Provider) {
|
||||
case 0:
|
||||
_upProvider = std::make_unique<PylontechCanReceiver>();
|
||||
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
|
||||
break;
|
||||
case 1:
|
||||
_upProvider = std::make_unique<JkBms::Controller>();
|
||||
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
|
||||
break;
|
||||
case 2:
|
||||
_upProvider = std::make_unique<MqttBattery>();
|
||||
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
|
||||
break;
|
||||
case 3:
|
||||
_upProvider = std::make_unique<VictronSmartShunt>();
|
||||
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
|
||||
break;
|
||||
default:
|
||||
MessageOutput.printf("Unknown battery provider: %d\r\n", config.Battery.Provider);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void BatteryClass::loop()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
|
||||
if (!_upProvider) { return; }
|
||||
|
||||
_upProvider->loop();
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
if (!MqttSettings.getConnected()
|
||||
|| (millis() - _lastMqttPublish) < (config.Mqtt.PublishInterval * 1000)) {
|
||||
return;
|
||||
}
|
||||
|
||||
_upProvider->getStats()->mqttPublish();
|
||||
|
||||
_lastMqttPublish = millis();
|
||||
}
|
||||
389
src/BatteryStats.cpp
Normal file
@ -0,0 +1,389 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include "BatteryStats.h"
|
||||
#include "Configuration.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "JkBmsDataPoints.h"
|
||||
|
||||
template<typename T>
|
||||
static void addLiveViewInSection(JsonVariant& root,
|
||||
std::string const& section, std::string const& name,
|
||||
T&& value, std::string const& unit, uint8_t precision)
|
||||
{
|
||||
auto jsonValue = root["values"][section][name];
|
||||
jsonValue["v"] = value;
|
||||
jsonValue["u"] = unit;
|
||||
jsonValue["d"] = precision;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static void addLiveViewValue(JsonVariant& root, std::string const& name,
|
||||
T&& value, std::string const& unit, uint8_t precision)
|
||||
{
|
||||
addLiveViewInSection(root, "status", name, value, unit, precision);
|
||||
}
|
||||
|
||||
static void addLiveViewTextInSection(JsonVariant& root,
|
||||
std::string const& section, std::string const& name, std::string const& text)
|
||||
{
|
||||
root["values"][section][name] = text;
|
||||
}
|
||||
|
||||
static void addLiveViewTextValue(JsonVariant& root, std::string const& name,
|
||||
std::string const& text)
|
||||
{
|
||||
addLiveViewTextInSection(root, "status", name, text);
|
||||
}
|
||||
|
||||
static void addLiveViewWarning(JsonVariant& root, std::string const& name,
|
||||
bool warning)
|
||||
{
|
||||
if (!warning) { return; }
|
||||
root["issues"][name] = 1;
|
||||
}
|
||||
|
||||
static void addLiveViewAlarm(JsonVariant& root, std::string const& name,
|
||||
bool alarm)
|
||||
{
|
||||
if (!alarm) { return; }
|
||||
root["issues"][name] = 2;
|
||||
}
|
||||
|
||||
void BatteryStats::getLiveViewData(JsonVariant& root) const
|
||||
{
|
||||
root[F("manufacturer")] = _manufacturer;
|
||||
root[F("data_age")] = getAgeSeconds();
|
||||
|
||||
addLiveViewValue(root, "SoC", _SoC, "%", 0);
|
||||
}
|
||||
|
||||
void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const
|
||||
{
|
||||
BatteryStats::getLiveViewData(root);
|
||||
|
||||
// values go into the "Status" card of the web application
|
||||
addLiveViewValue(root, "chargeVoltage", _chargeVoltage, "V", 1);
|
||||
addLiveViewValue(root, "chargeCurrentLimitation", _chargeCurrentLimitation, "A", 1);
|
||||
addLiveViewValue(root, "dischargeCurrentLimitation", _dischargeCurrentLimitation, "A", 1);
|
||||
addLiveViewValue(root, "stateOfHealth", _stateOfHealth, "%", 0);
|
||||
addLiveViewValue(root, "voltage", _voltage, "V", 2);
|
||||
addLiveViewValue(root, "current", _current, "A", 1);
|
||||
addLiveViewValue(root, "temperature", _temperature, "°C", 1);
|
||||
|
||||
addLiveViewTextValue(root, "chargeEnabled", (_chargeEnabled?"yes":"no"));
|
||||
addLiveViewTextValue(root, "dischargeEnabled", (_dischargeEnabled?"yes":"no"));
|
||||
addLiveViewTextValue(root, "chargeImmediately", (_chargeImmediately?"yes":"no"));
|
||||
|
||||
// alarms and warnings go into the "Issues" card of the web application
|
||||
addLiveViewWarning(root, "highCurrentDischarge", _warningHighCurrentDischarge);
|
||||
addLiveViewAlarm(root, "overCurrentDischarge", _alarmOverCurrentDischarge);
|
||||
|
||||
addLiveViewWarning(root, "highCurrentCharge", _warningHighCurrentCharge);
|
||||
addLiveViewAlarm(root, "overCurrentCharge", _alarmOverCurrentCharge);
|
||||
|
||||
addLiveViewWarning(root, "lowTemperature", _warningLowTemperature);
|
||||
addLiveViewAlarm(root, "underTemperature", _alarmUnderTemperature);
|
||||
|
||||
addLiveViewWarning(root, "highTemperature", _warningHighTemperature);
|
||||
addLiveViewAlarm(root, "overTemperature", _alarmOverTemperature);
|
||||
|
||||
addLiveViewWarning(root, "lowVoltage", _warningLowVoltage);
|
||||
addLiveViewAlarm(root, "underVoltage", _alarmUnderVoltage);
|
||||
|
||||
addLiveViewWarning(root, "highVoltage", _warningHighVoltage);
|
||||
addLiveViewAlarm(root, "overVoltage", _alarmOverVoltage);
|
||||
|
||||
addLiveViewWarning(root, "bmsInternal", _warningBmsInternal);
|
||||
addLiveViewAlarm(root, "bmsInternal", _alarmBmsInternal);
|
||||
}
|
||||
|
||||
void JkBmsBatteryStats::getJsonData(JsonVariant& root, bool verbose) const
|
||||
{
|
||||
BatteryStats::getLiveViewData(root);
|
||||
|
||||
using Label = JkBms::DataPointLabel;
|
||||
|
||||
auto oVoltage = _dataPoints.get<Label::BatteryVoltageMilliVolt>();
|
||||
if (oVoltage.has_value()) {
|
||||
addLiveViewValue(root, "voltage",
|
||||
static_cast<float>(*oVoltage) / 1000, "V", 2);
|
||||
}
|
||||
|
||||
auto oCurrent = _dataPoints.get<Label::BatteryCurrentMilliAmps>();
|
||||
if (oCurrent.has_value()) {
|
||||
addLiveViewValue(root, "current",
|
||||
static_cast<float>(*oCurrent) / 1000, "A", 2);
|
||||
}
|
||||
|
||||
if (oVoltage.has_value() && oCurrent.has_value()) {
|
||||
auto current = static_cast<float>(*oCurrent) / 1000;
|
||||
auto voltage = static_cast<float>(*oVoltage) / 1000;
|
||||
addLiveViewValue(root, "power", current * voltage , "W", 2);
|
||||
}
|
||||
|
||||
auto oTemperatureBms = _dataPoints.get<Label::BmsTempCelsius>();
|
||||
if (oTemperatureBms.has_value()) {
|
||||
addLiveViewValue(root, "bmsTemp", *oTemperatureBms, "°C", 0);
|
||||
}
|
||||
|
||||
// labels BatteryChargeEnabled, BatteryDischargeEnabled, and
|
||||
// BalancingEnabled refer to the user setting. we want to show the
|
||||
// actual MOSFETs' state which control whether charging and discharging
|
||||
// is possible and whether the BMS is currently balancing cells.
|
||||
auto oStatus = _dataPoints.get<Label::StatusBitmask>();
|
||||
if (oStatus.has_value()) {
|
||||
using Bits = JkBms::StatusBits;
|
||||
auto chargeEnabled = *oStatus & static_cast<uint16_t>(Bits::ChargingActive);
|
||||
addLiveViewTextValue(root, "chargeEnabled", (chargeEnabled?"yes":"no"));
|
||||
auto dischargeEnabled = *oStatus & static_cast<uint16_t>(Bits::DischargingActive);
|
||||
addLiveViewTextValue(root, "dischargeEnabled", (dischargeEnabled?"yes":"no"));
|
||||
}
|
||||
|
||||
auto oTemperatureOne = _dataPoints.get<Label::BatteryTempOneCelsius>();
|
||||
if (oTemperatureOne.has_value()) {
|
||||
addLiveViewInSection(root, "cells", "batOneTemp", *oTemperatureOne, "°C", 0);
|
||||
}
|
||||
|
||||
auto oTemperatureTwo = _dataPoints.get<Label::BatteryTempTwoCelsius>();
|
||||
if (oTemperatureTwo.has_value()) {
|
||||
addLiveViewInSection(root, "cells", "batTwoTemp", *oTemperatureTwo, "°C", 0);
|
||||
}
|
||||
|
||||
if (_cellVoltageTimestamp > 0) {
|
||||
addLiveViewInSection(root, "cells", "cellMinVoltage", static_cast<float>(_cellMinMilliVolt)/1000, "V", 3);
|
||||
addLiveViewInSection(root, "cells", "cellAvgVoltage", static_cast<float>(_cellAvgMilliVolt)/1000, "V", 3);
|
||||
addLiveViewInSection(root, "cells", "cellMaxVoltage", static_cast<float>(_cellMaxMilliVolt)/1000, "V", 3);
|
||||
addLiveViewInSection(root, "cells", "cellDiffVoltage", (_cellMaxMilliVolt - _cellMinMilliVolt), "mV", 0);
|
||||
}
|
||||
|
||||
if (oStatus.has_value()) {
|
||||
using Bits = JkBms::StatusBits;
|
||||
auto balancingActive = *oStatus & static_cast<uint16_t>(Bits::BalancingActive);
|
||||
addLiveViewTextInSection(root, "cells", "balancingActive", (balancingActive?"yes":"no"));
|
||||
}
|
||||
|
||||
auto oAlarms = _dataPoints.get<Label::AlarmsBitmask>();
|
||||
if (oAlarms.has_value()) {
|
||||
#define ISSUE(t, x) \
|
||||
auto x = *oAlarms & static_cast<uint16_t>(JkBms::AlarmBits::x); \
|
||||
addLiveView##t(root, "JkBmsIssue"#x, x > 0);
|
||||
|
||||
ISSUE(Warning, LowCapacity);
|
||||
ISSUE(Alarm, BmsOvertemperature);
|
||||
ISSUE(Alarm, ChargingOvervoltage);
|
||||
ISSUE(Alarm, DischargeUndervoltage);
|
||||
ISSUE(Alarm, BatteryOvertemperature);
|
||||
ISSUE(Alarm, ChargingOvercurrent);
|
||||
ISSUE(Alarm, DischargeOvercurrent);
|
||||
ISSUE(Alarm, CellVoltageDifference);
|
||||
ISSUE(Alarm, BatteryBoxOvertemperature);
|
||||
ISSUE(Alarm, BatteryUndertemperature);
|
||||
ISSUE(Alarm, CellOvervoltage);
|
||||
ISSUE(Alarm, CellUndervoltage);
|
||||
ISSUE(Alarm, AProtect);
|
||||
ISSUE(Alarm, BProtect);
|
||||
#undef ISSUE
|
||||
}
|
||||
}
|
||||
|
||||
void BatteryStats::mqttPublish() const
|
||||
{
|
||||
MqttSettings.publish(F("battery/manufacturer"), _manufacturer);
|
||||
MqttSettings.publish(F("battery/dataAge"), String(getAgeSeconds()));
|
||||
MqttSettings.publish(F("battery/stateOfCharge"), String(_SoC));
|
||||
}
|
||||
|
||||
void PylontechBatteryStats::mqttPublish() const
|
||||
{
|
||||
BatteryStats::mqttPublish();
|
||||
|
||||
MqttSettings.publish(F("battery/settings/chargeVoltage"), String(_chargeVoltage));
|
||||
MqttSettings.publish(F("battery/settings/chargeCurrentLimitation"), String(_chargeCurrentLimitation));
|
||||
MqttSettings.publish(F("battery/settings/dischargeCurrentLimitation"), String(_dischargeCurrentLimitation));
|
||||
MqttSettings.publish(F("battery/stateOfHealth"), String(_stateOfHealth));
|
||||
MqttSettings.publish(F("battery/voltage"), String(_voltage));
|
||||
MqttSettings.publish(F("battery/current"), String(_current));
|
||||
MqttSettings.publish(F("battery/temperature"), String(_temperature));
|
||||
MqttSettings.publish(F("battery/alarm/overCurrentDischarge"), String(_alarmOverCurrentDischarge));
|
||||
MqttSettings.publish(F("battery/alarm/overCurrentCharge"), String(_alarmOverCurrentCharge));
|
||||
MqttSettings.publish(F("battery/alarm/underTemperature"), String(_alarmUnderTemperature));
|
||||
MqttSettings.publish(F("battery/alarm/overTemperature"), String(_alarmOverTemperature));
|
||||
MqttSettings.publish(F("battery/alarm/underVoltage"), String(_alarmUnderVoltage));
|
||||
MqttSettings.publish(F("battery/alarm/overVoltage"), String(_alarmOverVoltage));
|
||||
MqttSettings.publish(F("battery/alarm/bmsInternal"), String(_alarmBmsInternal));
|
||||
MqttSettings.publish(F("battery/warning/highCurrentDischarge"), String(_warningHighCurrentDischarge));
|
||||
MqttSettings.publish(F("battery/warning/highCurrentCharge"), String(_warningHighCurrentCharge));
|
||||
MqttSettings.publish(F("battery/warning/lowTemperature"), String(_warningLowTemperature));
|
||||
MqttSettings.publish(F("battery/warning/highTemperature"), String(_warningHighTemperature));
|
||||
MqttSettings.publish(F("battery/warning/lowVoltage"), String(_warningLowVoltage));
|
||||
MqttSettings.publish(F("battery/warning/highVoltage"), String(_warningHighVoltage));
|
||||
MqttSettings.publish(F("battery/warning/bmsInternal"), String(_warningBmsInternal));
|
||||
MqttSettings.publish(F("battery/charging/chargeEnabled"), String(_chargeEnabled));
|
||||
MqttSettings.publish(F("battery/charging/dischargeEnabled"), String(_dischargeEnabled));
|
||||
MqttSettings.publish(F("battery/charging/chargeImmediately"), String(_chargeImmediately));
|
||||
}
|
||||
|
||||
void JkBmsBatteryStats::mqttPublish() const
|
||||
{
|
||||
BatteryStats::mqttPublish();
|
||||
|
||||
using Label = JkBms::DataPointLabel;
|
||||
|
||||
static std::vector<Label> mqttSkip = {
|
||||
Label::CellsMilliVolt, // complex data format
|
||||
Label::ModificationPassword, // sensitive data
|
||||
Label::BatterySoCPercent // already published by base class
|
||||
};
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
// publish all topics every minute, unless the retain flag is enabled
|
||||
bool fullPublish = _lastFullMqttPublish + 60 * 1000 < millis();
|
||||
fullPublish &= !config.Mqtt.Retain;
|
||||
|
||||
for (auto iter = _dataPoints.cbegin(); iter != _dataPoints.cend(); ++iter) {
|
||||
// skip data points that did not change since last published
|
||||
if (!fullPublish && iter->second.getTimestamp() < _lastMqttPublish) { continue; }
|
||||
|
||||
auto skipMatch = std::find(mqttSkip.begin(), mqttSkip.end(), iter->first);
|
||||
if (skipMatch != mqttSkip.end()) { continue; }
|
||||
|
||||
String topic((std::string("battery/") + iter->second.getLabelText()).c_str());
|
||||
MqttSettings.publish(topic, iter->second.getValueText().c_str());
|
||||
}
|
||||
|
||||
auto oCellVoltages = _dataPoints.get<Label::CellsMilliVolt>();
|
||||
if (oCellVoltages.has_value() && (fullPublish || _cellVoltageTimestamp > _lastMqttPublish)) {
|
||||
unsigned idx = 1;
|
||||
for (auto iter = oCellVoltages->cbegin(); iter != oCellVoltages->cend(); ++iter) {
|
||||
String topic("battery/Cell");
|
||||
topic += String(idx);
|
||||
topic += "MilliVolt";
|
||||
|
||||
MqttSettings.publish(topic, String(iter->second));
|
||||
|
||||
++idx;
|
||||
}
|
||||
|
||||
MqttSettings.publish("battery/CellMinMilliVolt", String(_cellMinMilliVolt));
|
||||
MqttSettings.publish("battery/CellAvgMilliVolt", String(_cellAvgMilliVolt));
|
||||
MqttSettings.publish("battery/CellMaxMilliVolt", String(_cellMaxMilliVolt));
|
||||
MqttSettings.publish("battery/CellDiffMilliVolt", String(_cellMaxMilliVolt - _cellMinMilliVolt));
|
||||
}
|
||||
|
||||
auto oAlarms = _dataPoints.get<Label::AlarmsBitmask>();
|
||||
if (oAlarms.has_value()) {
|
||||
for (auto iter = JkBms::AlarmBitTexts.begin(); iter != JkBms::AlarmBitTexts.end(); ++iter) {
|
||||
auto bit = iter->first;
|
||||
String value = (*oAlarms & static_cast<uint16_t>(bit))?"1":"0";
|
||||
MqttSettings.publish(String("battery/alarms/") + iter->second.data(), value);
|
||||
}
|
||||
}
|
||||
|
||||
auto oStatus = _dataPoints.get<Label::StatusBitmask>();
|
||||
if (oStatus.has_value()) {
|
||||
for (auto iter = JkBms::StatusBitTexts.begin(); iter != JkBms::StatusBitTexts.end(); ++iter) {
|
||||
auto bit = iter->first;
|
||||
String value = (*oStatus & static_cast<uint16_t>(bit))?"1":"0";
|
||||
MqttSettings.publish(String("battery/status/") + iter->second.data(), value);
|
||||
}
|
||||
}
|
||||
|
||||
_lastMqttPublish = millis();
|
||||
if (fullPublish) { _lastFullMqttPublish = _lastMqttPublish; }
|
||||
}
|
||||
|
||||
void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp)
|
||||
{
|
||||
using Label = JkBms::DataPointLabel;
|
||||
|
||||
_manufacturer = "JKBMS";
|
||||
auto oProductId = dp.get<Label::ProductId>();
|
||||
if (oProductId.has_value()) {
|
||||
_manufacturer = oProductId->c_str();
|
||||
auto pos = oProductId->rfind("JK");
|
||||
if (pos != std::string::npos) {
|
||||
_manufacturer = oProductId->substr(pos).c_str();
|
||||
}
|
||||
}
|
||||
|
||||
auto oSoCValue = dp.get<Label::BatterySoCPercent>();
|
||||
if (oSoCValue.has_value()) {
|
||||
_SoC = *oSoCValue;
|
||||
auto oSoCDataPoint = dp.getDataPointFor<Label::BatterySoCPercent>();
|
||||
_lastUpdateSoC = oSoCDataPoint->getTimestamp();
|
||||
}
|
||||
|
||||
_dataPoints.updateFrom(dp);
|
||||
|
||||
auto oCellVoltages = _dataPoints.get<Label::CellsMilliVolt>();
|
||||
if (oCellVoltages.has_value()) {
|
||||
for (auto iter = oCellVoltages->cbegin(); iter != oCellVoltages->cend(); ++iter) {
|
||||
if (iter == oCellVoltages->cbegin()) {
|
||||
_cellMinMilliVolt = _cellAvgMilliVolt = _cellMaxMilliVolt = iter->second;
|
||||
continue;
|
||||
}
|
||||
_cellMinMilliVolt = std::min(_cellMinMilliVolt, iter->second);
|
||||
_cellAvgMilliVolt = (_cellAvgMilliVolt + iter->second) / 2;
|
||||
_cellMaxMilliVolt = std::max(_cellMaxMilliVolt, iter->second);
|
||||
}
|
||||
_cellVoltageTimestamp = millis();
|
||||
}
|
||||
|
||||
_lastUpdate = millis();
|
||||
}
|
||||
|
||||
void VictronSmartShuntStats::updateFrom(VeDirectShuntController::veShuntStruct const& shuntData) {
|
||||
_SoC = shuntData.SOC / 10;
|
||||
_voltage = shuntData.V;
|
||||
_current = shuntData.I;
|
||||
_modelName = shuntData.getPidAsString().data();
|
||||
_chargeCycles = shuntData.H4;
|
||||
_timeToGo = shuntData.TTG / 60;
|
||||
_chargedEnergy = shuntData.H18 / 100;
|
||||
_dischargedEnergy = shuntData.H17 / 100;
|
||||
_manufacturer = "Victron " + _modelName;
|
||||
_temperature = shuntData.T;
|
||||
_tempPresent = shuntData.tempPresent;
|
||||
|
||||
// shuntData.AR is a bitfield, so we need to check each bit individually
|
||||
_alarmLowVoltage = shuntData.AR & 1;
|
||||
_alarmHighVoltage = shuntData.AR & 2;
|
||||
_alarmLowSOC = shuntData.AR & 4;
|
||||
_alarmLowTemperature = shuntData.AR & 32;
|
||||
_alarmHighTemperature = shuntData.AR & 64;
|
||||
|
||||
_lastUpdate = VeDirectShunt.getLastUpdate();
|
||||
_lastUpdateSoC = VeDirectShunt.getLastUpdate();
|
||||
}
|
||||
|
||||
void VictronSmartShuntStats::getLiveViewData(JsonVariant& root) const {
|
||||
BatteryStats::getLiveViewData(root);
|
||||
|
||||
// values go into the "Status" card of the web application
|
||||
addLiveViewValue(root, "voltage", _voltage, "V", 2);
|
||||
addLiveViewValue(root, "current", _current, "A", 1);
|
||||
addLiveViewValue(root, "chargeCycles", _chargeCycles, "", 0);
|
||||
addLiveViewValue(root, "chargedEnergy", _chargedEnergy, "KWh", 1);
|
||||
addLiveViewValue(root, "dischargedEnergy", _dischargedEnergy, "KWh", 1);
|
||||
if (_tempPresent) {
|
||||
addLiveViewValue(root, "temperature", _temperature, "°C", 0);
|
||||
}
|
||||
|
||||
addLiveViewAlarm(root, "lowVoltage", _alarmLowVoltage);
|
||||
addLiveViewAlarm(root, "highVoltage", _alarmHighVoltage);
|
||||
addLiveViewAlarm(root, "lowSOC", _alarmLowSOC);
|
||||
addLiveViewAlarm(root, "lowTemperature", _alarmLowTemperature);
|
||||
addLiveViewAlarm(root, "highTemperature", _alarmHighTemperature);
|
||||
}
|
||||
|
||||
void VictronSmartShuntStats::mqttPublish() const {
|
||||
BatteryStats::mqttPublish();
|
||||
|
||||
MqttSettings.publish(F("battery/voltage"), String(_voltage));
|
||||
MqttSettings.publish(F("battery/current"), String(_current));
|
||||
MqttSettings.publish(F("battery/chargeCycles"), String(_chargeCycles));
|
||||
MqttSettings.publish(F("battery/chargedEnergy"), String(_chargedEnergy));
|
||||
MqttSettings.publish(F("battery/dischargedEnergy"), String(_dischargedEnergy));
|
||||
}
|
||||
@ -60,6 +60,7 @@ bool ConfigurationClass::write()
|
||||
|
||||
JsonObject mqtt = doc.createNestedObject("mqtt");
|
||||
mqtt["enabled"] = config.Mqtt.Enabled;
|
||||
mqtt["verbose_logging"] = config.Mqtt.VerboseLogging;
|
||||
mqtt["hostname"] = config.Mqtt.Hostname;
|
||||
mqtt["port"] = config.Mqtt.Port;
|
||||
mqtt["username"] = config.Mqtt.Username;
|
||||
@ -92,6 +93,7 @@ bool ConfigurationClass::write()
|
||||
JsonObject dtu = doc.createNestedObject("dtu");
|
||||
dtu["serial"] = config.Dtu.Serial;
|
||||
dtu["poll_interval"] = config.Dtu.PollInterval;
|
||||
dtu["verbose_logging"] = config.Dtu.VerboseLogging;
|
||||
dtu["nrf_pa_level"] = config.Dtu.Nrf.PaLevel;
|
||||
dtu["cmt_pa_level"] = config.Dtu.Cmt.PaLevel;
|
||||
dtu["cmt_frequency"] = config.Dtu.Cmt.Frequency;
|
||||
@ -143,6 +145,79 @@ bool ConfigurationClass::write()
|
||||
}
|
||||
}
|
||||
|
||||
JsonObject vedirect = doc.createNestedObject("vedirect");
|
||||
vedirect["enabled"] = config.Vedirect.Enabled;
|
||||
vedirect["verbose_logging"] = config.Vedirect.VerboseLogging;
|
||||
vedirect["updates_only"] = config.Vedirect.UpdatesOnly;
|
||||
|
||||
JsonObject powermeter = doc.createNestedObject("powermeter");
|
||||
powermeter["enabled"] = config.PowerMeter.Enabled;
|
||||
powermeter["verbose_logging"] = config.PowerMeter.VerboseLogging;
|
||||
powermeter["interval"] = config.PowerMeter.Interval;
|
||||
powermeter["source"] = config.PowerMeter.Source;
|
||||
powermeter["mqtt_topic_powermeter_1"] = config.PowerMeter.MqttTopicPowerMeter1;
|
||||
powermeter["mqtt_topic_powermeter_2"] = config.PowerMeter.MqttTopicPowerMeter2;
|
||||
powermeter["mqtt_topic_powermeter_3"] = config.PowerMeter.MqttTopicPowerMeter3;
|
||||
powermeter["sdmbaudrate"] = config.PowerMeter.SdmBaudrate;
|
||||
powermeter["sdmaddress"] = config.PowerMeter.SdmAddress;
|
||||
powermeter["http_individual_requests"] = config.PowerMeter.HttpIndividualRequests;
|
||||
|
||||
JsonArray powermeter_http_phases = powermeter.createNestedArray("http_phases");
|
||||
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
|
||||
JsonObject powermeter_phase = powermeter_http_phases.createNestedObject();
|
||||
|
||||
powermeter_phase["enabled"] = config.PowerMeter.Http_Phase[i].Enabled;
|
||||
powermeter_phase["url"] = config.PowerMeter.Http_Phase[i].Url;
|
||||
powermeter_phase["auth_type"] = config.PowerMeter.Http_Phase[i].AuthType;
|
||||
powermeter_phase["username"] = config.PowerMeter.Http_Phase[i].Username;
|
||||
powermeter_phase["password"] = config.PowerMeter.Http_Phase[i].Password;
|
||||
powermeter_phase["header_key"] = config.PowerMeter.Http_Phase[i].HeaderKey;
|
||||
powermeter_phase["header_value"] = config.PowerMeter.Http_Phase[i].HeaderValue;
|
||||
powermeter_phase["timeout"] = config.PowerMeter.Http_Phase[i].Timeout;
|
||||
powermeter_phase["json_path"] = config.PowerMeter.Http_Phase[i].JsonPath;
|
||||
}
|
||||
|
||||
JsonObject powerlimiter = doc.createNestedObject("powerlimiter");
|
||||
powerlimiter["enabled"] = config.PowerLimiter.Enabled;
|
||||
powerlimiter["verbose_logging"] = config.PowerLimiter.VerboseLogging;
|
||||
powerlimiter["solar_passtrough_enabled"] = config.PowerLimiter.SolarPassThroughEnabled;
|
||||
powerlimiter["solar_passtrough_losses"] = config.PowerLimiter.SolarPassThroughLosses;
|
||||
powerlimiter["battery_drain_strategy"] = config.PowerLimiter.BatteryDrainStategy;
|
||||
powerlimiter["interval"] = config.PowerLimiter.Interval;
|
||||
powerlimiter["is_inverter_behind_powermeter"] = config.PowerLimiter.IsInverterBehindPowerMeter;
|
||||
powerlimiter["inverter_id"] = config.PowerLimiter.InverterId;
|
||||
powerlimiter["inverter_channel_id"] = config.PowerLimiter.InverterChannelId;
|
||||
powerlimiter["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption;
|
||||
powerlimiter["target_power_consumption_hysteresis"] = config.PowerLimiter.TargetPowerConsumptionHysteresis;
|
||||
powerlimiter["lower_power_limit"] = config.PowerLimiter.LowerPowerLimit;
|
||||
powerlimiter["upper_power_limit"] = config.PowerLimiter.UpperPowerLimit;
|
||||
powerlimiter["battery_soc_start_threshold"] = config.PowerLimiter.BatterySocStartThreshold;
|
||||
powerlimiter["battery_soc_stop_threshold"] = config.PowerLimiter.BatterySocStopThreshold;
|
||||
powerlimiter["voltage_start_threshold"] = config.PowerLimiter.VoltageStartThreshold;
|
||||
powerlimiter["voltage_stop_threshold"] = config.PowerLimiter.VoltageStopThreshold;
|
||||
powerlimiter["voltage_load_correction_factor"] = config.PowerLimiter.VoltageLoadCorrectionFactor;
|
||||
powerlimiter["inverter_restart_hour"] = config.PowerLimiter.RestartHour;
|
||||
powerlimiter["full_solar_passthrough_soc"] = config.PowerLimiter.FullSolarPassThroughSoc;
|
||||
powerlimiter["full_solar_passthrough_start_voltage"] = config.PowerLimiter.FullSolarPassThroughStartVoltage;
|
||||
powerlimiter["full_solar_passthrough_stop_voltage"] = config.PowerLimiter.FullSolarPassThroughStopVoltage;
|
||||
|
||||
JsonObject battery = doc.createNestedObject("battery");
|
||||
battery["enabled"] = config.Battery.Enabled;
|
||||
battery["verbose_logging"] = config.Battery.VerboseLogging;
|
||||
battery["provider"] = config.Battery.Provider;
|
||||
battery["jkbms_interface"] = config.Battery.JkBmsInterface;
|
||||
battery["jkbms_polling_interval"] = config.Battery.JkBmsPollingInterval;
|
||||
battery["mqtt_topic"] = config.Battery.MqttTopic;
|
||||
|
||||
JsonObject huawei = doc.createNestedObject("huawei");
|
||||
huawei["enabled"] = config.Huawei.Enabled;
|
||||
huawei["can_controller_frequency"] = config.Huawei.CAN_Controller_Frequency;
|
||||
huawei["auto_power_enabled"] = config.Huawei.Auto_Power_Enabled;
|
||||
huawei["voltage_limit"] = config.Huawei.Auto_Power_Voltage_Limit;
|
||||
huawei["enable_voltage_limit"] = config.Huawei.Auto_Power_Enable_Voltage_Limit;
|
||||
huawei["lower_power_limit"] = config.Huawei.Auto_Power_Lower_Power_Limit;
|
||||
huawei["upper_power_limit"] = config.Huawei.Auto_Power_Upper_Power_Limit;
|
||||
|
||||
// Serialize JSON to file
|
||||
if (serializeJson(doc, f) == 0) {
|
||||
MessageOutput.println("Failed to write file");
|
||||
@ -229,6 +304,7 @@ bool ConfigurationClass::read()
|
||||
|
||||
JsonObject mqtt = doc["mqtt"];
|
||||
config.Mqtt.Enabled = mqtt["enabled"] | MQTT_ENABLED;
|
||||
config.Mqtt.VerboseLogging = mqtt["verbose_logging"] | VERBOSE_LOGGING;
|
||||
strlcpy(config.Mqtt.Hostname, mqtt["hostname"] | MQTT_HOST, sizeof(config.Mqtt.Hostname));
|
||||
config.Mqtt.Port = mqtt["port"] | MQTT_PORT;
|
||||
strlcpy(config.Mqtt.Username, mqtt["username"] | MQTT_USER, sizeof(config.Mqtt.Username));
|
||||
@ -261,6 +337,7 @@ bool ConfigurationClass::read()
|
||||
JsonObject dtu = doc["dtu"];
|
||||
config.Dtu.Serial = dtu["serial"] | DTU_SERIAL;
|
||||
config.Dtu.PollInterval = dtu["poll_interval"] | DTU_POLL_INTERVAL;
|
||||
config.Dtu.VerboseLogging = dtu["verbose_logging"] | VERBOSE_LOGGING;
|
||||
config.Dtu.Nrf.PaLevel = dtu["nrf_pa_level"] | DTU_NRF_PA_LEVEL;
|
||||
config.Dtu.Cmt.PaLevel = dtu["cmt_pa_level"] | DTU_CMT_PA_LEVEL;
|
||||
config.Dtu.Cmt.Frequency = dtu["cmt_frequency"] | DTU_CMT_FREQUENCY;
|
||||
@ -312,6 +389,79 @@ bool ConfigurationClass::read()
|
||||
}
|
||||
}
|
||||
|
||||
JsonObject vedirect = doc["vedirect"];
|
||||
config.Vedirect.Enabled = vedirect["enabled"] | VEDIRECT_ENABLED;
|
||||
config.Vedirect.VerboseLogging = vedirect["verbose_logging"] | VEDIRECT_VERBOSE_LOGGING;
|
||||
config.Vedirect.UpdatesOnly = vedirect["updates_only"] | VEDIRECT_UPDATESONLY;
|
||||
|
||||
JsonObject powermeter = doc["powermeter"];
|
||||
config.PowerMeter.Enabled = powermeter["enabled"] | POWERMETER_ENABLED;
|
||||
config.PowerMeter.VerboseLogging = powermeter["verbose_logging"] | VERBOSE_LOGGING;
|
||||
config.PowerMeter.Interval = powermeter["interval"] | POWERMETER_INTERVAL;
|
||||
config.PowerMeter.Source = powermeter["source"] | POWERMETER_SOURCE;
|
||||
strlcpy(config.PowerMeter.MqttTopicPowerMeter1, powermeter["mqtt_topic_powermeter_1"] | "", sizeof(config.PowerMeter.MqttTopicPowerMeter1));
|
||||
strlcpy(config.PowerMeter.MqttTopicPowerMeter2, powermeter["mqtt_topic_powermeter_2"] | "", sizeof(config.PowerMeter.MqttTopicPowerMeter2));
|
||||
strlcpy(config.PowerMeter.MqttTopicPowerMeter3, powermeter["mqtt_topic_powermeter_3"] | "", sizeof(config.PowerMeter.MqttTopicPowerMeter3));
|
||||
config.PowerMeter.SdmBaudrate = powermeter["sdmbaudrate"] | POWERMETER_SDMBAUDRATE;
|
||||
config.PowerMeter.SdmAddress = powermeter["sdmaddress"] | POWERMETER_SDMADDRESS;
|
||||
config.PowerMeter.HttpIndividualRequests = powermeter["http_individual_requests"] | false;
|
||||
|
||||
JsonArray powermeter_http_phases = powermeter["http_phases"];
|
||||
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
|
||||
JsonObject powermeter_phase = powermeter_http_phases[i].as<JsonObject>();
|
||||
|
||||
config.PowerMeter.Http_Phase[i].Enabled = powermeter_phase["enabled"] | (i == 0);
|
||||
strlcpy(config.PowerMeter.Http_Phase[i].Url, powermeter_phase["url"] | "", sizeof(config.PowerMeter.Http_Phase[i].Url));
|
||||
config.PowerMeter.Http_Phase[i].AuthType = powermeter_phase["auth_type"] | Auth::none;
|
||||
strlcpy(config.PowerMeter.Http_Phase[i].Username, powermeter_phase["username"] | "", sizeof(config.PowerMeter.Http_Phase[i].Username));
|
||||
strlcpy(config.PowerMeter.Http_Phase[i].Password, powermeter_phase["password"] | "", sizeof(config.PowerMeter.Http_Phase[i].Password));
|
||||
strlcpy(config.PowerMeter.Http_Phase[i].HeaderKey, powermeter_phase["header_key"] | "", sizeof(config.PowerMeter.Http_Phase[i].HeaderKey));
|
||||
strlcpy(config.PowerMeter.Http_Phase[i].HeaderValue, powermeter_phase["header_value"] | "", sizeof(config.PowerMeter.Http_Phase[i].HeaderValue));
|
||||
config.PowerMeter.Http_Phase[i].Timeout = powermeter_phase["timeout"] | POWERMETER_HTTP_TIMEOUT;
|
||||
strlcpy(config.PowerMeter.Http_Phase[i].JsonPath, powermeter_phase["json_path"] | "", sizeof(config.PowerMeter.Http_Phase[i].JsonPath));
|
||||
}
|
||||
|
||||
JsonObject powerlimiter = doc["powerlimiter"];
|
||||
config.PowerLimiter.Enabled = powerlimiter["enabled"] | POWERLIMITER_ENABLED;
|
||||
config.PowerLimiter.VerboseLogging = powerlimiter["verbose_logging"] | VERBOSE_LOGGING;
|
||||
config.PowerLimiter.SolarPassThroughEnabled = powerlimiter["solar_passtrough_enabled"] | POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED;
|
||||
config.PowerLimiter.SolarPassThroughLosses = powerlimiter["solar_passthrough_losses"] | POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES;
|
||||
config.PowerLimiter.BatteryDrainStategy = powerlimiter["battery_drain_strategy"] | POWERLIMITER_BATTERY_DRAIN_STRATEGY;
|
||||
config.PowerLimiter.Interval = powerlimiter["interval"] | POWERLIMITER_INTERVAL;
|
||||
config.PowerLimiter.IsInverterBehindPowerMeter = powerlimiter["is_inverter_behind_powermeter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER;
|
||||
config.PowerLimiter.InverterId = powerlimiter["inverter_id"] | POWERLIMITER_INVERTER_ID;
|
||||
config.PowerLimiter.InverterChannelId = powerlimiter["inverter_channel_id"] | POWERLIMITER_INVERTER_CHANNEL_ID;
|
||||
config.PowerLimiter.TargetPowerConsumption = powerlimiter["target_power_consumption"] | POWERLIMITER_TARGET_POWER_CONSUMPTION;
|
||||
config.PowerLimiter.TargetPowerConsumptionHysteresis = powerlimiter["target_power_consumption_hysteresis"] | POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS;
|
||||
config.PowerLimiter.LowerPowerLimit = powerlimiter["lower_power_limit"] | POWERLIMITER_LOWER_POWER_LIMIT;
|
||||
config.PowerLimiter.UpperPowerLimit = powerlimiter["upper_power_limit"] | POWERLIMITER_UPPER_POWER_LIMIT;
|
||||
config.PowerLimiter.BatterySocStartThreshold = powerlimiter["battery_soc_start_threshold"] | POWERLIMITER_BATTERY_SOC_START_THRESHOLD;
|
||||
config.PowerLimiter.BatterySocStopThreshold = powerlimiter["battery_soc_stop_threshold"] | POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD;
|
||||
config.PowerLimiter.VoltageStartThreshold = powerlimiter["voltage_start_threshold"] | POWERLIMITER_VOLTAGE_START_THRESHOLD;
|
||||
config.PowerLimiter.VoltageStopThreshold = powerlimiter["voltage_stop_threshold"] | POWERLIMITER_VOLTAGE_STOP_THRESHOLD;
|
||||
config.PowerLimiter.VoltageLoadCorrectionFactor = powerlimiter["voltage_load_correction_factor"] | POWERLIMITER_VOLTAGE_LOAD_CORRECTION_FACTOR;
|
||||
config.PowerLimiter.RestartHour = powerlimiter["inverter_restart_hour"] | POWERLIMITER_RESTART_HOUR;
|
||||
config.PowerLimiter.FullSolarPassThroughSoc = powerlimiter["full_solar_passthrough_soc"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_SOC;
|
||||
config.PowerLimiter.FullSolarPassThroughStartVoltage = powerlimiter["full_solar_passthrough_start_voltage"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_START_VOLTAGE;
|
||||
config.PowerLimiter.FullSolarPassThroughStopVoltage = powerlimiter["full_solar_passthrough_stop_voltage"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_STOP_VOLTAGE;
|
||||
|
||||
JsonObject battery = doc["battery"];
|
||||
config.Battery.Enabled = battery["enabled"] | BATTERY_ENABLED;
|
||||
config.Battery.VerboseLogging = battery["verbose_logging"] | VERBOSE_LOGGING;
|
||||
config.Battery.Provider = battery["provider"] | BATTERY_PROVIDER;
|
||||
config.Battery.JkBmsInterface = battery["jkbms_interface"] | BATTERY_JKBMS_INTERFACE;
|
||||
config.Battery.JkBmsPollingInterval = battery["jkbms_polling_interval"] | BATTERY_JKBMS_POLLING_INTERVAL;
|
||||
strlcpy(config.Battery.MqttTopic, battery["mqtt_topic"] | "", sizeof(config.Battery.MqttTopic));
|
||||
|
||||
JsonObject huawei = doc["huawei"];
|
||||
config.Huawei.Enabled = huawei["enabled"] | HUAWEI_ENABLED;
|
||||
config.Huawei.CAN_Controller_Frequency = huawei["can_controller_frequency"] | HUAWEI_CAN_CONTROLLER_FREQUENCY;
|
||||
config.Huawei.Auto_Power_Enabled = huawei["auto_power_enabled"] | false;
|
||||
config.Huawei.Auto_Power_Voltage_Limit = huawei["voltage_limit"] | HUAWEI_AUTO_POWER_VOLTAGE_LIMIT;
|
||||
config.Huawei.Auto_Power_Enable_Voltage_Limit = huawei["enable_voltage_limit"] | HUAWEI_AUTO_POWER_ENABLE_VOLTAGE_LIMIT;
|
||||
config.Huawei.Auto_Power_Lower_Power_Limit = huawei["lower_power_limit"] | HUAWEI_AUTO_POWER_LOWER_POWER_LIMIT;
|
||||
config.Huawei.Auto_Power_Upper_Power_Limit = huawei["upper_power_limit"] | HUAWEI_AUTO_POWER_UPPER_POWER_LIMIT;
|
||||
|
||||
f.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
288
src/HttpPowerMeter.cpp
Normal file
@ -0,0 +1,288 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include "Configuration.h"
|
||||
#include "HttpPowerMeter.h"
|
||||
#include "MessageOutput.h"
|
||||
#include <WiFiClientSecure.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <Crypto.h>
|
||||
#include <SHA256.h>
|
||||
#include <base64.h>
|
||||
#include <memory>
|
||||
#include <ESPmDNS.h>
|
||||
|
||||
void HttpPowerMeterClass::init()
|
||||
{
|
||||
}
|
||||
|
||||
float HttpPowerMeterClass::getPower(int8_t phase)
|
||||
{
|
||||
return power[phase - 1];
|
||||
}
|
||||
|
||||
bool HttpPowerMeterClass::updateValues()
|
||||
{
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
|
||||
POWERMETER_HTTP_PHASE_CONFIG_T phaseConfig = config.PowerMeter.Http_Phase[i];
|
||||
|
||||
if (!phaseConfig.Enabled) {
|
||||
power[i] = 0.0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i == 0 || config.PowerMeter.HttpIndividualRequests) {
|
||||
if (!queryPhase(i, phaseConfig.Url, phaseConfig.AuthType, phaseConfig.Username, phaseConfig.Password, phaseConfig.HeaderKey, phaseConfig.HeaderValue, phaseConfig.Timeout,
|
||||
phaseConfig.JsonPath)) {
|
||||
MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d failed.\r\n", i + 1);
|
||||
MessageOutput.printf("%s\r\n", httpPowerMeterError);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HttpPowerMeterClass::queryPhase(int phase, const String& url, Auth authType, const char* username, const char* password,
|
||||
const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath)
|
||||
{
|
||||
//hostByName in WiFiGeneric fails to resolve local names. issue described in
|
||||
//https://github.com/espressif/arduino-esp32/issues/3822
|
||||
//and in depth analyzed in https://github.com/espressif/esp-idf/issues/2507#issuecomment-761836300
|
||||
//in conclusion: we cannot rely on httpClient.begin(*wifiClient, url) to resolve IP adresses.
|
||||
//have to do it manually here. Feels Hacky...
|
||||
String protocol;
|
||||
String host;
|
||||
String uri;
|
||||
extractUrlComponents(url, protocol, host, uri);
|
||||
|
||||
IPAddress ipaddr((uint32_t)0);
|
||||
//first check if "host" is already an IP adress
|
||||
if (!ipaddr.fromString(host))
|
||||
{
|
||||
//"host"" is not an IP address so try to resolve the IP adress
|
||||
//first try locally via mDNS, then via DNS. WiFiGeneric::hostByName() will spam the console if done the otherway around.
|
||||
const bool mdnsEnabled = Configuration.get().Mdns.Enabled;
|
||||
if (!mdnsEnabled) {
|
||||
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via DNS, try to enable mDNS in Network Settings"), host.c_str());
|
||||
//ensure we try resolving via DNS even if mDNS is disabled
|
||||
if(!WiFiGenericClass::hostByName(host.c_str(), ipaddr)){
|
||||
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via DNS"), host.c_str());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ipaddr = MDNS.queryHost(host);
|
||||
if (ipaddr == INADDR_NONE){
|
||||
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via mDNS"), host.c_str());
|
||||
//when we cannot find local server via mDNS, try resolving via DNS
|
||||
if(!WiFiGenericClass::hostByName(host.c_str(), ipaddr)){
|
||||
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via DNS"), host.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// secureWifiClient MUST be created before HTTPClient
|
||||
// see discussion: https://github.com/helgeerbe/OpenDTU-OnBattery/issues/381
|
||||
std::unique_ptr<WiFiClient> wifiClient;
|
||||
|
||||
bool https = protocol == "https";
|
||||
if (https) {
|
||||
auto secureWifiClient = std::make_unique<WiFiClientSecure>();
|
||||
secureWifiClient->setInsecure();
|
||||
wifiClient = std::move(secureWifiClient);
|
||||
} else {
|
||||
wifiClient = std::make_unique<WiFiClient>();
|
||||
}
|
||||
|
||||
return httpRequest(phase, *wifiClient, ipaddr.toString(), uri, https, authType, username, password, httpHeader, httpValue, timeout, jsonPath);
|
||||
}
|
||||
|
||||
bool HttpPowerMeterClass::httpRequest(int phase, WiFiClient &wifiClient, const String& host, const String& uri, bool https, Auth authType, const char* username,
|
||||
const char* password, const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath)
|
||||
{
|
||||
int port = (https ? 443 : 80);
|
||||
if(!httpClient.begin(wifiClient, host, port, uri, https)){
|
||||
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s"), (https ? "https" : "http"), host.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
prepareRequest(timeout, httpHeader, httpValue);
|
||||
if (authType == Auth::digest) {
|
||||
const char *headers[1] = {"WWW-Authenticate"};
|
||||
httpClient.collectHeaders(headers, 1);
|
||||
} else if (authType == Auth::basic) {
|
||||
String authString = username;
|
||||
authString += ":";
|
||||
authString += password;
|
||||
String auth = "Basic ";
|
||||
auth.concat(base64::encode(authString));
|
||||
httpClient.addHeader("Authorization", auth);
|
||||
}
|
||||
int httpCode = httpClient.GET();
|
||||
|
||||
if (httpCode == HTTP_CODE_UNAUTHORIZED && authType == Auth::digest) {
|
||||
// Handle authentication challenge
|
||||
if (httpClient.hasHeader("WWW-Authenticate")) {
|
||||
String authReq = httpClient.header("WWW-Authenticate");
|
||||
String authorization = getDigestAuth(authReq, String(username), String(password), "GET", String(uri), 1);
|
||||
httpClient.end();
|
||||
if(!httpClient.begin(wifiClient, host, port, uri, https)){
|
||||
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s using digest auth"), (https ? "https" : "http"), host.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
prepareRequest(timeout, httpHeader, httpValue);
|
||||
httpClient.addHeader("Authorization", authorization);
|
||||
httpCode = httpClient.GET();
|
||||
}
|
||||
}
|
||||
bool result = tryGetFloatValueForPhase(phase, httpCode, jsonPath);
|
||||
httpClient.end();
|
||||
return result;
|
||||
}
|
||||
|
||||
String HttpPowerMeterClass::extractParam(String& authReq, const String& param, const char delimit) {
|
||||
int _begin = authReq.indexOf(param);
|
||||
if (_begin == -1) { return ""; }
|
||||
return authReq.substring(_begin + param.length(), authReq.indexOf(delimit, _begin + param.length()));
|
||||
}
|
||||
|
||||
String HttpPowerMeterClass::getcNonce(const int len) {
|
||||
static const char alphanum[] = "0123456789"
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz";
|
||||
String s = "";
|
||||
|
||||
for (int i = 0; i < len; ++i) { s += alphanum[rand() % (sizeof(alphanum) - 1)]; }
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
String HttpPowerMeterClass::getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter) {
|
||||
// extracting required parameters for RFC 2617 Digest
|
||||
String realm = extractParam(authReq, "realm=\"", '"');
|
||||
String nonce = extractParam(authReq, "nonce=\"", '"');
|
||||
String cNonce = getcNonce(8);
|
||||
|
||||
char nc[9];
|
||||
snprintf(nc, sizeof(nc), "%08x", counter);
|
||||
|
||||
//sha256 of the user:realm:password
|
||||
String ha1 = sha256(username + ":" + realm + ":" + password);
|
||||
|
||||
//sha256 of method:uri
|
||||
String ha2 = sha256(method + ":" + uri);
|
||||
|
||||
//sha256 of h1:nonce:nc:cNonce:auth:h2
|
||||
String response = sha256(ha1 + ":" + nonce + ":" + String(nc) + ":" + cNonce + ":" + "auth" + ":" + ha2);
|
||||
|
||||
//Final authorization String;
|
||||
String authorization = "Digest username=\"";
|
||||
authorization += username;
|
||||
authorization += "\", realm=\"";
|
||||
authorization += realm;
|
||||
authorization += "\", nonce=\"";
|
||||
authorization += nonce;
|
||||
authorization += "\", uri=\"";
|
||||
authorization += uri;
|
||||
authorization += "\", cnonce=\"";
|
||||
authorization += cNonce;
|
||||
authorization += "\", nc=";
|
||||
authorization += String(nc);
|
||||
authorization += ", qop=auth, response=\"";
|
||||
authorization += response;
|
||||
authorization += "\", algorithm=SHA-256";
|
||||
|
||||
return authorization;
|
||||
}
|
||||
|
||||
bool HttpPowerMeterClass::tryGetFloatValueForPhase(int phase, int httpCode, const char* jsonPath)
|
||||
{
|
||||
bool success = false;
|
||||
if (httpCode == HTTP_CODE_OK) {
|
||||
httpResponse = httpClient.getString(); //very unfortunate that we cannot parse WifiClient stream directly
|
||||
StaticJsonDocument<2048> json; //however creating these allocations on stack should be fine to avoid heap fragmentation
|
||||
deserializeJson(json, httpResponse);
|
||||
if(!json.containsKey(jsonPath))
|
||||
{
|
||||
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("[HttpPowerMeter] Couldn't find a value for phase %i with Json query \"%s\""), phase, jsonPath);
|
||||
}else {
|
||||
power[phase] = json[jsonPath].as<float>();
|
||||
//MessageOutput.printf("Power for Phase %i: %5.2fW\r\n", phase, power[phase]);
|
||||
success = true;
|
||||
}
|
||||
} else if (httpCode <= 0) {
|
||||
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("HTTP Error %s"), httpClient.errorToString(httpCode).c_str());
|
||||
} else if (httpCode != HTTP_CODE_OK) {
|
||||
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Bad HTTP code: %d"), httpCode);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
void HttpPowerMeterClass::extractUrlComponents(const String& url, String& protocol, String& hostname, String& uri) {
|
||||
// Find protocol delimiter
|
||||
int protocolEndIndex = url.indexOf(":");
|
||||
if (protocolEndIndex != -1) {
|
||||
protocol = url.substring(0, protocolEndIndex);
|
||||
|
||||
// Find double slash delimiter
|
||||
int doubleSlashIndex = url.indexOf("//", protocolEndIndex);
|
||||
if (doubleSlashIndex != -1) {
|
||||
// Find slash after double slash delimiter
|
||||
int slashIndex = url.indexOf("/", doubleSlashIndex + 2);
|
||||
if (slashIndex != -1) {
|
||||
// Extract hostname and uri
|
||||
hostname = url.substring(doubleSlashIndex + 2, slashIndex);
|
||||
uri = url.substring(slashIndex);
|
||||
} else {
|
||||
// No slash after double slash delimiter, so the whole remaining part is the hostname
|
||||
hostname = url.substring(doubleSlashIndex + 2);
|
||||
uri = "/";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove username:password if present in the hostname
|
||||
int atIndex = hostname.indexOf("@");
|
||||
if (atIndex != -1) {
|
||||
hostname = hostname.substring(atIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
#define HASH_SIZE 32
|
||||
|
||||
String HttpPowerMeterClass::sha256(const String& data) {
|
||||
SHA256 sha256;
|
||||
uint8_t hash[HASH_SIZE];
|
||||
|
||||
sha256.reset();
|
||||
sha256.update(data.c_str(), data.length());
|
||||
sha256.finalize(hash, HASH_SIZE);
|
||||
|
||||
String hashStr = "";
|
||||
for (int i = 0; i < HASH_SIZE; i++) {
|
||||
String hex = String(hash[i], HEX);
|
||||
if (hex.length() == 1) {
|
||||
hashStr += "0";
|
||||
}
|
||||
hashStr += hex;
|
||||
}
|
||||
|
||||
return hashStr;
|
||||
}
|
||||
void HttpPowerMeterClass::prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue) {
|
||||
httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
||||
httpClient.setUserAgent("OpenDTU-OnBattery");
|
||||
httpClient.setConnectTimeout(timeout);
|
||||
httpClient.setTimeout(timeout);
|
||||
httpClient.addHeader("Content-Type", "application/json");
|
||||
httpClient.addHeader("Accept", "application/json");
|
||||
|
||||
if (strlen(httpHeader) > 0) {
|
||||
httpClient.addHeader(httpHeader, httpValue);
|
||||
}
|
||||
}
|
||||
|
||||
HttpPowerMeterClass HttpPowerMeter;
|
||||
466
src/Huawei_can.cpp
Normal file
@ -0,0 +1,466 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2023 Malte Schmidt and others
|
||||
*/
|
||||
#include "Huawei_can.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "PowerMeter.h"
|
||||
#include "PowerLimiter.h"
|
||||
#include "Configuration.h"
|
||||
#include <SPI.h>
|
||||
#include <mcp_can.h>
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
#include <math.h>
|
||||
|
||||
HuaweiCanClass HuaweiCan;
|
||||
HuaweiCanCommClass HuaweiCanComm;
|
||||
|
||||
// *******************************************************
|
||||
// Huawei CAN Communication
|
||||
// *******************************************************
|
||||
|
||||
// Using a C function to avoid static C++ member
|
||||
void HuaweiCanCommunicationTask(void* parameter) {
|
||||
for( ;; ) {
|
||||
HuaweiCanComm.loop();
|
||||
yield();
|
||||
}
|
||||
}
|
||||
|
||||
bool HuaweiCanCommClass::init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk,
|
||||
uint8_t huawei_irq, uint8_t huawei_cs, uint32_t frequency) {
|
||||
SPI = new SPIClass(HSPI);
|
||||
SPI->begin(huawei_clk, huawei_miso, huawei_mosi, huawei_cs);
|
||||
pinMode(huawei_cs, OUTPUT);
|
||||
digitalWrite(huawei_cs, HIGH);
|
||||
|
||||
pinMode(huawei_irq, INPUT_PULLUP);
|
||||
_huaweiIrq = huawei_irq;
|
||||
|
||||
auto mcp_frequency = MCP_8MHZ;
|
||||
if (16000000UL == frequency) { mcp_frequency = MCP_16MHZ; }
|
||||
else if (8000000UL != frequency) {
|
||||
MessageOutput.printf("Huawei CAN: unknown frequency %d Hz, using 8 MHz\r\n", mcp_frequency);
|
||||
}
|
||||
|
||||
_CAN = new MCP_CAN(SPI, huawei_cs);
|
||||
if (!_CAN->begin(MCP_STDEXT, CAN_125KBPS, mcp_frequency) == CAN_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t myMask = 0xFFFFFFFF; // Look at all incoming bits and...
|
||||
const uint32_t myFilter = 0x1081407F; // filter for this message only
|
||||
_CAN->init_Mask(0, 1, myMask);
|
||||
_CAN->init_Filt(0, 1, myFilter);
|
||||
_CAN->init_Mask(1, 1, myMask);
|
||||
|
||||
// Change to normal mode to allow messages to be transmitted
|
||||
_CAN->setMode(MCP_NORMAL);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Public methods need to obtain semaphore
|
||||
|
||||
void HuaweiCanCommClass::loop()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
|
||||
INT32U rxId;
|
||||
unsigned char len = 0;
|
||||
unsigned char rxBuf[8];
|
||||
uint8_t i;
|
||||
|
||||
if (!digitalRead(_huaweiIrq)) {
|
||||
// If CAN_INT pin is low, read receive buffer
|
||||
_CAN->readMsgBuf(&rxId, &len, rxBuf); // Read data: len = data length, buf = data byte(s)
|
||||
if((rxId & 0x80000000) == 0x80000000) { // Determine if ID is standard (11 bits) or extended (29 bits)
|
||||
if ((rxId & 0x1FFFFFFF) == 0x1081407F && len == 8) {
|
||||
|
||||
uint32_t value = __bswap32(* reinterpret_cast<uint32_t*> (rxBuf + 4));
|
||||
|
||||
// Input power 0x70, Input frequency 0x71, Input current 0x72
|
||||
// Output power 0x73, Efficiency 0x74, Output Voltage 0x75 and Output Current 0x76
|
||||
if(rxBuf[1] >= 0x70 && rxBuf[1] <= 0x76 ) {
|
||||
_recValues[rxBuf[1] - 0x70] = value;
|
||||
}
|
||||
|
||||
// Input voltage
|
||||
if(rxBuf[1] == 0x78 ) {
|
||||
_recValues[HUAWEI_INPUT_VOLTAGE_IDX] = value;
|
||||
}
|
||||
|
||||
// Output Temperature
|
||||
if(rxBuf[1] == 0x7F ) {
|
||||
_recValues[HUAWEI_OUTPUT_TEMPERATURE_IDX] = value;
|
||||
}
|
||||
|
||||
// Input Temperature 0x80, Output Current 1 0x81 and Output Current 2 0x82
|
||||
if(rxBuf[1] >= 0x80 && rxBuf[1] <= 0x82 ) {
|
||||
_recValues[rxBuf[1] - 0x80 + HUAWEI_INPUT_TEMPERATURE_IDX] = value;
|
||||
}
|
||||
|
||||
// This is the last value that is send
|
||||
if(rxBuf[1] == 0x81) {
|
||||
_completeUpdateReceived = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Other emitted codes not handled here are: 0x1081407E (Ack), 0x1081807E (Ack Frame), 0x1081D27F (Description), 0x1001117E (Whr meter), 0x100011FE (unclear), 0x108111FE (output enabled), 0x108081FE (unclear). See:
|
||||
// https://github.com/craigpeacock/Huawei_R4850G2_CAN/blob/main/r4850.c
|
||||
// https://www.beyondlogic.org/review-huawei-r4850g2-power-supply-53-5vdc-3kw/
|
||||
}
|
||||
|
||||
// Transmit values
|
||||
for (i = 0; i < HUAWEI_OFFLINE_CURRENT; i++) {
|
||||
if ( _hasNewTxValue[i] == true) {
|
||||
uint8_t data[8] = {0x01, i, 0x00, 0x00, 0x00, 0x00, (uint8_t)((_txValues[i] & 0xFF00) >> 8), (uint8_t)(_txValues[i] & 0xFF)};
|
||||
|
||||
// Send extended message
|
||||
byte sndStat = _CAN->sendMsgBuf(0x108180FE, 1, 8, data);
|
||||
if (sndStat == CAN_OK) {
|
||||
_hasNewTxValue[i] = false;
|
||||
} else {
|
||||
_errorCode |= HUAWEI_ERROR_CODE_TX;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_nextRequestMillis < millis()) {
|
||||
sendRequest();
|
||||
_nextRequestMillis = millis() + HUAWEI_DATA_REQUEST_INTERVAL_MS;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
uint32_t HuaweiCanCommClass::getParameterValue(uint8_t parameter)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
uint32_t v = 0;
|
||||
if (parameter < HUAWEI_OUTPUT_CURRENT1_IDX) {
|
||||
v = _recValues[parameter];
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
bool HuaweiCanCommClass::gotNewRxDataFrame(bool clear)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
bool b = false;
|
||||
b = _completeUpdateReceived;
|
||||
if (clear) {
|
||||
_completeUpdateReceived = false;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
uint8_t HuaweiCanCommClass::getErrorCode(bool clear)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
uint8_t e = 0;
|
||||
e = _errorCode;
|
||||
if (clear) {
|
||||
_errorCode = 0;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
void HuaweiCanCommClass::setParameterValue(uint16_t in, uint8_t parameterType)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
if (parameterType < HUAWEI_OFFLINE_CURRENT) {
|
||||
_txValues[parameterType] = in;
|
||||
_hasNewTxValue[parameterType] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Private methods
|
||||
// Requests current values from Huawei unit. Response is handled in onReceive
|
||||
void HuaweiCanCommClass::sendRequest()
|
||||
{
|
||||
uint8_t data[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
//Send extended message
|
||||
byte sndStat = _CAN->sendMsgBuf(0x108040FE, 1, 8, data);
|
||||
if(sndStat != CAN_OK) {
|
||||
_errorCode |= HUAWEI_ERROR_CODE_RX;
|
||||
}
|
||||
}
|
||||
|
||||
// *******************************************************
|
||||
// Huawei CAN Controller
|
||||
// *******************************************************
|
||||
|
||||
void HuaweiCanClass::init(Scheduler& scheduler, uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power)
|
||||
{
|
||||
scheduler.addTask(_loopTask);
|
||||
_loopTask.setCallback(std::bind(&HuaweiCanClass::loop, this));
|
||||
_loopTask.setIterations(TASK_FOREVER);
|
||||
_loopTask.enable();
|
||||
|
||||
this->updateSettings(huawei_miso, huawei_mosi, huawei_clk, huawei_irq, huawei_cs, huawei_power);
|
||||
}
|
||||
|
||||
void HuaweiCanClass::updateSettings(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power)
|
||||
{
|
||||
if (_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
if (!config.Huawei.Enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!HuaweiCanComm.init(huawei_miso, huawei_mosi, huawei_clk, huawei_irq, huawei_cs, config.Huawei.CAN_Controller_Frequency)) {
|
||||
MessageOutput.println("[HuaweiCanClass::init] Error Initializing Huawei CAN communication...");
|
||||
return;
|
||||
};
|
||||
|
||||
pinMode(huawei_power, OUTPUT);
|
||||
digitalWrite(huawei_power, HIGH);
|
||||
_huaweiPower = huawei_power;
|
||||
|
||||
if (config.Huawei.Auto_Power_Enabled) {
|
||||
_mode = HUAWEI_MODE_AUTO_INT;
|
||||
}
|
||||
|
||||
xTaskCreate(HuaweiCanCommunicationTask,"HUAWEI_CAN_0",1000,NULL,0,&_HuaweiCanCommunicationTaskHdl);
|
||||
|
||||
MessageOutput.println("[HuaweiCanClass::init] MCP2515 Initialized Successfully!");
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
RectifierParameters_t * HuaweiCanClass::get()
|
||||
{
|
||||
return &_rp;
|
||||
}
|
||||
|
||||
uint32_t HuaweiCanClass::getLastUpdate()
|
||||
{
|
||||
return _lastUpdateReceivedMillis;
|
||||
}
|
||||
|
||||
void HuaweiCanClass::processReceivedParameters()
|
||||
{
|
||||
_rp.input_power = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_POWER_IDX) / 1024.0;
|
||||
_rp.input_frequency = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_FREQ_IDX) / 1024.0;
|
||||
_rp.input_current = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_CURRENT_IDX) / 1024.0;
|
||||
_rp.output_power = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_POWER_IDX) / 1024.0;
|
||||
_rp.efficiency = HuaweiCanComm.getParameterValue(HUAWEI_EFFICIENCY_IDX) / 1024.0;
|
||||
_rp.output_voltage = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_VOLTAGE_IDX) / 1024.0;
|
||||
_rp.max_output_current = static_cast<float>(HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_CURRENT_MAX_IDX)) / MAX_CURRENT_MULTIPLIER;
|
||||
_rp.input_voltage = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_VOLTAGE_IDX) / 1024.0;
|
||||
_rp.output_temp = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_TEMPERATURE_IDX) / 1024.0;
|
||||
_rp.input_temp = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_TEMPERATURE_IDX) / 1024.0;
|
||||
_rp.output_current = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_CURRENT_IDX) / 1024.0;
|
||||
|
||||
if (HuaweiCanComm.gotNewRxDataFrame(true)) {
|
||||
_lastUpdateReceivedMillis = millis();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void HuaweiCanClass::loop()
|
||||
{
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
if (!config.Huawei.Enabled || !_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
processReceivedParameters();
|
||||
|
||||
uint8_t com_error = HuaweiCanComm.getErrorCode(true);
|
||||
if (com_error & HUAWEI_ERROR_CODE_RX) {
|
||||
MessageOutput.println("[HuaweiCanClass::loop] Data request error");
|
||||
}
|
||||
if (com_error & HUAWEI_ERROR_CODE_TX) {
|
||||
MessageOutput.println("[HuaweiCanClass::loop] Data set error");
|
||||
}
|
||||
|
||||
// Print updated data
|
||||
if (HuaweiCanComm.gotNewRxDataFrame(false)) {
|
||||
MessageOutput.printf("[HuaweiCanClass::loop] In: %.02fV, %.02fA, %.02fW\n", _rp.input_voltage, _rp.input_current, _rp.input_power);
|
||||
MessageOutput.printf("[HuaweiCanClass::loop] Out: %.02fV, %.02fA of %.02fA, %.02fW\n", _rp.output_voltage, _rp.output_current, _rp.max_output_current, _rp.output_power);
|
||||
MessageOutput.printf("[HuaweiCanClass::loop] Eff : %.01f%%, Temp in: %.01fC, Temp out: %.01fC\n", _rp.efficiency * 100, _rp.input_temp, _rp.output_temp);
|
||||
}
|
||||
|
||||
// Internal PSU power pin (slot detect) control
|
||||
if (_rp.output_current > HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT) {
|
||||
_outputCurrentOnSinceMillis = millis();
|
||||
}
|
||||
if (_outputCurrentOnSinceMillis + HUAWEI_AUTO_MODE_SHUTDOWN_DELAY < millis() &&
|
||||
(_mode == HUAWEI_MODE_AUTO_EXT || _mode == HUAWEI_MODE_AUTO_INT)) {
|
||||
digitalWrite(_huaweiPower, 1);
|
||||
}
|
||||
|
||||
// ***********************
|
||||
// Automatic power control
|
||||
// ***********************
|
||||
|
||||
if (_mode == HUAWEI_MODE_AUTO_INT ) {
|
||||
|
||||
// Set voltage limit in periodic intervals
|
||||
if ( _nextAutoModePeriodicIntMillis < millis()) {
|
||||
MessageOutput.printf("[HuaweiCanClass::loop] Periodically setting voltage limit: %f \r\n", config.Huawei.Auto_Power_Voltage_Limit);
|
||||
_setValue(config.Huawei.Auto_Power_Voltage_Limit, HUAWEI_ONLINE_VOLTAGE);
|
||||
_nextAutoModePeriodicIntMillis = millis() + 60000;
|
||||
}
|
||||
|
||||
// Check if we should run automatic power calculation at all.
|
||||
// We may have set a value recently and still wait for output stabilization
|
||||
if (_autoModeBlockedTillMillis > millis()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-enable automatic power control if the output voltage has dropped below threshold
|
||||
if(_rp.output_voltage < config.Huawei.Auto_Power_Enable_Voltage_Limit ) {
|
||||
_autoPowerEnabledCounter = 10;
|
||||
}
|
||||
|
||||
|
||||
// Check if inverter used by the power limiter is active
|
||||
std::shared_ptr<InverterAbstract> inverter =
|
||||
Hoymiles.getInverterByPos(config.PowerLimiter.InverterId);
|
||||
|
||||
if (inverter != nullptr) {
|
||||
if(inverter->isProducing()) {
|
||||
_setValue(0.0, HUAWEI_ONLINE_CURRENT);
|
||||
// Don't run auto mode for a second now. Otherwise we may send too much over the CAN bus
|
||||
_autoModeBlockedTillMillis = millis() + 1000;
|
||||
MessageOutput.printf("[HuaweiCanClass::loop] Inverter is active, disable\r\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (PowerMeter.getLastPowerMeterUpdate() > _lastPowerMeterUpdateReceivedMillis &&
|
||||
_autoPowerEnabledCounter > 0) {
|
||||
// We have received a new PowerMeter value. Also we're _autoPowerEnabled
|
||||
// So we're good to calculate a new limit
|
||||
|
||||
_lastPowerMeterUpdateReceivedMillis = PowerMeter.getLastPowerMeterUpdate();
|
||||
|
||||
// Calculate new power limit
|
||||
float newPowerLimit = -1 * round(PowerMeter.getPowerTotal());
|
||||
newPowerLimit += _rp.output_power;
|
||||
MessageOutput.printf("[HuaweiCanClass::loop] PL: %f, OP: %f \r\n", newPowerLimit, _rp.output_power);
|
||||
|
||||
if (newPowerLimit > config.Huawei.Auto_Power_Lower_Power_Limit) {
|
||||
|
||||
// Check if the output power has dropped below the lower limit (i.e. the battery is full)
|
||||
// and if the PSU should be turned off. Also we use a simple counter mechanism here to be able
|
||||
// to ramp up from zero output power when starting up
|
||||
if (_rp.output_power < config.Huawei.Auto_Power_Lower_Power_Limit) {
|
||||
MessageOutput.printf("[HuaweiCanClass::loop] Power and voltage limit reached. Disabling automatic power control .... \r\n");
|
||||
_autoPowerEnabledCounter--;
|
||||
if (_autoPowerEnabledCounter == 0) {
|
||||
_autoPowerEnabled = false;
|
||||
_setValue(0, HUAWEI_ONLINE_CURRENT);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
_autoPowerEnabledCounter = 10;
|
||||
}
|
||||
|
||||
// Limit power to maximum
|
||||
if (newPowerLimit > config.Huawei.Auto_Power_Upper_Power_Limit) {
|
||||
newPowerLimit = config.Huawei.Auto_Power_Upper_Power_Limit;
|
||||
}
|
||||
|
||||
// Set the actual output limit
|
||||
float efficiency = (_rp.efficiency > 0.5 ? _rp.efficiency : 1.0);
|
||||
float outputCurrent = efficiency * (newPowerLimit / _rp.output_voltage);
|
||||
MessageOutput.printf("[HuaweiCanClass::loop] Output current %f \r\n", outputCurrent);
|
||||
_autoPowerEnabled = true;
|
||||
_setValue(outputCurrent, HUAWEI_ONLINE_CURRENT);
|
||||
|
||||
// Don't run auto mode some time to allow for output stabilization after issuing a new value
|
||||
_autoModeBlockedTillMillis = millis() + 2 * HUAWEI_DATA_REQUEST_INTERVAL_MS;
|
||||
} else {
|
||||
// requested PL is below minium. Set current to 0
|
||||
_autoPowerEnabled = false;
|
||||
_setValue(0.0, HUAWEI_ONLINE_CURRENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HuaweiCanClass::setValue(float in, uint8_t parameterType)
|
||||
{
|
||||
if (_mode != HUAWEI_MODE_AUTO_INT) {
|
||||
_setValue(in, parameterType);
|
||||
}
|
||||
}
|
||||
|
||||
void HuaweiCanClass::_setValue(float in, uint8_t parameterType)
|
||||
{
|
||||
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
if (!config.Huawei.Enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t value;
|
||||
|
||||
if (in < 0) {
|
||||
MessageOutput.printf("[HuaweiCanClass::_setValue] Error: Tried to set voltage/current to negative value %f \r\n", in);
|
||||
}
|
||||
|
||||
// Start PSU if needed
|
||||
if (in > HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT && parameterType == HUAWEI_ONLINE_CURRENT &&
|
||||
(_mode == HUAWEI_MODE_AUTO_EXT || _mode == HUAWEI_MODE_AUTO_INT)) {
|
||||
digitalWrite(_huaweiPower, 0);
|
||||
_outputCurrentOnSinceMillis = millis();
|
||||
}
|
||||
|
||||
if (parameterType == HUAWEI_OFFLINE_VOLTAGE || parameterType == HUAWEI_ONLINE_VOLTAGE) {
|
||||
value = in * 1024;
|
||||
} else if (parameterType == HUAWEI_OFFLINE_CURRENT || parameterType == HUAWEI_ONLINE_CURRENT) {
|
||||
value = in * MAX_CURRENT_MULTIPLIER;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
HuaweiCanComm.setParameterValue(value, parameterType);
|
||||
}
|
||||
|
||||
void HuaweiCanClass::setMode(uint8_t mode) {
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
if (!config.Huawei.Enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(mode == HUAWEI_MODE_OFF) {
|
||||
digitalWrite(_huaweiPower, 1);
|
||||
_mode = HUAWEI_MODE_OFF;
|
||||
}
|
||||
if(mode == HUAWEI_MODE_ON) {
|
||||
digitalWrite(_huaweiPower, 0);
|
||||
_mode = HUAWEI_MODE_ON;
|
||||
}
|
||||
|
||||
if (mode == HUAWEI_MODE_AUTO_INT && !config.Huawei.Auto_Power_Enabled ) {
|
||||
MessageOutput.println("[HuaweiCanClass::setMode] WARNING: Trying to setmode to internal automatic power control without being enabled in the UI. Ignoring command");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_mode == HUAWEI_MODE_AUTO_INT && mode != HUAWEI_MODE_AUTO_INT) {
|
||||
_autoPowerEnabled = false;
|
||||
_setValue(0, HUAWEI_ONLINE_CURRENT);
|
||||
}
|
||||
|
||||
if(mode == HUAWEI_MODE_AUTO_EXT || mode == HUAWEI_MODE_AUTO_INT) {
|
||||
_mode = mode;
|
||||
}
|
||||
}
|
||||
|
||||
bool HuaweiCanClass::getAutoPowerStatus() {
|
||||
return _autoPowerEnabled;
|
||||
}
|
||||
|
||||
@ -62,6 +62,9 @@ void InverterSettingsClass::init(Scheduler& scheduler)
|
||||
MessageOutput.println(" Setting poll interval... ");
|
||||
Hoymiles.setPollInterval(config.Dtu.PollInterval);
|
||||
|
||||
MessageOutput.println(" Setting verbosity... ");
|
||||
Hoymiles.setVerboseLogging(config.Dtu.VerboseLogging);
|
||||
|
||||
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
|
||||
if (config.Inverter[i].Serial > 0) {
|
||||
MessageOutput.print(" Adding inverter: ");
|
||||
|
||||
430
src/JkBmsController.cpp
Normal file
@ -0,0 +1,430 @@
|
||||
#include <Arduino.h>
|
||||
#include "Configuration.h"
|
||||
#include "HardwareSerial.h"
|
||||
#include "PinMapping.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "JkBmsDataPoints.h"
|
||||
#include "JkBmsController.h"
|
||||
#include <frozen/map.h>
|
||||
|
||||
//#define JKBMS_DUMMY_SERIAL
|
||||
|
||||
#ifdef JKBMS_DUMMY_SERIAL
|
||||
class DummySerial {
|
||||
public:
|
||||
DummySerial() = default;
|
||||
void begin(uint32_t, uint32_t, int8_t, int8_t) {
|
||||
MessageOutput.println("JK BMS Dummy Serial: begin()");
|
||||
}
|
||||
void end() { MessageOutput.println("JK BMS Dummy Serial: end()"); }
|
||||
void flush() { }
|
||||
bool availableForWrite() const { return true; }
|
||||
size_t write(const uint8_t *buffer, size_t size) {
|
||||
MessageOutput.printf("JK BMS Dummy Serial: write(%d Bytes)\r\n", size);
|
||||
_byte_idx = 0;
|
||||
_msg_idx = (_msg_idx + 1) % _data.size();
|
||||
return size;
|
||||
}
|
||||
bool available() const {
|
||||
return _byte_idx < _data[_msg_idx].size();
|
||||
}
|
||||
int read() {
|
||||
if (_byte_idx >= _data[_msg_idx].size()) { return 0; }
|
||||
return _data[_msg_idx][_byte_idx++];
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::vector<uint8_t>> const _data =
|
||||
{
|
||||
{
|
||||
0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00,
|
||||
0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0xfb,
|
||||
0x02, 0x0c, 0xfb, 0x03, 0x0c, 0xfb, 0x04, 0x0c,
|
||||
0xfb, 0x05, 0x0c, 0xfb, 0x06, 0x0c, 0xfb, 0x07,
|
||||
0x0c, 0xfb, 0x08, 0x0c, 0xf7, 0x09, 0x0d, 0x01,
|
||||
0x0a, 0x0c, 0xf9, 0x0b, 0x0c, 0xfb, 0x0c, 0x0c,
|
||||
0xfb, 0x0d, 0x0c, 0xfb, 0x0e, 0x0c, 0xf8, 0x0f,
|
||||
0x0c, 0xf9, 0x10, 0x0c, 0xfb, 0x80, 0x00, 0x1a,
|
||||
0x81, 0x00, 0x12, 0x82, 0x00, 0x12, 0x83, 0x14,
|
||||
0xc3, 0x84, 0x83, 0xf4, 0x85, 0x2e, 0x86, 0x02,
|
||||
0x87, 0x00, 0x15, 0x89, 0x00, 0x00, 0x13, 0x52,
|
||||
0x8a, 0x00, 0x10, 0x8b, 0x00, 0x00, 0x8c, 0x00,
|
||||
0x03, 0x8e, 0x16, 0x80, 0x8f, 0x12, 0xc0, 0x90,
|
||||
0x0e, 0x10, 0x91, 0x0c, 0xda, 0x92, 0x00, 0x05,
|
||||
0x93, 0x0b, 0xb8, 0x94, 0x0c, 0x80, 0x95, 0x00,
|
||||
0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98,
|
||||
0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e,
|
||||
0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01,
|
||||
0x9e, 0x00, 0x64, 0x9f, 0x00, 0x50, 0xa0, 0x00,
|
||||
0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3,
|
||||
0x00, 0x46, 0xa4, 0x00, 0x46, 0xa5, 0x00, 0x00,
|
||||
0xa6, 0x00, 0x02, 0xa7, 0xff, 0xec, 0xa8, 0xff,
|
||||
0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6,
|
||||
0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae,
|
||||
0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14,
|
||||
0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00,
|
||||
0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65,
|
||||
0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32,
|
||||
0x33, 0x30, 0x36, 0xb6, 0x00, 0x01, 0x4a, 0xc3,
|
||||
0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53,
|
||||
0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f,
|
||||
0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba,
|
||||
0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42,
|
||||
0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50,
|
||||
0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00,
|
||||
0x00, 0x53, 0xbb
|
||||
},
|
||||
{
|
||||
0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00,
|
||||
0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0xc0,
|
||||
0x02, 0x0c, 0xc1, 0x03, 0x0c, 0xc0, 0x04, 0x0c,
|
||||
0xc4, 0x05, 0x0c, 0xc4, 0x06, 0x0c, 0xc2, 0x07,
|
||||
0x0c, 0xc2, 0x08, 0x0c, 0xc1, 0x09, 0x0c, 0xba,
|
||||
0x0a, 0x0c, 0xc1, 0x0b, 0x0c, 0xc2, 0x0c, 0x0c,
|
||||
0xc2, 0x0d, 0x0c, 0xc2, 0x0e, 0x0c, 0xc4, 0x0f,
|
||||
0x0c, 0xc2, 0x10, 0x0c, 0xc1, 0x80, 0x00, 0x1b,
|
||||
0x81, 0x00, 0x1b, 0x82, 0x00, 0x1a, 0x83, 0x14,
|
||||
0x68, 0x84, 0x03, 0x70, 0x85, 0x3c, 0x86, 0x02,
|
||||
0x87, 0x00, 0x19, 0x89, 0x00, 0x00, 0x16, 0x86,
|
||||
0x8a, 0x00, 0x10, 0x8b, 0x00, 0x00, 0x8c, 0x00,
|
||||
0x07, 0x8e, 0x16, 0x80, 0x8f, 0x12, 0xc0, 0x90,
|
||||
0x0e, 0x10, 0x91, 0x0c, 0xda, 0x92, 0x00, 0x05,
|
||||
0x93, 0x0b, 0xb8, 0x94, 0x0c, 0x80, 0x95, 0x00,
|
||||
0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98,
|
||||
0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e,
|
||||
0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01,
|
||||
0x9e, 0x00, 0x64, 0x9f, 0x00, 0x50, 0xa0, 0x00,
|
||||
0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3,
|
||||
0x00, 0x46, 0xa4, 0x00, 0x46, 0xa5, 0x00, 0x00,
|
||||
0xa6, 0x00, 0x02, 0xa7, 0xff, 0xec, 0xa8, 0xff,
|
||||
0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6,
|
||||
0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae,
|
||||
0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14,
|
||||
0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00,
|
||||
0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65,
|
||||
0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32,
|
||||
0x33, 0x30, 0x36, 0xb6, 0x00, 0x01, 0x7f, 0x2a,
|
||||
0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53,
|
||||
0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f,
|
||||
0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba,
|
||||
0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42,
|
||||
0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50,
|
||||
0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00,
|
||||
0x00, 0x4f, 0xc1
|
||||
},
|
||||
{
|
||||
0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00,
|
||||
0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0x13,
|
||||
0x02, 0x0c, 0x12, 0x03, 0x0c, 0x0f, 0x04, 0x0c,
|
||||
0x15, 0x05, 0x0c, 0x0d, 0x06, 0x0c, 0x13, 0x07,
|
||||
0x0c, 0x16, 0x08, 0x0c, 0x13, 0x09, 0x0b, 0xdb,
|
||||
0x0a, 0x0b, 0xf6, 0x0b, 0x0c, 0x17, 0x0c, 0x0b,
|
||||
0xf5, 0x0d, 0x0c, 0x16, 0x0e, 0x0c, 0x1a, 0x0f,
|
||||
0x0c, 0x1b, 0x10, 0x0c, 0x1c, 0x80, 0x00, 0x18,
|
||||
0x81, 0x00, 0x18, 0x82, 0x00, 0x18, 0x83, 0x13,
|
||||
0x49, 0x84, 0x00, 0x00, 0x85, 0x00, 0x86, 0x02,
|
||||
0x87, 0x00, 0x23, 0x89, 0x00, 0x00, 0x20, 0x14,
|
||||
0x8a, 0x00, 0x10, 0x8b, 0x00, 0x08, 0x8c, 0x00,
|
||||
0x05, 0x8e, 0x16, 0x80, 0x8f, 0x12, 0xc0, 0x90,
|
||||
0x0e, 0x10, 0x91, 0x0c, 0xda, 0x92, 0x00, 0x05,
|
||||
0x93, 0x0b, 0xb8, 0x94, 0x0c, 0x80, 0x95, 0x00,
|
||||
0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98,
|
||||
0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e,
|
||||
0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01,
|
||||
0x9e, 0x00, 0x64, 0x9f, 0x00, 0x50, 0xa0, 0x00,
|
||||
0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3,
|
||||
0x00, 0x46, 0xa4, 0x00, 0x46, 0xa5, 0x00, 0x00,
|
||||
0xa6, 0x00, 0x02, 0xa7, 0xff, 0xec, 0xa8, 0xff,
|
||||
0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6,
|
||||
0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae,
|
||||
0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14,
|
||||
0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00,
|
||||
0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65,
|
||||
0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32,
|
||||
0x33, 0x30, 0x36, 0xb6, 0x00, 0x02, 0x17, 0x10,
|
||||
0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53,
|
||||
0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f,
|
||||
0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba,
|
||||
0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42,
|
||||
0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50,
|
||||
0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00,
|
||||
0x00, 0x45, 0xce
|
||||
},
|
||||
{
|
||||
0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00,
|
||||
0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0x07,
|
||||
0x02, 0x0c, 0x0a, 0x03, 0x0c, 0x0b, 0x04, 0x0c,
|
||||
0x08, 0x05, 0x0c, 0x05, 0x06, 0x0c, 0x0b, 0x07,
|
||||
0x0c, 0x07, 0x08, 0x0c, 0x0a, 0x09, 0x0c, 0x08,
|
||||
0x0a, 0x0c, 0x06, 0x0b, 0x0c, 0x0a, 0x0c, 0x0c,
|
||||
0x05, 0x0d, 0x0c, 0x0a, 0x0e, 0x0c, 0x0a, 0x0f,
|
||||
0x0c, 0x0a, 0x10, 0x0c, 0x0a, 0x80, 0x00, 0x06,
|
||||
0x81, 0x00, 0x03, 0x82, 0x00, 0x03, 0x83, 0x13,
|
||||
0x40, 0x84, 0x00, 0x00, 0x85, 0x29, 0x86, 0x02,
|
||||
0x87, 0x00, 0x01, 0x89, 0x00, 0x00, 0x01, 0x0a,
|
||||
0x8a, 0x00, 0x10, 0x8b, 0x02, 0x00, 0x8c, 0x00,
|
||||
0x02, 0x8e, 0x16, 0x80, 0x8f, 0x10, 0x40, 0x90,
|
||||
0x0e, 0x10, 0x91, 0x0d, 0xde, 0x92, 0x00, 0x05,
|
||||
0x93, 0x0a, 0x28, 0x94, 0x0a, 0x5a, 0x95, 0x00,
|
||||
0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98,
|
||||
0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e,
|
||||
0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01,
|
||||
0x9e, 0x00, 0x5a, 0x9f, 0x00, 0x50, 0xa0, 0x00,
|
||||
0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3,
|
||||
0x00, 0x37, 0xa4, 0x00, 0x37, 0xa5, 0x00, 0x03,
|
||||
0xa6, 0x00, 0x05, 0xa7, 0xff, 0xec, 0xa8, 0xff,
|
||||
0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6,
|
||||
0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae,
|
||||
0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14,
|
||||
0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00,
|
||||
0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65,
|
||||
0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32,
|
||||
0x33, 0x30, 0x36, 0xb6, 0x00, 0x03, 0xb7, 0x2d,
|
||||
0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53,
|
||||
0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f,
|
||||
0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba,
|
||||
0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42,
|
||||
0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50,
|
||||
0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00,
|
||||
0x00, 0x41, 0x7b
|
||||
}
|
||||
};
|
||||
size_t _msg_idx = 0;
|
||||
size_t _byte_idx = 0;
|
||||
};
|
||||
DummySerial HwSerial;
|
||||
#else
|
||||
HardwareSerial HwSerial(2);
|
||||
#endif
|
||||
|
||||
namespace JkBms {
|
||||
|
||||
bool Controller::init(bool verboseLogging)
|
||||
{
|
||||
_verboseLogging = verboseLogging;
|
||||
|
||||
std::string ifcType = "transceiver";
|
||||
if (Interface::Transceiver != getInterface()) { ifcType = "TTL-UART"; }
|
||||
MessageOutput.printf("[JK BMS] Initialize %s interface...\r\n", ifcType.c_str());
|
||||
|
||||
const PinMapping_t& pin = PinMapping.get();
|
||||
MessageOutput.printf("[JK BMS] rx = %d, rxen = %d, tx = %d, txen = %d\r\n",
|
||||
pin.battery_rx, pin.battery_rxen, pin.battery_tx, pin.battery_txen);
|
||||
|
||||
if (pin.battery_rx < 0 || pin.battery_tx < 0) {
|
||||
MessageOutput.println("[JK BMS] Invalid RX/TX pin config");
|
||||
return false;
|
||||
}
|
||||
|
||||
HwSerial.begin(115200, SERIAL_8N1, pin.battery_rx, pin.battery_tx);
|
||||
HwSerial.flush();
|
||||
|
||||
if (Interface::Transceiver != getInterface()) { return true; }
|
||||
|
||||
_rxEnablePin = pin.battery_rxen;
|
||||
_txEnablePin = pin.battery_txen;
|
||||
|
||||
if (_rxEnablePin < 0 || _txEnablePin < 0) {
|
||||
MessageOutput.println("[JK BMS] Invalid transceiver pin config");
|
||||
return false;
|
||||
}
|
||||
|
||||
pinMode(_rxEnablePin, OUTPUT);
|
||||
pinMode(_txEnablePin, OUTPUT);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Controller::deinit()
|
||||
{
|
||||
HwSerial.end();
|
||||
|
||||
if (_rxEnablePin > 0) { pinMode(_rxEnablePin, INPUT); }
|
||||
if (_txEnablePin > 0) { pinMode(_txEnablePin, INPUT); }
|
||||
}
|
||||
|
||||
Controller::Interface Controller::getInterface() const
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
if (0x00 == config.Battery.JkBmsInterface) { return Interface::Uart; }
|
||||
if (0x01 == config.Battery.JkBmsInterface) { return Interface::Transceiver; }
|
||||
return Interface::Invalid;
|
||||
}
|
||||
|
||||
frozen::string const& Controller::getStatusText(Controller::Status status)
|
||||
{
|
||||
static constexpr frozen::string missing = "programmer error: missing status text";
|
||||
|
||||
static constexpr frozen::map<Status, frozen::string, 6> texts = {
|
||||
{ Status::Timeout, "timeout wating for response from BMS" },
|
||||
{ Status::WaitingForPollInterval, "waiting for poll interval to elapse" },
|
||||
{ Status::HwSerialNotAvailableForWrite, "UART is not available for writing" },
|
||||
{ Status::BusyReading, "busy waiting for or reading a message from the BMS" },
|
||||
{ Status::RequestSent, "request for data sent" },
|
||||
{ Status::FrameCompleted, "a whole frame was received" }
|
||||
};
|
||||
|
||||
auto iter = texts.find(status);
|
||||
if (iter == texts.end()) { return missing; }
|
||||
|
||||
return iter->second;
|
||||
}
|
||||
|
||||
void Controller::announceStatus(Controller::Status status)
|
||||
{
|
||||
if (_lastStatus == status && millis() < _lastStatusPrinted + 10 * 1000) { return; }
|
||||
|
||||
MessageOutput.printf("[%11.3f] JK BMS: %s\r\n",
|
||||
static_cast<double>(millis())/1000, getStatusText(status).data());
|
||||
|
||||
_lastStatus = status;
|
||||
_lastStatusPrinted = millis();
|
||||
}
|
||||
|
||||
void Controller::sendRequest(uint8_t pollInterval)
|
||||
{
|
||||
if (ReadState::Idle != _readState) {
|
||||
return announceStatus(Status::BusyReading);
|
||||
}
|
||||
|
||||
if ((millis() - _lastRequest) < pollInterval * 1000) {
|
||||
return announceStatus(Status::WaitingForPollInterval);
|
||||
}
|
||||
|
||||
if (!HwSerial.availableForWrite()) {
|
||||
return announceStatus(Status::HwSerialNotAvailableForWrite);
|
||||
}
|
||||
|
||||
SerialCommand readAll(SerialCommand::Command::ReadAll);
|
||||
|
||||
if (Interface::Transceiver == getInterface()) {
|
||||
digitalWrite(_rxEnablePin, HIGH); // disable reception (of our own data)
|
||||
digitalWrite(_txEnablePin, HIGH); // enable transmission
|
||||
}
|
||||
|
||||
HwSerial.write(readAll.data(), readAll.size());
|
||||
|
||||
if (Interface::Transceiver == getInterface()) {
|
||||
HwSerial.flush();
|
||||
digitalWrite(_rxEnablePin, LOW); // enable reception
|
||||
digitalWrite(_txEnablePin, LOW); // disable transmission (free the bus)
|
||||
}
|
||||
|
||||
_lastRequest = millis();
|
||||
|
||||
setReadState(ReadState::WaitingForFrameStart);
|
||||
return announceStatus(Status::RequestSent);
|
||||
}
|
||||
|
||||
void Controller::loop()
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
uint8_t pollInterval = config.Battery.JkBmsPollingInterval;
|
||||
|
||||
while (HwSerial.available()) {
|
||||
rxData(HwSerial.read());
|
||||
}
|
||||
|
||||
sendRequest(pollInterval);
|
||||
|
||||
if (millis() > _lastRequest + 2 * pollInterval * 1000 + 250) {
|
||||
reset();
|
||||
return announceStatus(Status::Timeout);
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::rxData(uint8_t inbyte)
|
||||
{
|
||||
_buffer.push_back(inbyte);
|
||||
|
||||
switch(_readState) {
|
||||
case ReadState::Idle: // unsolicited message from BMS
|
||||
case ReadState::WaitingForFrameStart:
|
||||
if (inbyte == 0x4E) {
|
||||
return setReadState(ReadState::FrameStartReceived);
|
||||
}
|
||||
break;
|
||||
case ReadState::FrameStartReceived:
|
||||
if (inbyte == 0x57) {
|
||||
return setReadState(ReadState::StartMarkerReceived);
|
||||
}
|
||||
break;
|
||||
case ReadState::StartMarkerReceived:
|
||||
_frameLength = inbyte << 8 | 0x00;
|
||||
return setReadState(ReadState::FrameLengthMsbReceived);
|
||||
break;
|
||||
case ReadState::FrameLengthMsbReceived:
|
||||
_frameLength |= inbyte;
|
||||
_frameLength -= 2; // length field already read
|
||||
return setReadState(ReadState::ReadingFrame);
|
||||
break;
|
||||
case ReadState::ReadingFrame:
|
||||
_frameLength--;
|
||||
if (_frameLength == 0) {
|
||||
return frameComplete();
|
||||
}
|
||||
return setReadState(ReadState::ReadingFrame);
|
||||
break;
|
||||
}
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
void Controller::reset()
|
||||
{
|
||||
_buffer.clear();
|
||||
return setReadState(ReadState::Idle);
|
||||
}
|
||||
|
||||
void Controller::frameComplete()
|
||||
{
|
||||
announceStatus(Status::FrameCompleted);
|
||||
|
||||
if (_verboseLogging) {
|
||||
double ts = static_cast<double>(millis())/1000;
|
||||
MessageOutput.printf("[%11.3f] JK BMS: raw data (%d Bytes):",
|
||||
ts, _buffer.size());
|
||||
for (size_t ctr = 0; ctr < _buffer.size(); ++ctr) {
|
||||
if (ctr % 16 == 0) {
|
||||
MessageOutput.printf("\r\n[%11.3f] JK BMS:", ts);
|
||||
}
|
||||
MessageOutput.printf(" %02x", _buffer[ctr]);
|
||||
}
|
||||
MessageOutput.println();
|
||||
}
|
||||
|
||||
auto pResponse = std::make_unique<SerialResponse>(std::move(_buffer), _protocolVersion);
|
||||
if (pResponse->isValid()) {
|
||||
processDataPoints(pResponse->getDataPoints());
|
||||
} // if invalid, error message has been produced by SerialResponse c'tor
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
void Controller::processDataPoints(DataPointContainer const& dataPoints)
|
||||
{
|
||||
_stats->updateFrom(dataPoints);
|
||||
|
||||
using Label = JkBms::DataPointLabel;
|
||||
|
||||
auto oProtocolVersion = dataPoints.get<Label::ProtocolVersion>();
|
||||
if (oProtocolVersion.has_value()) { _protocolVersion = *oProtocolVersion; }
|
||||
|
||||
if (!_verboseLogging) { return; }
|
||||
|
||||
auto iter = dataPoints.cbegin();
|
||||
while ( iter != dataPoints.cend() ) {
|
||||
MessageOutput.printf("[%11.3f] JK BMS: %s: %s%s\r\n",
|
||||
static_cast<double>(iter->second.getTimestamp())/1000,
|
||||
iter->second.getLabelText().c_str(),
|
||||
iter->second.getValueText().c_str(),
|
||||
iter->second.getUnitText().c_str());
|
||||
++iter;
|
||||
}
|
||||
}
|
||||
|
||||
} /* namespace JkBms */
|
||||
63
src/JkBmsDataPoints.cpp
Normal file
@ -0,0 +1,63 @@
|
||||
#include <stdio.h>
|
||||
|
||||
#include "JkBmsDataPoints.h"
|
||||
|
||||
namespace JkBms {
|
||||
|
||||
static char conversionBuffer[16];
|
||||
|
||||
template<typename T>
|
||||
std::string dataPointValueToStr(T const& v) {
|
||||
snprintf(conversionBuffer, sizeof(conversionBuffer), "%d", v);
|
||||
return conversionBuffer;
|
||||
}
|
||||
|
||||
// explicit instanciations for the above unspecialized implementation
|
||||
template std::string dataPointValueToStr(int16_t const& v);
|
||||
template std::string dataPointValueToStr(int32_t const& v);
|
||||
template std::string dataPointValueToStr(uint8_t const& v);
|
||||
template std::string dataPointValueToStr(uint16_t const& v);
|
||||
template std::string dataPointValueToStr(uint32_t const& v);
|
||||
|
||||
template<>
|
||||
std::string dataPointValueToStr(std::string const& v) {
|
||||
return v;
|
||||
}
|
||||
|
||||
template<>
|
||||
std::string dataPointValueToStr(bool const& v) {
|
||||
return v?"yes":"no";
|
||||
}
|
||||
|
||||
template<>
|
||||
std::string dataPointValueToStr(tCells const& v) {
|
||||
std::string res;
|
||||
res.reserve(v.size()*(2+2+1+4)); // separator, index, equal sign, value
|
||||
res += "(";
|
||||
std::string sep = "";
|
||||
for(auto const& mapval : v) {
|
||||
snprintf(conversionBuffer, sizeof(conversionBuffer), "%s%d=%d",
|
||||
sep.c_str(), mapval.first, mapval.second);
|
||||
res += conversionBuffer;
|
||||
sep = ", ";
|
||||
}
|
||||
res += ")";
|
||||
return std::move(res);
|
||||
}
|
||||
|
||||
void DataPointContainer::updateFrom(DataPointContainer const& source)
|
||||
{
|
||||
for (auto iter = source.cbegin(); iter != source.cend(); ++iter) {
|
||||
auto pos = _dataPoints.find(iter->first);
|
||||
|
||||
if (pos != _dataPoints.end()) {
|
||||
// do not update existing data points with the same value
|
||||
if (pos->second == iter->second) { continue; }
|
||||
|
||||
_dataPoints.erase(pos);
|
||||
}
|
||||
_dataPoints.insert(*iter);
|
||||
}
|
||||
}
|
||||
|
||||
} /* namespace JkBms */
|
||||
363
src/JkBmsSerialMessage.cpp
Normal file
@ -0,0 +1,363 @@
|
||||
#include <numeric>
|
||||
|
||||
#include "JkBmsSerialMessage.h"
|
||||
#include "MessageOutput.h"
|
||||
|
||||
namespace JkBms {
|
||||
|
||||
SerialCommand::SerialCommand(SerialCommand::Command cmd)
|
||||
: SerialMessage(20, 0x00)
|
||||
{
|
||||
set(_raw.begin(), startMarker);
|
||||
set(_raw.begin() + 2, static_cast<uint16_t>(_raw.size() - 2)); // frame length
|
||||
set(_raw.begin() + 8, static_cast<uint8_t>(cmd));
|
||||
set(_raw.begin() + 9, static_cast<uint8_t>(Source::Host));
|
||||
set(_raw.begin() + 10, static_cast<uint8_t>(Type::Command));
|
||||
set(_raw.end() - 5, endMarker);
|
||||
updateChecksum();
|
||||
}
|
||||
|
||||
using Label = JkBms::DataPointLabel;
|
||||
template<Label L> using Traits = DataPointLabelTraits<L>;
|
||||
|
||||
SerialResponse::SerialResponse(tData&& raw, uint8_t protocolVersion)
|
||||
: SerialMessage(std::move(raw))
|
||||
{
|
||||
if (!isValid()) { return; }
|
||||
|
||||
auto pos = _raw.cbegin() + 11;
|
||||
auto end = pos + getVariableFieldLength();
|
||||
|
||||
while ( pos < end ) {
|
||||
uint8_t fieldType = *(pos++);
|
||||
|
||||
/**
|
||||
* there seems to be no way to make this more generic. the main reason
|
||||
* is that a non-constexpr value (fieldType cast as Label) cannot be
|
||||
* used as a template parameter.
|
||||
*/
|
||||
switch(fieldType) {
|
||||
case 0x79:
|
||||
{
|
||||
uint8_t cellAmount = *(pos++) / 3;
|
||||
std::map<uint8_t, uint16_t> voltages;
|
||||
for (size_t cellCounter = 0; cellCounter < cellAmount; ++cellCounter) {
|
||||
uint8_t idx = *(pos++);
|
||||
auto cellMilliVolt = get<uint16_t>(pos);
|
||||
voltages[idx] = cellMilliVolt;
|
||||
}
|
||||
_dp.add<Label::CellsMilliVolt>(voltages);
|
||||
break;
|
||||
}
|
||||
case 0x80:
|
||||
_dp.add<Label::BmsTempCelsius>(getTemperature(pos));
|
||||
break;
|
||||
case 0x81:
|
||||
_dp.add<Label::BatteryTempOneCelsius>(getTemperature(pos));
|
||||
break;
|
||||
case 0x82:
|
||||
_dp.add<Label::BatteryTempTwoCelsius>(getTemperature(pos));
|
||||
break;
|
||||
case 0x83:
|
||||
_dp.add<Label::BatteryVoltageMilliVolt>(static_cast<uint32_t>(get<uint16_t>(pos)) * 10);
|
||||
break;
|
||||
case 0x84:
|
||||
processBatteryCurrent(pos, protocolVersion);
|
||||
break;
|
||||
case 0x85:
|
||||
_dp.add<Label::BatterySoCPercent>(get<uint8_t>(pos));
|
||||
break;
|
||||
case 0x86:
|
||||
_dp.add<Label::BatteryTemperatureSensorAmount>(get<uint8_t>(pos));
|
||||
break;
|
||||
case 0x87:
|
||||
_dp.add<Label::BatteryCycles>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x89:
|
||||
_dp.add<Label::BatteryCycleCapacity>(get<uint32_t>(pos));
|
||||
break;
|
||||
case 0x8a:
|
||||
_dp.add<Label::BatteryCellAmount>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x8b:
|
||||
_dp.add<Label::AlarmsBitmask>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x8c:
|
||||
_dp.add<Label::StatusBitmask>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x8e:
|
||||
_dp.add<Label::TotalOvervoltageThresholdMilliVolt>(static_cast<uint32_t>(get<uint16_t>(pos)) * 10);
|
||||
break;
|
||||
case 0x8f:
|
||||
_dp.add<Label::TotalUndervoltageThresholdMilliVolt>(static_cast<uint32_t>(get<uint16_t>(pos)) * 10);
|
||||
break;
|
||||
case 0x90:
|
||||
_dp.add<Label::CellOvervoltageThresholdMilliVolt>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x91:
|
||||
_dp.add<Label::CellOvervoltageRecoveryMilliVolt>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x92:
|
||||
_dp.add<Label::CellOvervoltageProtectionDelaySeconds>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x93:
|
||||
_dp.add<Label::CellUndervoltageThresholdMilliVolt>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x94:
|
||||
_dp.add<Label::CellUndervoltageRecoveryMilliVolt>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x95:
|
||||
_dp.add<Label::CellUndervoltageProtectionDelaySeconds>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x96:
|
||||
_dp.add<Label::CellVoltageDiffThresholdMilliVolt>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x97:
|
||||
_dp.add<Label::DischargeOvercurrentThresholdAmperes>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x98:
|
||||
_dp.add<Label::DischargeOvercurrentDelaySeconds>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x99:
|
||||
_dp.add<Label::ChargeOvercurrentThresholdAmps>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x9a:
|
||||
_dp.add<Label::ChargeOvercurrentDelaySeconds>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x9b:
|
||||
_dp.add<Label::BalanceCellVoltageThresholdMilliVolt>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x9c:
|
||||
_dp.add<Label::BalanceVoltageDiffThresholdMilliVolt>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x9d:
|
||||
_dp.add<Label::BalancingEnabled>(get<bool>(pos));
|
||||
break;
|
||||
case 0x9e:
|
||||
_dp.add<Label::BmsTempProtectionThresholdCelsius>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0x9f:
|
||||
_dp.add<Label::BmsTempRecoveryThresholdCelsius>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0xa0:
|
||||
_dp.add<Label::BatteryTempProtectionThresholdCelsius>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0xa1:
|
||||
_dp.add<Label::BatteryTempRecoveryThresholdCelsius>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0xa2:
|
||||
_dp.add<Label::BatteryTempDiffThresholdCelsius>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0xa3:
|
||||
_dp.add<Label::ChargeHighTempThresholdCelsius>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0xa4:
|
||||
_dp.add<Label::DischargeHighTempThresholdCelsius>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0xa5:
|
||||
_dp.add<Label::ChargeLowTempThresholdCelsius>(get<int16_t>(pos));
|
||||
break;
|
||||
case 0xa6:
|
||||
_dp.add<Label::ChargeLowTempRecoveryCelsius>(get<int16_t>(pos));
|
||||
break;
|
||||
case 0xa7:
|
||||
_dp.add<Label::DischargeLowTempThresholdCelsius>(get<int16_t>(pos));
|
||||
break;
|
||||
case 0xa8:
|
||||
_dp.add<Label::DischargeLowTempRecoveryCelsius>(get<int16_t>(pos));
|
||||
break;
|
||||
case 0xa9:
|
||||
_dp.add<Label::CellAmountSetting>(get<uint8_t>(pos));
|
||||
break;
|
||||
case 0xaa:
|
||||
_dp.add<Label::BatteryCapacitySettingAmpHours>(get<uint32_t>(pos));
|
||||
break;
|
||||
case 0xab:
|
||||
_dp.add<Label::BatteryChargeEnabled>(get<bool>(pos));
|
||||
break;
|
||||
case 0xac:
|
||||
_dp.add<Label::BatteryDischargeEnabled>(get<bool>(pos));
|
||||
break;
|
||||
case 0xad:
|
||||
_dp.add<Label::CurrentCalibrationMilliAmps>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0xae:
|
||||
_dp.add<Label::BmsAddress>(get<uint8_t>(pos));
|
||||
break;
|
||||
case 0xaf:
|
||||
_dp.add<Label::BatteryType>(get<uint8_t>(pos));
|
||||
break;
|
||||
case 0xb0:
|
||||
_dp.add<Label::SleepWaitTime>(get<uint16_t>(pos));
|
||||
break;
|
||||
case 0xb1:
|
||||
_dp.add<Label::LowCapacityAlarmThresholdPercent>(get<uint8_t>(pos));
|
||||
break;
|
||||
case 0xb2:
|
||||
_dp.add<Label::ModificationPassword>(getString(pos, 10));
|
||||
break;
|
||||
case 0xb3:
|
||||
_dp.add<Label::DedicatedChargerSwitch>(getBool(pos));
|
||||
break;
|
||||
case 0xb4:
|
||||
_dp.add<Label::EquipmentId>(getString(pos, 8));
|
||||
break;
|
||||
case 0xb5:
|
||||
_dp.add<Label::DateOfManufacturing >(getString(pos, 4));
|
||||
break;
|
||||
case 0xb6:
|
||||
_dp.add<Label::BmsHourMeterMinutes>(get<uint32_t>(pos));
|
||||
break;
|
||||
case 0xb7:
|
||||
_dp.add<Label::BmsSoftwareVersion>(getString(pos, 15));
|
||||
break;
|
||||
case 0xb8:
|
||||
_dp.add<Label::CurrentCalibration>(getBool(pos));
|
||||
break;
|
||||
case 0xb9:
|
||||
_dp.add<Label::ActualBatteryCapacityAmpHours>(get<uint32_t>(pos));
|
||||
break;
|
||||
case 0xba:
|
||||
_dp.add<Label::ProductId>(getString(pos, 24, true));
|
||||
break;
|
||||
case 0xc0:
|
||||
_dp.add<Label::ProtocolVersion>(get<uint8_t>(pos));
|
||||
break;
|
||||
default:
|
||||
MessageOutput.printf("unknown field type 0x%02x\r\n", fieldType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE that this function moves the iterator by the amount of bytes read.
|
||||
*/
|
||||
template<typename T, typename It>
|
||||
T SerialMessage::get(It&& pos) const
|
||||
{
|
||||
// add easy-to-understand error message when called with non-const iter,
|
||||
// as compiler generated error message is hard to understand.
|
||||
using ItNoRef = typename std::remove_reference<It>::type;
|
||||
using PtrType = typename std::iterator_traits<ItNoRef>::pointer;
|
||||
using ValueType = typename std::remove_pointer<PtrType>::type;
|
||||
static_assert(std::is_const<ValueType>::value, "get() must be called with a const_iterator");
|
||||
|
||||
// avoid out-of-bound read
|
||||
if (std::distance(pos, _raw.cend()) < sizeof(T)) { return 0; }
|
||||
|
||||
T res = 0;
|
||||
for (unsigned i = 0; i < sizeof(T); ++i) {
|
||||
res |= static_cast<T>(*(pos++)) << (sizeof(T)-1-i)*8;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
template<typename It>
|
||||
bool SerialMessage::getBool(It&& pos) const
|
||||
{
|
||||
uint8_t raw = get<uint8_t>(pos);
|
||||
return raw > 0;
|
||||
}
|
||||
|
||||
template<typename It>
|
||||
int16_t SerialMessage::getTemperature(It&& pos) const
|
||||
{
|
||||
uint16_t raw = get<uint16_t>(pos);
|
||||
if (raw <= 100) { return static_cast<int16_t>(raw); }
|
||||
return static_cast<int16_t>(raw - 100) * (-1);
|
||||
}
|
||||
|
||||
template<typename It>
|
||||
std::string SerialMessage::getString(It&& pos, size_t len, bool replaceZeroes) const
|
||||
{
|
||||
// avoid out-of-bound read
|
||||
len = std::min<size_t>(std::distance(pos, _raw.cend()), len);
|
||||
|
||||
auto start = pos;
|
||||
pos += len;
|
||||
|
||||
if (replaceZeroes) {
|
||||
std::vector<uint8_t> copy(start, pos);
|
||||
for (auto& c : copy) {
|
||||
if (c == 0) { c = 0x20; } // replace by ASCII space
|
||||
}
|
||||
return std::string(copy.cbegin(), copy.cend());
|
||||
}
|
||||
|
||||
return std::string(start, pos);
|
||||
}
|
||||
|
||||
void SerialMessage::processBatteryCurrent(SerialMessage::tData::const_iterator& pos, uint8_t protocolVersion)
|
||||
{
|
||||
uint16_t raw = get<uint16_t>(pos);
|
||||
|
||||
if (0x00 == protocolVersion) {
|
||||
// untested!
|
||||
_dp.add<Label::BatteryCurrentMilliAmps>((static_cast<int32_t>(10000) - raw) * 10);
|
||||
return;
|
||||
}
|
||||
else if (0x01 == protocolVersion) {
|
||||
bool charging = (raw & 0x8000) > 0;
|
||||
_dp.add<Label::BatteryCurrentMilliAmps>(static_cast<int32_t>(raw & 0x7FFF) * (charging ? 10 : -10));
|
||||
return;
|
||||
}
|
||||
|
||||
MessageOutput.println("cannot decode battery current field without knowing the protocol version");
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void SerialMessage::set(tData::iterator const& pos, T val)
|
||||
{
|
||||
// avoid out-of-bound write
|
||||
if (std::distance(pos, _raw.end()) < sizeof(T)) { return; }
|
||||
|
||||
for (unsigned i = 0; i < sizeof(T); ++i) {
|
||||
*(pos+i) = static_cast<uint8_t>(val >> (sizeof(T)-1-i)*8);
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t SerialMessage::calcChecksum() const
|
||||
{
|
||||
return std::accumulate(_raw.cbegin(), _raw.cend()-4, 0);
|
||||
}
|
||||
|
||||
void SerialMessage::updateChecksum()
|
||||
{
|
||||
set(_raw.end()-2, calcChecksum());
|
||||
}
|
||||
|
||||
bool SerialMessage::isValid() const {
|
||||
uint16_t const actualStartMarker = get<uint16_t>(_raw.cbegin());
|
||||
if (actualStartMarker != startMarker) {
|
||||
MessageOutput.printf("JkBms::SerialMessage: invalid start marker %04x, expected 0x%04x\r\n",
|
||||
actualStartMarker, startMarker);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t const frameLength = get<uint16_t>(_raw.cbegin()+2);
|
||||
if (frameLength != _raw.size() - 2) {
|
||||
MessageOutput.printf("JkBms::SerialMessage: unexpected frame length %04x, expected 0x%04x\r\n",
|
||||
frameLength, _raw.size() - 2);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t const actualEndMarker = *(_raw.cend()-5);
|
||||
if (actualEndMarker != endMarker) {
|
||||
MessageOutput.printf("JkBms::SerialMessage: invalid end marker %02x, expected 0x%02x\r\n",
|
||||
actualEndMarker, endMarker);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t const actualChecksum = get<uint16_t>(_raw.cend()-2);
|
||||
uint16_t const expectedChecksum = calcChecksum();
|
||||
if (actualChecksum != expectedChecksum) {
|
||||
MessageOutput.printf("JkBms::SerialMessage: invalid checksum 0x%04x, expected 0x%04x\r\n",
|
||||
actualChecksum, expectedChecksum);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} /* namespace JkBms */
|
||||
@ -2,10 +2,9 @@
|
||||
/*
|
||||
* Copyright (C) 2022-2023 Thomas Basler and others
|
||||
*/
|
||||
#include <HardwareSerial.h>
|
||||
#include "MessageOutput.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
MessageOutputClass MessageOutput;
|
||||
|
||||
void MessageOutputClass::init(Scheduler& scheduler)
|
||||
@ -18,46 +17,97 @@ void MessageOutputClass::init(Scheduler& scheduler)
|
||||
|
||||
void MessageOutputClass::register_ws_output(AsyncWebSocket* output)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_msgLock);
|
||||
|
||||
_ws = output;
|
||||
}
|
||||
|
||||
void MessageOutputClass::serialWrite(MessageOutputClass::message_t const& m)
|
||||
{
|
||||
// on ESP32-S3, Serial.flush() blocks until a serial console is attached.
|
||||
// operator bool() of HWCDC returns false if the device is not attached to
|
||||
// a USB host. in general it makes sense to skip writing entirely if the
|
||||
// default serial port is not ready.
|
||||
if (!Serial) { return; }
|
||||
|
||||
size_t written = 0;
|
||||
while (written < m.size()) {
|
||||
written += Serial.write(m.data() + written, m.size() - written);
|
||||
}
|
||||
Serial.flush();
|
||||
}
|
||||
|
||||
size_t MessageOutputClass::write(uint8_t c)
|
||||
{
|
||||
if (_buff_pos < BUFFER_SIZE) {
|
||||
std::lock_guard<std::mutex> lock(_msgLock);
|
||||
_buffer[_buff_pos] = c;
|
||||
_buff_pos++;
|
||||
} else {
|
||||
_forceSend = true;
|
||||
std::lock_guard<std::mutex> lock(_msgLock);
|
||||
|
||||
auto res = _task_messages.emplace(xTaskGetCurrentTaskHandle(), message_t());
|
||||
auto iter = res.first;
|
||||
auto& message = iter->second;
|
||||
|
||||
message.push_back(c);
|
||||
|
||||
if (c == '\n') {
|
||||
serialWrite(message);
|
||||
_lines.emplace(std::move(message));
|
||||
_task_messages.erase(iter);
|
||||
}
|
||||
|
||||
return Serial.write(c);
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t MessageOutputClass::write(const uint8_t* buffer, size_t size)
|
||||
size_t MessageOutputClass::write(const uint8_t *buffer, size_t size)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_msgLock);
|
||||
if (_buff_pos + size < BUFFER_SIZE) {
|
||||
memcpy(&_buffer[_buff_pos], buffer, size);
|
||||
_buff_pos += size;
|
||||
}
|
||||
_forceSend = true;
|
||||
|
||||
return Serial.write(buffer, size);
|
||||
auto res = _task_messages.emplace(xTaskGetCurrentTaskHandle(), message_t());
|
||||
auto iter = res.first;
|
||||
auto& message = iter->second;
|
||||
|
||||
message.reserve(message.size() + size);
|
||||
|
||||
for (size_t idx = 0; idx < size; ++idx) {
|
||||
uint8_t c = buffer[idx];
|
||||
|
||||
message.push_back(c);
|
||||
|
||||
if (c == '\n') {
|
||||
serialWrite(message);
|
||||
_lines.emplace(std::move(message));
|
||||
message.clear();
|
||||
message.reserve(size - idx - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (message.empty()) { _task_messages.erase(iter); }
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
void MessageOutputClass::loop()
|
||||
{
|
||||
// Send data via websocket if either time is over or buffer is full
|
||||
if (_forceSend || (millis() - _lastSend > 1000)) {
|
||||
std::lock_guard<std::mutex> lock(_msgLock);
|
||||
if (_ws && _buff_pos > 0) {
|
||||
_ws->textAll(_buffer, _buff_pos);
|
||||
_buff_pos = 0;
|
||||
std::lock_guard<std::mutex> lock(_msgLock);
|
||||
|
||||
// clean up (possibly filled) buffers of deleted tasks
|
||||
auto map_iter = _task_messages.begin();
|
||||
while (map_iter != _task_messages.end()) {
|
||||
if (eTaskGetState(map_iter->first) == eDeleted) {
|
||||
map_iter = _task_messages.erase(map_iter);
|
||||
continue;
|
||||
}
|
||||
if (_forceSend) {
|
||||
_buff_pos = 0;
|
||||
|
||||
++map_iter;
|
||||
}
|
||||
|
||||
if (!_ws) {
|
||||
while (!_lines.empty()) {
|
||||
_lines.pop(); // do not hog memory
|
||||
}
|
||||
_forceSend = false;
|
||||
return;
|
||||
}
|
||||
|
||||
while (!_lines.empty() && _ws->availableForWriteAll()) {
|
||||
_ws->textAll(std::make_shared<message_t>(std::move(_lines.front())));
|
||||
_lines.pop();
|
||||
}
|
||||
}
|
||||
65
src/MqttBattery.cpp
Normal file
@ -0,0 +1,65 @@
|
||||
#include <functional>
|
||||
|
||||
#include "Configuration.h"
|
||||
#include "MqttBattery.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "MessageOutput.h"
|
||||
|
||||
bool MqttBattery::init(bool verboseLogging)
|
||||
{
|
||||
_verboseLogging = verboseLogging;
|
||||
|
||||
auto const& config = Configuration.get();
|
||||
_socTopic = config.Battery.MqttTopic;
|
||||
|
||||
if (_socTopic.isEmpty()) { return false; }
|
||||
|
||||
MqttSettings.subscribe(_socTopic, 0/*QoS*/,
|
||||
std::bind(&MqttBattery::onMqttMessage,
|
||||
this, std::placeholders::_1, std::placeholders::_2,
|
||||
std::placeholders::_3, std::placeholders::_4,
|
||||
std::placeholders::_5, std::placeholders::_6)
|
||||
);
|
||||
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("MqttBattery: Subscribed to '%s'\r\n",
|
||||
_socTopic.c_str());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void MqttBattery::deinit()
|
||||
{
|
||||
if (_socTopic.isEmpty()) { return; }
|
||||
MqttSettings.unsubscribe(_socTopic);
|
||||
}
|
||||
|
||||
void MqttBattery::onMqttMessage(espMqttClientTypes::MessageProperties const& properties,
|
||||
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total)
|
||||
{
|
||||
float soc = 0;
|
||||
std::string value(reinterpret_cast<const char*>(payload), len);
|
||||
|
||||
try {
|
||||
soc = std::stof(value);
|
||||
}
|
||||
catch(std::invalid_argument const& e) {
|
||||
MessageOutput.printf("MqttBattery: Cannot parse payload '%s' in topic '%s' as float\r\n",
|
||||
value.c_str(), topic);
|
||||
return;
|
||||
}
|
||||
|
||||
if (soc < 0 || soc > 100) {
|
||||
MessageOutput.printf("MqttBattery: Implausible SoC '%.2f' in topic '%s'\r\n",
|
||||
soc, topic);
|
||||
return;
|
||||
}
|
||||
|
||||
_stats->setSoC(static_cast<uint8_t>(soc));
|
||||
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("MqttBattery: Updated SoC to %d from '%s'\r\n",
|
||||
static_cast<uint8_t>(soc), topic);
|
||||
}
|
||||
}
|
||||
205
src/MqttHandlVedirectHass.cpp
Normal file
@ -0,0 +1,205 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022 Thomas Basler and others
|
||||
*/
|
||||
#include "MqttHandleVedirectHass.h"
|
||||
#include "Configuration.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "NetworkSettings.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "VictronMppt.h"
|
||||
#include "Utils.h"
|
||||
|
||||
MqttHandleVedirectHassClass MqttHandleVedirectHass;
|
||||
|
||||
void MqttHandleVedirectHassClass::init(Scheduler& scheduler)
|
||||
{
|
||||
scheduler.addTask(_loopTask);
|
||||
_loopTask.setCallback(std::bind(&MqttHandleVedirectHassClass::loop, this));
|
||||
_loopTask.setIterations(TASK_FOREVER);
|
||||
_loopTask.enable();
|
||||
}
|
||||
|
||||
void MqttHandleVedirectHassClass::loop()
|
||||
{
|
||||
if (!Configuration.get().Vedirect.Enabled) {
|
||||
return;
|
||||
}
|
||||
if (_updateForced) {
|
||||
publishConfig();
|
||||
_updateForced = false;
|
||||
}
|
||||
|
||||
if (MqttSettings.getConnected() && !_wasConnected) {
|
||||
// Connection established
|
||||
_wasConnected = true;
|
||||
publishConfig();
|
||||
} else if (!MqttSettings.getConnected() && _wasConnected) {
|
||||
// Connection lost
|
||||
_wasConnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
void MqttHandleVedirectHassClass::forceUpdate()
|
||||
{
|
||||
_updateForced = true;
|
||||
}
|
||||
|
||||
void MqttHandleVedirectHassClass::publishConfig()
|
||||
{
|
||||
if ((!Configuration.get().Mqtt.Hass.Enabled) ||
|
||||
(!Configuration.get().Vedirect.Enabled)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!MqttSettings.getConnected()) {
|
||||
return;
|
||||
}
|
||||
// ensure data is revieved from victron
|
||||
if (!VictronMppt.isDataValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// device info
|
||||
publishBinarySensor("MPPT load output state", "mdi:export", "LOAD", "ON", "OFF");
|
||||
publishSensor("MPPT serial number", "mdi:counter", "SER");
|
||||
publishSensor("MPPT firmware number", "mdi:counter", "FW");
|
||||
publishSensor("MPPT state of operation", "mdi:wrench", "CS");
|
||||
publishSensor("MPPT error code", "mdi:bell", "ERR");
|
||||
publishSensor("MPPT off reason", "mdi:wrench", "OR");
|
||||
publishSensor("MPPT tracker operation mode", "mdi:wrench", "MPPT");
|
||||
publishSensor("MPPT Day sequence number (0...364)", "mdi:calendar-month-outline", "HSDS", NULL, "total", "d");
|
||||
|
||||
// battery info
|
||||
publishSensor("Battery voltage", NULL, "V", "voltage", "measurement", "V");
|
||||
publishSensor("Battery current", NULL, "I", "current", "measurement", "A");
|
||||
publishSensor("Battery power (calculated)", NULL, "P", "power", "measurement", "W");
|
||||
publishSensor("Battery efficiency (calculated)", NULL, "E", NULL, "measurement", "%");
|
||||
|
||||
// panel info
|
||||
publishSensor("Panel voltage", NULL, "VPV", "voltage", "measurement", "V");
|
||||
publishSensor("Panel current (calculated)", NULL, "IPV", "current", "measurement", "A");
|
||||
publishSensor("Panel power", NULL, "PPV", "power", "measurement", "W");
|
||||
publishSensor("Panel yield total", NULL, "H19", "energy", "total_increasing", "kWh");
|
||||
publishSensor("Panel yield today", NULL, "H20", "energy", "total", "kWh");
|
||||
publishSensor("Panel maximum power today", NULL, "H21", "power", "measurement", "W");
|
||||
publishSensor("Panel yield yesterday", NULL, "H22", "energy", "total", "kWh");
|
||||
publishSensor("Panel maximum power yesterday", NULL, "H23", "power", "measurement", "W");
|
||||
|
||||
yield();
|
||||
}
|
||||
|
||||
void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char* icon, const char* subTopic, const char* deviceClass, const char* stateClass, const char* unitOfMeasurement )
|
||||
{
|
||||
String serial = VictronMppt.getData()->SER;
|
||||
|
||||
String sensorId = caption;
|
||||
sensorId.replace(" ", "_");
|
||||
sensorId.replace(".", "");
|
||||
sensorId.replace("(", "");
|
||||
sensorId.replace(")", "");
|
||||
sensorId.toLowerCase();
|
||||
|
||||
String configTopic = "sensor/dtu_victron_" + serial
|
||||
+ "/" + sensorId
|
||||
+ "/config";
|
||||
|
||||
String statTopic = MqttSettings.getPrefix() + "victron/";
|
||||
statTopic.concat(serial);
|
||||
statTopic.concat("/");
|
||||
statTopic.concat(subTopic);
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
return;
|
||||
}
|
||||
root["name"] = caption;
|
||||
root["stat_t"] = statTopic;
|
||||
root["uniq_id"] = serial + "_" + sensorId;
|
||||
|
||||
if (icon != NULL) {
|
||||
root["icon"] = icon;
|
||||
}
|
||||
|
||||
if (unitOfMeasurement != NULL) {
|
||||
root["unit_of_meas"] = unitOfMeasurement;
|
||||
}
|
||||
|
||||
JsonObject deviceObj = root.createNestedObject("dev");
|
||||
createDeviceInfo(deviceObj);
|
||||
|
||||
if (Configuration.get().Mqtt.Hass.Expire) {
|
||||
root["exp_aft"] = Configuration.get().Mqtt.PublishInterval * 3;
|
||||
}
|
||||
if (deviceClass != NULL) {
|
||||
root["dev_cla"] = deviceClass;
|
||||
}
|
||||
if (stateClass != NULL) {
|
||||
root["stat_cla"] = stateClass;
|
||||
}
|
||||
|
||||
char buffer[512];
|
||||
serializeJson(root, buffer);
|
||||
publish(configTopic, buffer);
|
||||
|
||||
}
|
||||
void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const char* icon, const char* subTopic, const char* payload_on, const char* payload_off)
|
||||
{
|
||||
String serial = VictronMppt.getData()->SER;
|
||||
|
||||
String sensorId = caption;
|
||||
sensorId.replace(" ", "_");
|
||||
sensorId.replace(".", "");
|
||||
sensorId.replace("(", "");
|
||||
sensorId.replace(")", "");
|
||||
sensorId.toLowerCase();
|
||||
|
||||
String configTopic = "binary_sensor/dtu_victron_" + serial
|
||||
+ "/" + sensorId
|
||||
+ "/config";
|
||||
|
||||
String statTopic = MqttSettings.getPrefix() + "victron/";
|
||||
statTopic.concat(serial);
|
||||
statTopic.concat("/");
|
||||
statTopic.concat(subTopic);
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
return;
|
||||
}
|
||||
root["name"] = caption;
|
||||
root["uniq_id"] = serial + "_" + sensorId;
|
||||
root["stat_t"] = statTopic;
|
||||
root["pl_on"] = payload_on;
|
||||
root["pl_off"] = payload_off;
|
||||
|
||||
if (icon != NULL) {
|
||||
root["icon"] = icon;
|
||||
}
|
||||
|
||||
JsonObject deviceObj = root.createNestedObject("dev");
|
||||
createDeviceInfo(deviceObj);
|
||||
|
||||
char buffer[512];
|
||||
serializeJson(root, buffer);
|
||||
publish(configTopic, buffer);
|
||||
}
|
||||
|
||||
void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject& object)
|
||||
{
|
||||
auto spMpptData = VictronMppt.getData();
|
||||
String serial = spMpptData->SER;
|
||||
object["name"] = "Victron(" + serial + ")";
|
||||
object["ids"] = serial;
|
||||
object["cu"] = String("http://") + NetworkSettings.localIP().toString();
|
||||
object["mf"] = "OpenDTU";
|
||||
object["mdl"] = spMpptData->getPidAsString();
|
||||
object["sw"] = AUTO_GIT_HASH;
|
||||
}
|
||||
|
||||
void MqttHandleVedirectHassClass::publish(const String& subtopic, const String& payload)
|
||||
{
|
||||
String topic = Configuration.get().Mqtt.Hass.Topic;
|
||||
topic += subtopic;
|
||||
MqttSettings.publishGeneric(topic.c_str(), payload.c_str(), Configuration.get().Mqtt.Hass.Retain);
|
||||
}
|
||||
161
src/MqttHandleHuawei.cpp
Normal file
@ -0,0 +1,161 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022 Thomas Basler and others
|
||||
*/
|
||||
#include "MqttHandleHuawei.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "Huawei_can.h"
|
||||
// #include "Failsafe.h"
|
||||
#include "WebApi_Huawei.h"
|
||||
#include <ctime>
|
||||
|
||||
MqttHandleHuaweiClass MqttHandleHuawei;
|
||||
|
||||
void MqttHandleHuaweiClass::init(Scheduler& scheduler)
|
||||
{
|
||||
scheduler.addTask(_loopTask);
|
||||
_loopTask.setCallback(std::bind(&MqttHandleHuaweiClass::loop, this));
|
||||
_loopTask.setIterations(TASK_FOREVER);
|
||||
_loopTask.enable();
|
||||
|
||||
String const& prefix = MqttSettings.getPrefix();
|
||||
|
||||
auto subscribe = [&prefix, this](char const* subTopic, Topic t) {
|
||||
String fullTopic(prefix + "huawei/cmd/" + subTopic);
|
||||
MqttSettings.subscribe(fullTopic.c_str(), 0,
|
||||
std::bind(&MqttHandleHuaweiClass::onMqttMessage, this, t,
|
||||
std::placeholders::_1, std::placeholders::_2,
|
||||
std::placeholders::_3, std::placeholders::_4,
|
||||
std::placeholders::_5, std::placeholders::_6));
|
||||
};
|
||||
|
||||
subscribe("limit_online_voltage", Topic::LimitOnlineVoltage);
|
||||
subscribe("limit_online_current", Topic::LimitOnlineCurrent);
|
||||
subscribe("limit_offline_voltage", Topic::LimitOfflineVoltage);
|
||||
subscribe("limit_offline_current", Topic::LimitOfflineCurrent);
|
||||
subscribe("mode", Topic::Mode);
|
||||
|
||||
_lastPublish = millis();
|
||||
|
||||
}
|
||||
|
||||
|
||||
void MqttHandleHuaweiClass::loop()
|
||||
{
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
std::unique_lock<std::mutex> mqttLock(_mqttMutex);
|
||||
|
||||
if (!config.Huawei.Enabled) {
|
||||
_mqttCallbacks.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& callback : _mqttCallbacks) { callback(); }
|
||||
_mqttCallbacks.clear();
|
||||
|
||||
mqttLock.unlock();
|
||||
|
||||
if (!MqttSettings.getConnected() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const RectifierParameters_t *rp = HuaweiCan.get();
|
||||
|
||||
if ((millis() - _lastPublish) > (config.Mqtt.PublishInterval * 1000) ) {
|
||||
MqttSettings.publish("huawei/data_age", String((millis() - HuaweiCan.getLastUpdate()) / 1000));
|
||||
MqttSettings.publish("huawei/input_voltage", String(rp->input_voltage));
|
||||
MqttSettings.publish("huawei/input_current", String(rp->input_current));
|
||||
MqttSettings.publish("huawei/input_power", String(rp->input_power));
|
||||
MqttSettings.publish("huawei/output_voltage", String(rp->output_voltage));
|
||||
MqttSettings.publish("huawei/output_current", String(rp->output_current));
|
||||
MqttSettings.publish("huawei/max_output_current", String(rp->max_output_current));
|
||||
MqttSettings.publish("huawei/output_power", String(rp->output_power));
|
||||
MqttSettings.publish("huawei/input_temp", String(rp->input_temp));
|
||||
MqttSettings.publish("huawei/output_temp", String(rp->output_temp));
|
||||
MqttSettings.publish("huawei/efficiency", String(rp->efficiency));
|
||||
|
||||
|
||||
yield();
|
||||
_lastPublish = millis();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void MqttHandleHuaweiClass::onMqttMessage(Topic t,
|
||||
const espMqttClientTypes::MessageProperties& properties,
|
||||
const char* topic, const uint8_t* payload, size_t len,
|
||||
size_t index, size_t total)
|
||||
{
|
||||
std::string strValue(reinterpret_cast<const char*>(payload), len);
|
||||
float payload_val = -1;
|
||||
try {
|
||||
payload_val = std::stof(strValue);
|
||||
}
|
||||
catch (std::invalid_argument const& e) {
|
||||
MessageOutput.printf("Huawei MQTT handler: cannot parse payload of topic '%s' as float: %s\r\n",
|
||||
topic, strValue.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> mqttLock(_mqttMutex);
|
||||
|
||||
switch (t) {
|
||||
case Topic::LimitOnlineVoltage:
|
||||
MessageOutput.printf("Limit Voltage: %f V\r\n", payload_val);
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue,
|
||||
&HuaweiCan, payload_val, HUAWEI_ONLINE_VOLTAGE));
|
||||
break;
|
||||
|
||||
case Topic::LimitOfflineVoltage:
|
||||
MessageOutput.printf("Offline Limit Voltage: %f V\r\n", payload_val);
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue,
|
||||
&HuaweiCan, payload_val, HUAWEI_OFFLINE_VOLTAGE));
|
||||
break;
|
||||
|
||||
case Topic::LimitOnlineCurrent:
|
||||
MessageOutput.printf("Limit Current: %f A\r\n", payload_val);
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue,
|
||||
&HuaweiCan, payload_val, HUAWEI_ONLINE_CURRENT));
|
||||
break;
|
||||
|
||||
case Topic::LimitOfflineCurrent:
|
||||
MessageOutput.printf("Offline Limit Current: %f A\r\n", payload_val);
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue,
|
||||
&HuaweiCan, payload_val, HUAWEI_OFFLINE_CURRENT));
|
||||
break;
|
||||
|
||||
case Topic::Mode:
|
||||
switch (static_cast<int>(payload_val)) {
|
||||
case 3:
|
||||
MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Full internal control");
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode,
|
||||
&HuaweiCan, HUAWEI_MODE_AUTO_INT));
|
||||
break;
|
||||
|
||||
case 2:
|
||||
MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Internal on/off control, external power limit");
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode,
|
||||
&HuaweiCan, HUAWEI_MODE_AUTO_EXT));
|
||||
break;
|
||||
|
||||
case 1:
|
||||
MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Turned ON");
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode,
|
||||
&HuaweiCan, HUAWEI_MODE_ON));
|
||||
break;
|
||||
|
||||
case 0:
|
||||
MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Turned OFF");
|
||||
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode,
|
||||
&HuaweiCan, HUAWEI_MODE_OFF));
|
||||
break;
|
||||
|
||||
default:
|
||||
MessageOutput.printf("[Huawei MQTT::] Invalid mode %.0f\r\n", payload_val);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
100
src/MqttHandlePowerLimiter.cpp
Normal file
@ -0,0 +1,100 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022 Thomas Basler, Malte Schmidt and others
|
||||
*/
|
||||
#include "MessageOutput.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "MqttHandlePowerLimiter.h"
|
||||
#include "PowerLimiter.h"
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
|
||||
MqttHandlePowerLimiterClass MqttHandlePowerLimiter;
|
||||
|
||||
void MqttHandlePowerLimiterClass::init(Scheduler& scheduler)
|
||||
{
|
||||
scheduler.addTask(_loopTask);
|
||||
_loopTask.setCallback(std::bind(&MqttHandlePowerLimiterClass::loop, this));
|
||||
_loopTask.setIterations(TASK_FOREVER);
|
||||
_loopTask.enable();
|
||||
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
using std::placeholders::_3;
|
||||
using std::placeholders::_4;
|
||||
using std::placeholders::_5;
|
||||
using std::placeholders::_6;
|
||||
|
||||
String topic = MqttSettings.getPrefix() + "powerlimiter/cmd/mode";
|
||||
MqttSettings.subscribe(topic.c_str(), 0, std::bind(&MqttHandlePowerLimiterClass::onCmdMode, this, _1, _2, _3, _4, _5, _6));
|
||||
|
||||
_lastPublish = millis();
|
||||
}
|
||||
|
||||
|
||||
void MqttHandlePowerLimiterClass::loop()
|
||||
{
|
||||
std::unique_lock<std::mutex> mqttLock(_mqttMutex);
|
||||
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
if (!config.PowerLimiter.Enabled) {
|
||||
_mqttCallbacks.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& callback : _mqttCallbacks) { callback(); }
|
||||
_mqttCallbacks.clear();
|
||||
|
||||
mqttLock.unlock();
|
||||
|
||||
if (!MqttSettings.getConnected() ) { return; }
|
||||
|
||||
if ((millis() - _lastPublish) > (config.Mqtt.PublishInterval * 1000) ) {
|
||||
auto val = static_cast<unsigned>(PowerLimiter.getMode());
|
||||
MqttSettings.publish("powerlimiter/status/mode", String(val));
|
||||
|
||||
yield();
|
||||
_lastPublish = millis();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void MqttHandlePowerLimiterClass::onCmdMode(const espMqttClientTypes::MessageProperties& properties,
|
||||
const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
|
||||
{
|
||||
std::string strValue(reinterpret_cast<const char*>(payload), len);
|
||||
int intValue = -1;
|
||||
try {
|
||||
intValue = std::stoi(strValue);
|
||||
}
|
||||
catch (std::invalid_argument const& e) {
|
||||
MessageOutput.printf("PowerLimiter MQTT handler: cannot parse payload of topic '%s' as int: %s\r\n",
|
||||
topic, strValue.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> mqttLock(_mqttMutex);
|
||||
|
||||
using Mode = PowerLimiterClass::Mode;
|
||||
switch (static_cast<Mode>(intValue)) {
|
||||
case Mode::UnconditionalFullSolarPassthrough:
|
||||
MessageOutput.println("Power limiter unconditional full solar PT");
|
||||
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
|
||||
&PowerLimiter, Mode::UnconditionalFullSolarPassthrough));
|
||||
break;
|
||||
case Mode::Disabled:
|
||||
MessageOutput.println("Power limiter disabled (override)");
|
||||
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
|
||||
&PowerLimiter, Mode::Disabled));
|
||||
break;
|
||||
case Mode::Normal:
|
||||
MessageOutput.println("Power limiter normal operation");
|
||||
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
|
||||
&PowerLimiter, Mode::Normal));
|
||||
break;
|
||||
default:
|
||||
MessageOutput.printf("PowerLimiter - unknown mode %d\r\n", intValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
209
src/MqttHandlePylontechHass.cpp
Normal file
@ -0,0 +1,209 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "PylontechCanReceiver.h"
|
||||
#include "Battery.h"
|
||||
#include "MqttHandlePylontechHass.h"
|
||||
#include "Configuration.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "Utils.h"
|
||||
|
||||
MqttHandlePylontechHassClass MqttHandlePylontechHass;
|
||||
|
||||
void MqttHandlePylontechHassClass::init(Scheduler& scheduler)
|
||||
{
|
||||
scheduler.addTask(_loopTask);
|
||||
_loopTask.setCallback(std::bind(&MqttHandlePylontechHassClass::loop, this));
|
||||
_loopTask.setIterations(TASK_FOREVER);
|
||||
_loopTask.enable();
|
||||
}
|
||||
|
||||
void MqttHandlePylontechHassClass::loop()
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
if (!config.Battery.Enabled) {
|
||||
return;
|
||||
}
|
||||
if (_updateForced) {
|
||||
publishConfig();
|
||||
_updateForced = false;
|
||||
}
|
||||
|
||||
if (MqttSettings.getConnected() && !_wasConnected) {
|
||||
// Connection established
|
||||
_wasConnected = true;
|
||||
publishConfig();
|
||||
} else if (!MqttSettings.getConnected() && _wasConnected) {
|
||||
// Connection lost
|
||||
_wasConnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
void MqttHandlePylontechHassClass::forceUpdate()
|
||||
{
|
||||
_updateForced = true;
|
||||
}
|
||||
|
||||
void MqttHandlePylontechHassClass::publishConfig()
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
if ((!config.Mqtt.Hass.Enabled) || (!config.Battery.Enabled)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!MqttSettings.getConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// device info
|
||||
publishSensor("Manufacturer", "mdi:factory", "manufacturer");
|
||||
|
||||
// battery info
|
||||
publishSensor("Battery voltage", NULL, "voltage", "voltage", "measurement", "V");
|
||||
publishSensor("Battery current", NULL, "current", "current", "measurement", "A");
|
||||
publishSensor("Temperature", NULL, "temperature", "temperature", "measurement", "°C");
|
||||
publishSensor("State of Charge (SOC)", NULL, "stateOfCharge", "battery", "measurement", "%");
|
||||
publishSensor("State of Health (SOH)", "mdi:heart-plus", "stateOfHealth", NULL, "measurement", "%");
|
||||
publishSensor("Charge voltage (BMS)", NULL, "settings/chargeVoltage", "voltage", "measurement", "V");
|
||||
publishSensor("Charge current limit", NULL, "settings/chargeCurrentLimitation", "current", "measurement", "A");
|
||||
publishSensor("Discharge current limit", NULL, "settings/dischargeCurrentLimitation", "current", "measurement", "A");
|
||||
|
||||
publishBinarySensor("Alarm Discharge current", "mdi:alert", "alarm/overCurrentDischarge", "1", "0");
|
||||
publishBinarySensor("Warning Discharge current", "mdi:alert-outline", "warning/highCurrentDischarge", "1", "0");
|
||||
|
||||
publishBinarySensor("Alarm Temperature low", "mdi:thermometer-low", "alarm/underTemperature", "1", "0");
|
||||
publishBinarySensor("Warning Temperature low", "mdi:thermometer-low", "warning/lowTemperature", "1", "0");
|
||||
|
||||
publishBinarySensor("Alarm Temperature high", "mdi:thermometer-high", "alarm/overTemperature", "1", "0");
|
||||
publishBinarySensor("Warning Temperature high", "mdi:thermometer-high", "warning/highTemperature", "1", "0");
|
||||
|
||||
publishBinarySensor("Alarm Voltage low", "mdi:alert", "alarm/underVoltage", "1", "0");
|
||||
publishBinarySensor("Warning Voltage low", "mdi:alert-outline", "warning/lowVoltage", "1", "0");
|
||||
|
||||
publishBinarySensor("Alarm Voltage high", "mdi:alert", "alarm/overVoltage", "1", "0");
|
||||
publishBinarySensor("Warning Voltage high", "mdi:alert-outline", "warning/highVoltage", "1", "0");
|
||||
|
||||
publishBinarySensor("Alarm BMS internal", "mdi:alert", "alarm/bmsInternal", "1", "0");
|
||||
publishBinarySensor("Warning BMS internal", "mdi:alert-outline", "warning/bmsInternal", "1", "0");
|
||||
|
||||
publishBinarySensor("Alarm High charge current", "mdi:alert", "alarm/overCurrentCharge", "1", "0");
|
||||
publishBinarySensor("Warning High charge current", "mdi:alert-outline", "warning/highCurrentCharge", "1", "0");
|
||||
|
||||
publishBinarySensor("Charge enabled", "mdi:battery-arrow-up", "charging/chargeEnabled", "1", "0");
|
||||
publishBinarySensor("Discharge enabled", "mdi:battery-arrow-down", "charging/dischargeEnabled", "1", "0");
|
||||
publishBinarySensor("Charge immediately", "mdi:alert", "charging/chargeImmediately", "1", "0");
|
||||
|
||||
yield();
|
||||
}
|
||||
|
||||
void MqttHandlePylontechHassClass::publishSensor(const char* caption, const char* icon, const char* subTopic, const char* deviceClass, const char* stateClass, const char* unitOfMeasurement )
|
||||
{
|
||||
String sensorId = caption;
|
||||
sensorId.replace(" ", "_");
|
||||
sensorId.replace(".", "");
|
||||
sensorId.replace("(", "");
|
||||
sensorId.replace(")", "");
|
||||
sensorId.toLowerCase();
|
||||
|
||||
String configTopic = "sensor/dtu_battery_" + serial
|
||||
+ "/" + sensorId
|
||||
+ "/config";
|
||||
|
||||
String statTopic = MqttSettings.getPrefix() + "battery/";
|
||||
// omit serial to avoid a breaking change
|
||||
// statTopic.concat(serial);
|
||||
// statTopic.concat("/");
|
||||
statTopic.concat(subTopic);
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
return;
|
||||
}
|
||||
root["name"] = caption;
|
||||
root["stat_t"] = statTopic;
|
||||
root["uniq_id"] = serial + "_" + sensorId;
|
||||
|
||||
if (icon != NULL) {
|
||||
root["icon"] = icon;
|
||||
}
|
||||
|
||||
if (unitOfMeasurement != NULL) {
|
||||
root["unit_of_meas"] = unitOfMeasurement;
|
||||
}
|
||||
|
||||
JsonObject deviceObj = root.createNestedObject("dev");
|
||||
createDeviceInfo(deviceObj);
|
||||
|
||||
if (Configuration.get().Mqtt.Hass.Expire) {
|
||||
root["exp_aft"] = Configuration.get().Mqtt.PublishInterval * 3;
|
||||
}
|
||||
if (deviceClass != NULL) {
|
||||
root["dev_cla"] = deviceClass;
|
||||
}
|
||||
if (stateClass != NULL) {
|
||||
root["stat_cla"] = stateClass;
|
||||
}
|
||||
|
||||
char buffer[512];
|
||||
serializeJson(root, buffer);
|
||||
publish(configTopic, buffer);
|
||||
|
||||
}
|
||||
|
||||
void MqttHandlePylontechHassClass::publishBinarySensor(const char* caption, const char* icon, const char* subTopic, const char* payload_on, const char* payload_off)
|
||||
{
|
||||
String sensorId = caption;
|
||||
sensorId.replace(" ", "_");
|
||||
sensorId.replace(".", "");
|
||||
sensorId.replace("(", "");
|
||||
sensorId.replace(")", "");
|
||||
sensorId.toLowerCase();
|
||||
|
||||
String configTopic = "binary_sensor/dtu_battery_" + serial
|
||||
+ "/" + sensorId
|
||||
+ "/config";
|
||||
|
||||
String statTopic = MqttSettings.getPrefix() + "battery/";
|
||||
// omit serial to avoid a breaking change
|
||||
// statTopic.concat(serial);
|
||||
// statTopic.concat("/");
|
||||
statTopic.concat(subTopic);
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
return;
|
||||
}
|
||||
root["name"] = caption;
|
||||
root["uniq_id"] = serial + "_" + sensorId;
|
||||
root["stat_t"] = statTopic;
|
||||
root["pl_on"] = payload_on;
|
||||
root["pl_off"] = payload_off;
|
||||
|
||||
if (icon != NULL) {
|
||||
root["icon"] = icon;
|
||||
}
|
||||
|
||||
JsonObject deviceObj = root.createNestedObject("dev");
|
||||
createDeviceInfo(deviceObj);
|
||||
|
||||
char buffer[512];
|
||||
serializeJson(root, buffer);
|
||||
publish(configTopic, buffer);
|
||||
}
|
||||
|
||||
void MqttHandlePylontechHassClass::createDeviceInfo(JsonObject& object)
|
||||
{
|
||||
object["name"] = "Battery(" + serial + ")";
|
||||
object["ids"] = serial;
|
||||
object["cu"] = String("http://") + NetworkSettings.localIP().toString();
|
||||
object["mf"] = "OpenDTU";
|
||||
object["mdl"] = Battery.getStats()->getManufacturer();
|
||||
object["sw"] = AUTO_GIT_HASH;
|
||||
}
|
||||
|
||||
void MqttHandlePylontechHassClass::publish(const String& subtopic, const String& payload)
|
||||
{
|
||||
String topic = Configuration.get().Mqtt.Hass.Topic;
|
||||
topic += subtopic;
|
||||
MqttSettings.publishGeneric(topic.c_str(), payload.c_str(), Configuration.get().Mqtt.Hass.Retain);
|
||||
}
|
||||
168
src/MqttHandleVedirect.cpp
Normal file
@ -0,0 +1,168 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022 Helge Erbe and others
|
||||
*/
|
||||
#include "VictronMppt.h"
|
||||
#include "MqttHandleVedirect.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "MessageOutput.h"
|
||||
|
||||
|
||||
|
||||
|
||||
MqttHandleVedirectClass MqttHandleVedirect;
|
||||
|
||||
// #define MQTTHANDLEVEDIRECT_DEBUG
|
||||
|
||||
void MqttHandleVedirectClass::init(Scheduler& scheduler)
|
||||
{
|
||||
scheduler.addTask(_loopTask);
|
||||
_loopTask.setCallback(std::bind(&MqttHandleVedirectClass::loop, this));
|
||||
_loopTask.setIterations(TASK_FOREVER);
|
||||
_loopTask.enable();
|
||||
|
||||
// initially force a full publish
|
||||
this->forceUpdate();
|
||||
}
|
||||
|
||||
void MqttHandleVedirectClass::forceUpdate()
|
||||
{
|
||||
// initially force a full publish
|
||||
_nextPublishUpdatesOnly = 0;
|
||||
_nextPublishFull = 1;
|
||||
}
|
||||
|
||||
|
||||
void MqttHandleVedirectClass::loop()
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
if (!MqttSettings.getConnected() || !config.Vedirect.Enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!VictronMppt.isDataValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((millis() >= _nextPublishFull) || (millis() >= _nextPublishUpdatesOnly)) {
|
||||
// determine if this cycle should publish full values or updates only
|
||||
if (_nextPublishFull <= _nextPublishUpdatesOnly) {
|
||||
_PublishFull = true;
|
||||
} else {
|
||||
_PublishFull = !config.Vedirect.UpdatesOnly;
|
||||
}
|
||||
|
||||
#ifdef MQTTHANDLEVEDIRECT_DEBUG
|
||||
MessageOutput.printf("\r\n\r\nMqttHandleVedirectClass::loop millis %lu _nextPublishUpdatesOnly %u _nextPublishFull %u\r\n", millis(), _nextPublishUpdatesOnly, _nextPublishFull);
|
||||
if (_PublishFull) {
|
||||
MessageOutput.println("MqttHandleVedirectClass::loop publish full");
|
||||
} else {
|
||||
MessageOutput.println("MqttHandleVedirectClass::loop publish updates only");
|
||||
}
|
||||
#endif
|
||||
|
||||
auto spMpptData = VictronMppt.getData();
|
||||
String value;
|
||||
String topic = "victron/";
|
||||
topic.concat(spMpptData->SER);
|
||||
topic.concat("/");
|
||||
|
||||
if (_PublishFull || spMpptData->PID != _kvFrame.PID)
|
||||
MqttSettings.publish(topic + "PID", spMpptData->getPidAsString().data());
|
||||
if (_PublishFull || strcmp(spMpptData->SER, _kvFrame.SER) != 0)
|
||||
MqttSettings.publish(topic + "SER", spMpptData->SER );
|
||||
if (_PublishFull || strcmp(spMpptData->FW, _kvFrame.FW) != 0)
|
||||
MqttSettings.publish(topic + "FW", spMpptData->FW);
|
||||
if (_PublishFull || spMpptData->LOAD != _kvFrame.LOAD)
|
||||
MqttSettings.publish(topic + "LOAD", spMpptData->LOAD == true ? "ON": "OFF");
|
||||
if (_PublishFull || spMpptData->CS != _kvFrame.CS)
|
||||
MqttSettings.publish(topic + "CS", spMpptData->getCsAsString().data());
|
||||
if (_PublishFull || spMpptData->ERR != _kvFrame.ERR)
|
||||
MqttSettings.publish(topic + "ERR", spMpptData->getErrAsString().data());
|
||||
if (_PublishFull || spMpptData->OR != _kvFrame.OR)
|
||||
MqttSettings.publish(topic + "OR", spMpptData->getOrAsString().data());
|
||||
if (_PublishFull || spMpptData->MPPT != _kvFrame.MPPT)
|
||||
MqttSettings.publish(topic + "MPPT", spMpptData->getMpptAsString().data());
|
||||
if (_PublishFull || spMpptData->HSDS != _kvFrame.HSDS) {
|
||||
value = spMpptData->HSDS;
|
||||
MqttSettings.publish(topic + "HSDS", value);
|
||||
}
|
||||
if (_PublishFull || spMpptData->V != _kvFrame.V) {
|
||||
value = spMpptData->V;
|
||||
MqttSettings.publish(topic + "V", value);
|
||||
}
|
||||
if (_PublishFull || spMpptData->I != _kvFrame.I) {
|
||||
value = spMpptData->I;
|
||||
MqttSettings.publish(topic + "I", value);
|
||||
}
|
||||
if (_PublishFull || spMpptData->P != _kvFrame.P) {
|
||||
value = spMpptData->P;
|
||||
MqttSettings.publish(topic + "P", value);
|
||||
}
|
||||
if (_PublishFull || spMpptData->VPV != _kvFrame.VPV) {
|
||||
value = spMpptData->VPV;
|
||||
MqttSettings.publish(topic + "VPV", value);
|
||||
}
|
||||
if (_PublishFull || spMpptData->IPV != _kvFrame.IPV) {
|
||||
value = spMpptData->IPV;
|
||||
MqttSettings.publish(topic + "IPV", value);
|
||||
}
|
||||
if (_PublishFull || spMpptData->PPV != _kvFrame.PPV) {
|
||||
value = spMpptData->PPV;
|
||||
MqttSettings.publish(topic + "PPV", value);
|
||||
}
|
||||
if (_PublishFull || spMpptData->E != _kvFrame.E) {
|
||||
value = spMpptData->E;
|
||||
MqttSettings.publish(topic + "E", value);
|
||||
}
|
||||
if (_PublishFull || spMpptData->H19 != _kvFrame.H19) {
|
||||
value = spMpptData->H19;
|
||||
MqttSettings.publish(topic + "H19", value);
|
||||
}
|
||||
if (_PublishFull || spMpptData->H20 != _kvFrame.H20) {
|
||||
value = spMpptData->H20;
|
||||
MqttSettings.publish(topic + "H20", value);
|
||||
}
|
||||
if (_PublishFull || spMpptData->H21 != _kvFrame.H21) {
|
||||
value = spMpptData->H21;
|
||||
MqttSettings.publish(topic + "H21", value);
|
||||
}
|
||||
if (_PublishFull || spMpptData->H22 != _kvFrame.H22) {
|
||||
value = spMpptData->H22;
|
||||
MqttSettings.publish(topic + "H22", value);
|
||||
}
|
||||
if (_PublishFull || spMpptData->H23 != _kvFrame.H23) {
|
||||
value = spMpptData->H23;
|
||||
MqttSettings.publish(topic + "H23", value);
|
||||
}
|
||||
if (!_PublishFull) {
|
||||
_kvFrame = *spMpptData;
|
||||
}
|
||||
|
||||
// now calculate next points of time to publish
|
||||
_nextPublishUpdatesOnly = millis() + (config.Mqtt.PublishInterval * 1000);
|
||||
|
||||
if (_PublishFull) {
|
||||
// when Home Assistant MQTT-Auto-Discovery is active,
|
||||
// and "enable expiration" is active, all values must be published at
|
||||
// least once before the announced expiry interval is reached
|
||||
if ((config.Vedirect.UpdatesOnly) && (config.Mqtt.Hass.Enabled) && (config.Mqtt.Hass.Expire)) {
|
||||
_nextPublishFull = millis() + (((config.Mqtt.PublishInterval * 3) - 1) * 1000);
|
||||
|
||||
#ifdef MQTTHANDLEVEDIRECT_DEBUG
|
||||
uint32_t _tmpNextFullSeconds = (config.Mqtt_PublishInterval * 3) - 1;
|
||||
MessageOutput.printf("MqttHandleVedirectClass::loop _tmpNextFullSeconds %u - _nextPublishFull %u \r\n", _tmpNextFullSeconds, _nextPublishFull);
|
||||
#endif
|
||||
|
||||
} else {
|
||||
// no future publish full needed
|
||||
_nextPublishFull = UINT32_MAX;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef MQTTHANDLEVEDIRECT_DEBUG
|
||||
MessageOutput.printf("MqttHandleVedirectClass::loop _nextPublishUpdatesOnly %u _nextPublishFull %u\r\n", _nextPublishUpdatesOnly, _nextPublishFull);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@ -91,8 +91,10 @@ void MqttSettingsClass::onMqttDisconnect(espMqttClientTypes::DisconnectReason re
|
||||
|
||||
void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total)
|
||||
{
|
||||
MessageOutput.print("Received MQTT message on topic: ");
|
||||
MessageOutput.println(topic);
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.print("Received MQTT message on topic: ");
|
||||
MessageOutput.println(topic);
|
||||
}
|
||||
|
||||
_mqttSubscribeParser.handle_message(properties, topic, payload, len, index, total);
|
||||
}
|
||||
@ -114,6 +116,7 @@ void MqttSettingsClass::performConnect()
|
||||
|
||||
MessageOutput.println("Connecting to MQTT...");
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
_verboseLogging = config.Mqtt.VerboseLogging;
|
||||
const String willTopic = getPrefix() + config.Mqtt.Lwt.Topic;
|
||||
const String clientId = NetworkSettings.getApName();
|
||||
if (config.Mqtt.Tls.Enabled) {
|
||||
|
||||
@ -86,6 +86,64 @@
|
||||
#define CMT_SDIO -1
|
||||
#endif
|
||||
|
||||
#ifndef VICTRON_PIN_TX
|
||||
#define VICTRON_PIN_TX -1
|
||||
#endif
|
||||
|
||||
#ifndef VICTRON_PIN_RX
|
||||
#define VICTRON_PIN_RX -1
|
||||
#endif
|
||||
|
||||
#ifndef BATTERY_PIN_RX
|
||||
#define BATTERY_PIN_RX -1
|
||||
#endif
|
||||
|
||||
#ifdef PYLONTECH_PIN_RX
|
||||
#undef BATTERY_PIN_RX
|
||||
#define BATTERY_PIN_RX PYLONTECH_PIN_RX
|
||||
#endif
|
||||
|
||||
#ifndef BATTERY_PIN_RXEN
|
||||
#define BATTERY_PIN_RXEN -1
|
||||
#endif
|
||||
|
||||
#ifndef BATTERY_PIN_TX
|
||||
#define BATTERY_PIN_TX -1
|
||||
#endif
|
||||
|
||||
#ifdef PYLONTECH_PIN_TX
|
||||
#undef BATTERY_PIN_TX
|
||||
#define BATTERY_PIN_TX PYLONTECH_PIN_TX
|
||||
#endif
|
||||
|
||||
#ifndef BATTERY_PIN_TXEN
|
||||
#define BATTERY_PIN_TXEN -1
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_MISO
|
||||
#define HUAWEI_PIN_MISO -1
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_MOSI
|
||||
#define HUAWEI_PIN_MOSI -1
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_SCLK
|
||||
#define HUAWEI_PIN_SCLK -1
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_CS
|
||||
#define HUAWEI_PIN_CS -1
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_IRQ
|
||||
#define HUAWEI_PIN_IRQ -1
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_POWER
|
||||
#define HUAWEI_PIN_POWER -1
|
||||
#endif
|
||||
|
||||
PinMappingClass PinMapping;
|
||||
|
||||
PinMappingClass::PinMappingClass()
|
||||
@ -123,7 +181,21 @@ PinMappingClass::PinMappingClass()
|
||||
_pinMapping.display_clk = DISPLAY_CLK;
|
||||
_pinMapping.display_cs = DISPLAY_CS;
|
||||
_pinMapping.display_reset = DISPLAY_RESET;
|
||||
|
||||
_pinMapping.victron_tx = VICTRON_PIN_TX;
|
||||
_pinMapping.victron_rx = VICTRON_PIN_RX;
|
||||
|
||||
_pinMapping.battery_rx = BATTERY_PIN_RX;
|
||||
_pinMapping.battery_rxen = BATTERY_PIN_RXEN;
|
||||
_pinMapping.battery_tx = BATTERY_PIN_TX;
|
||||
_pinMapping.battery_txen = BATTERY_PIN_TXEN;
|
||||
|
||||
_pinMapping.huawei_miso = HUAWEI_PIN_MISO;
|
||||
_pinMapping.huawei_mosi = HUAWEI_PIN_MOSI;
|
||||
_pinMapping.huawei_clk = HUAWEI_PIN_SCLK;
|
||||
_pinMapping.huawei_cs = HUAWEI_PIN_CS;
|
||||
_pinMapping.huawei_irq = HUAWEI_PIN_IRQ;
|
||||
_pinMapping.huawei_power = HUAWEI_PIN_POWER;
|
||||
_pinMapping.led[0] = LED0;
|
||||
_pinMapping.led[1] = LED1;
|
||||
}
|
||||
@ -185,6 +257,21 @@ bool PinMappingClass::init(const String& deviceMapping)
|
||||
_pinMapping.display_cs = doc[i]["display"]["cs"] | DISPLAY_CS;
|
||||
_pinMapping.display_reset = doc[i]["display"]["reset"] | DISPLAY_RESET;
|
||||
|
||||
_pinMapping.victron_rx = doc[i]["victron"]["rx"] | VICTRON_PIN_RX;
|
||||
_pinMapping.victron_tx = doc[i]["victron"]["tx"] | VICTRON_PIN_TX;
|
||||
|
||||
_pinMapping.battery_rx = doc[i]["battery"]["rx"] | BATTERY_PIN_RX;
|
||||
_pinMapping.battery_rxen = doc[i]["battery"]["rxen"] | BATTERY_PIN_RXEN;
|
||||
_pinMapping.battery_tx = doc[i]["battery"]["tx"] | BATTERY_PIN_TX;
|
||||
_pinMapping.battery_txen = doc[i]["battery"]["txen"] | BATTERY_PIN_TXEN;
|
||||
|
||||
_pinMapping.huawei_miso = doc[i]["huawei"]["miso"] | HUAWEI_PIN_MISO;
|
||||
_pinMapping.huawei_mosi = doc[i]["huawei"]["mosi"] | HUAWEI_PIN_MOSI;
|
||||
_pinMapping.huawei_clk = doc[i]["huawei"]["clk"] | HUAWEI_PIN_SCLK;
|
||||
_pinMapping.huawei_irq = doc[i]["huawei"]["irq"] | HUAWEI_PIN_IRQ;
|
||||
_pinMapping.huawei_cs = doc[i]["huawei"]["cs"] | HUAWEI_PIN_CS;
|
||||
_pinMapping.huawei_power = doc[i]["huawei"]["power"] | HUAWEI_PIN_POWER;
|
||||
|
||||
_pinMapping.led[0] = doc[i]["led"]["led0"] | LED0;
|
||||
_pinMapping.led[1] = doc[i]["led"]["led1"] | LED1;
|
||||
|
||||
@ -216,4 +303,14 @@ bool PinMappingClass::isValidCmt2300Config() const
|
||||
bool PinMappingClass::isValidEthConfig() const
|
||||
{
|
||||
return _pinMapping.eth_enabled;
|
||||
}
|
||||
}
|
||||
|
||||
bool PinMappingClass::isValidHuaweiConfig() const
|
||||
{
|
||||
return _pinMapping.huawei_miso >= 0
|
||||
&& _pinMapping.huawei_mosi >= 0
|
||||
&& _pinMapping.huawei_clk >= 0
|
||||
&& _pinMapping.huawei_irq >= 0
|
||||
&& _pinMapping.huawei_cs >= 0
|
||||
&& _pinMapping.huawei_power >= 0;
|
||||
}
|
||||
|
||||
719
src/PowerLimiter.cpp
Normal file
@ -0,0 +1,719 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022 Thomas Basler and others
|
||||
*/
|
||||
|
||||
#include "Battery.h"
|
||||
#include "PowerMeter.h"
|
||||
#include "PowerLimiter.h"
|
||||
#include "Configuration.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "NetworkSettings.h"
|
||||
#include "Huawei_can.h"
|
||||
#include <VictronMppt.h>
|
||||
#include "MessageOutput.h"
|
||||
#include <ctime>
|
||||
#include <cmath>
|
||||
#include <frozen/map.h>
|
||||
|
||||
PowerLimiterClass PowerLimiter;
|
||||
|
||||
void PowerLimiterClass::init(Scheduler& scheduler)
|
||||
{
|
||||
scheduler.addTask(_loopTask);
|
||||
_loopTask.setCallback(std::bind(&PowerLimiterClass::loop, this));
|
||||
_loopTask.setIterations(TASK_FOREVER);
|
||||
_loopTask.enable();
|
||||
}
|
||||
|
||||
frozen::string const& PowerLimiterClass::getStatusText(PowerLimiterClass::Status status)
|
||||
{
|
||||
static const frozen::string missing = "programmer error: missing status text";
|
||||
|
||||
static const frozen::map<Status, frozen::string, 19> texts = {
|
||||
{ Status::Initializing, "initializing (should not see me)" },
|
||||
{ Status::DisabledByConfig, "disabled by configuration" },
|
||||
{ Status::DisabledByMqtt, "disabled by MQTT" },
|
||||
{ Status::WaitingForValidTimestamp, "waiting for valid date and time to be available" },
|
||||
{ Status::PowerMeterDisabled, "no power meter is configured/enabled" },
|
||||
{ Status::PowerMeterTimeout, "power meter readings are outdated" },
|
||||
{ Status::PowerMeterPending, "waiting for sufficiently recent power meter reading" },
|
||||
{ Status::InverterInvalid, "invalid inverter selection/configuration" },
|
||||
{ Status::InverterChanged, "target inverter changed" },
|
||||
{ Status::InverterOffline, "inverter is offline (polling enabled? radio okay?)" },
|
||||
{ Status::InverterCommandsDisabled, "inverter configuration prohibits sending commands" },
|
||||
{ Status::InverterLimitPending, "waiting for a power limit command to complete" },
|
||||
{ Status::InverterPowerCmdPending, "waiting for a start/stop/restart command to complete" },
|
||||
{ Status::InverterDevInfoPending, "waiting for inverter device information to be available" },
|
||||
{ Status::InverterStatsPending, "waiting for sufficiently recent inverter data" },
|
||||
{ Status::UnconditionalSolarPassthrough, "unconditionally passing through all solar power (MQTT override)" },
|
||||
{ Status::NoVeDirect, "VE.Direct disabled, connection broken, or data outdated" },
|
||||
{ Status::Settling, "waiting for the system to settle" },
|
||||
{ Status::Stable, "the system is stable, the last power limit is still valid" },
|
||||
};
|
||||
|
||||
auto iter = texts.find(status);
|
||||
if (iter == texts.end()) { return missing; }
|
||||
|
||||
return iter->second;
|
||||
}
|
||||
|
||||
void PowerLimiterClass::announceStatus(PowerLimiterClass::Status status)
|
||||
{
|
||||
// this method is called with high frequency. print the status text if
|
||||
// the status changed since we last printed the text of another one.
|
||||
// otherwise repeat the info with a fixed interval.
|
||||
if (_lastStatus == status && millis() < _lastStatusPrinted + 10 * 1000) { return; }
|
||||
|
||||
// after announcing once that the DPL is disabled by configuration, it
|
||||
// should just be silent while it is disabled.
|
||||
if (status == Status::DisabledByConfig && _lastStatus == status) { return; }
|
||||
|
||||
MessageOutput.printf("[%11.3f] DPL: %s\r\n",
|
||||
static_cast<double>(millis())/1000, getStatusText(status).data());
|
||||
|
||||
_lastStatus = status;
|
||||
_lastStatusPrinted = millis();
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if the inverter state was changed or is about to change, i.e.,
|
||||
* if it is actually in need of a shutdown. returns false otherwise, i.e., the
|
||||
* inverter is already (assumed to be) shut down.
|
||||
*/
|
||||
bool PowerLimiterClass::shutdown(PowerLimiterClass::Status status)
|
||||
{
|
||||
announceStatus(status);
|
||||
|
||||
if (_inverter == nullptr || !_inverter->isProducing() ||
|
||||
(_shutdownTimeout > 0 && _shutdownTimeout < millis()) ) {
|
||||
// we are actually (already) done with shutting down the inverter,
|
||||
// or a shutdown attempt was initiated but it timed out.
|
||||
_inverter = nullptr;
|
||||
_shutdownTimeout = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_inverter->isReachable()) { return true; } // retry later (until timeout)
|
||||
|
||||
// retry shutdown for a maximum amount of time before giving up
|
||||
if (_shutdownTimeout == 0) { _shutdownTimeout = millis() + 10 * 1000; }
|
||||
|
||||
auto lastLimitCommandState = _inverter->SystemConfigPara()->getLastLimitCommandSuccess();
|
||||
if (CMD_PENDING == lastLimitCommandState) { return true; }
|
||||
|
||||
auto lastPowerCommandState = _inverter->PowerCommand()->getLastPowerCommandSuccess();
|
||||
if (CMD_PENDING == lastPowerCommandState) { return true; }
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
commitPowerLimit(_inverter, config.PowerLimiter.LowerPowerLimit, false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PowerLimiterClass::loop()
|
||||
{
|
||||
CONFIG_T const& config = Configuration.get();
|
||||
_verboseLogging = config.PowerLimiter.VerboseLogging;
|
||||
|
||||
// we know that the Hoymiles library refuses to send any message to any
|
||||
// inverter until the system has valid time information. until then we can
|
||||
// do nothing, not even shutdown the inverter.
|
||||
struct tm timeinfo;
|
||||
if (!getLocalTime(&timeinfo, 5)) {
|
||||
return announceStatus(Status::WaitingForValidTimestamp);
|
||||
}
|
||||
|
||||
if (_shutdownTimeout > 0) {
|
||||
// we transition from SHUTDOWN to OFF when we know the inverter was
|
||||
// shut down. until then, we retry shutting it down. in this case we
|
||||
// preserve the original status that lead to the decision to shut down.
|
||||
shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!config.PowerLimiter.Enabled) {
|
||||
shutdown(Status::DisabledByConfig);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Mode::Disabled == _mode) {
|
||||
shutdown(Status::DisabledByMqtt);
|
||||
return;
|
||||
}
|
||||
|
||||
std::shared_ptr<InverterAbstract> currentInverter =
|
||||
Hoymiles.getInverterByPos(config.PowerLimiter.InverterId);
|
||||
|
||||
// in case of (newly) broken configuration, shut down
|
||||
// the last inverter we worked with (if any)
|
||||
if (currentInverter == nullptr) {
|
||||
shutdown(Status::InverterInvalid);
|
||||
return;
|
||||
}
|
||||
|
||||
// if the DPL is supposed to manage another inverter now, we first
|
||||
// shut down the previous one, if any. then we pick up the new one.
|
||||
if (_inverter != nullptr && _inverter->serial() != currentInverter->serial()) {
|
||||
shutdown(Status::InverterChanged);
|
||||
return;
|
||||
}
|
||||
|
||||
// update our pointer as the configuration might have changed
|
||||
_inverter = currentInverter;
|
||||
|
||||
// data polling is disabled or the inverter is deemed offline
|
||||
if (!_inverter->isReachable()) {
|
||||
return announceStatus(Status::InverterOffline);
|
||||
}
|
||||
|
||||
// sending commands to the inverter is disabled
|
||||
if (!_inverter->getEnableCommands()) {
|
||||
return announceStatus(Status::InverterCommandsDisabled);
|
||||
}
|
||||
|
||||
// concerns active power commands (power limits) only (also from web app or MQTT)
|
||||
auto lastLimitCommandState = _inverter->SystemConfigPara()->getLastLimitCommandSuccess();
|
||||
if (CMD_PENDING == lastLimitCommandState) {
|
||||
return announceStatus(Status::InverterLimitPending);
|
||||
}
|
||||
|
||||
// concerns power commands (start, stop, restart) only (also from web app or MQTT)
|
||||
auto lastPowerCommandState = _inverter->PowerCommand()->getLastPowerCommandSuccess();
|
||||
if (CMD_PENDING == lastPowerCommandState) {
|
||||
return announceStatus(Status::InverterPowerCmdPending);
|
||||
}
|
||||
|
||||
// a calculated power limit will always be limited to the reported
|
||||
// device's max power. that upper limit is only known after the first
|
||||
// DevInfoSimpleCommand succeeded.
|
||||
if (_inverter->DevInfo()->getMaxPower() <= 0) {
|
||||
return announceStatus(Status::InverterDevInfoPending);
|
||||
}
|
||||
|
||||
if (Mode::UnconditionalFullSolarPassthrough == _mode) {
|
||||
// handle this mode of operation separately
|
||||
return unconditionalSolarPassthrough(_inverter);
|
||||
}
|
||||
|
||||
// the normal mode of operation requires a valid
|
||||
// power meter reading to calculate a power limit
|
||||
if (!config.PowerMeter.Enabled) {
|
||||
shutdown(Status::PowerMeterDisabled);
|
||||
return;
|
||||
}
|
||||
|
||||
if (millis() - PowerMeter.getLastPowerMeterUpdate() > (30 * 1000)) {
|
||||
shutdown(Status::PowerMeterTimeout);
|
||||
return;
|
||||
}
|
||||
|
||||
// concerns both power limits and start/stop/restart commands and is
|
||||
// only updated if a respective response was received from the inverter
|
||||
auto lastUpdateCmd = std::max(
|
||||
_inverter->SystemConfigPara()->getLastUpdateCommand(),
|
||||
_inverter->PowerCommand()->getLastUpdateCommand());
|
||||
|
||||
// wait for power meter and inverter stat updates after a settling phase
|
||||
auto settlingEnd = lastUpdateCmd + 3 * 1000;
|
||||
|
||||
if (millis() < settlingEnd) { return announceStatus(Status::Settling); }
|
||||
|
||||
if (_inverter->Statistics()->getLastUpdate() <= settlingEnd) {
|
||||
return announceStatus(Status::InverterStatsPending);
|
||||
}
|
||||
|
||||
if (PowerMeter.getLastPowerMeterUpdate() <= settlingEnd) {
|
||||
return announceStatus(Status::PowerMeterPending);
|
||||
}
|
||||
|
||||
// since _lastCalculation and _calculationBackoffMs are initialized to
|
||||
// zero, this test is passed the first time the condition is checked.
|
||||
if (millis() < (_lastCalculation + _calculationBackoffMs)) {
|
||||
return announceStatus(Status::Stable);
|
||||
}
|
||||
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.println("[DPL::loop] ******************* ENTER **********************");
|
||||
}
|
||||
|
||||
// Check if next inverter restart time is reached
|
||||
if ((_nextInverterRestart > 1) && (_nextInverterRestart <= millis())) {
|
||||
MessageOutput.println("[DPL::loop] send inverter restart");
|
||||
_inverter->sendRestartControlRequest();
|
||||
calcNextInverterRestart();
|
||||
}
|
||||
|
||||
// Check if NTP time is set and next inverter restart not calculated yet
|
||||
if ((config.PowerLimiter.RestartHour >= 0) && (_nextInverterRestart == 0) ) {
|
||||
// check every 5 seconds
|
||||
if (_nextCalculateCheck < millis()) {
|
||||
struct tm timeinfo;
|
||||
if (getLocalTime(&timeinfo, 5)) {
|
||||
calcNextInverterRestart();
|
||||
} else {
|
||||
MessageOutput.println("[DPL::loop] inverter restart calculation: NTP not ready");
|
||||
_nextCalculateCheck += 5000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Battery charging cycle conditions
|
||||
// First we always disable discharge if the battery is empty
|
||||
if (isStopThresholdReached()) {
|
||||
// Disable battery discharge when empty
|
||||
_batteryDischargeEnabled = false;
|
||||
} else {
|
||||
// UI: Solar Passthrough Enabled -> false
|
||||
// Battery discharge can be enabled when start threshold is reached
|
||||
if (!config.PowerLimiter.SolarPassThroughEnabled && isStartThresholdReached()) {
|
||||
_batteryDischargeEnabled = true;
|
||||
}
|
||||
|
||||
// UI: Solar Passthrough Enabled -> true && EMPTY_AT_NIGHT
|
||||
if (config.PowerLimiter.SolarPassThroughEnabled && config.PowerLimiter.BatteryDrainStategy == EMPTY_AT_NIGHT) {
|
||||
if(isStartThresholdReached()) {
|
||||
// In this case we should only discharge the battery as long it is above startThreshold
|
||||
_batteryDischargeEnabled = true;
|
||||
}
|
||||
else {
|
||||
// In this case we should only discharge the battery when there is no sunshine
|
||||
_batteryDischargeEnabled = !canUseDirectSolarPower();
|
||||
}
|
||||
}
|
||||
|
||||
// UI: Solar Passthrough Enabled -> true && EMPTY_WHEN_FULL
|
||||
// Battery discharge can be enabled when start threshold is reached
|
||||
if (config.PowerLimiter.SolarPassThroughEnabled && isStartThresholdReached() && config.PowerLimiter.BatteryDrainStategy == EMPTY_WHEN_FULL) {
|
||||
_batteryDischargeEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[DPL::loop] battery interface %s, SoC: %d %%, StartTH: %d %%, StopTH: %d %%, SoC age: %d s\r\n",
|
||||
(config.Battery.Enabled?"enabled":"disabled"),
|
||||
Battery.getStats()->getSoC(),
|
||||
config.PowerLimiter.BatterySocStartThreshold,
|
||||
config.PowerLimiter.BatterySocStopThreshold,
|
||||
Battery.getStats()->getSoCAgeSeconds());
|
||||
|
||||
float dcVoltage = _inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t)config.PowerLimiter.InverterChannelId, FLD_UDC);
|
||||
MessageOutput.printf("[DPL::loop] dcVoltage: %.2f V, loadCorrectedVoltage: %.2f V, StartTH: %.2f V, StopTH: %.2f V\r\n",
|
||||
dcVoltage, getLoadCorrectedVoltage(),
|
||||
config.PowerLimiter.VoltageStartThreshold,
|
||||
config.PowerLimiter.VoltageStopThreshold);
|
||||
|
||||
MessageOutput.printf("[DPL::loop] StartTH reached: %s, StopTH reached: %s, inverter %s producing\r\n",
|
||||
(isStartThresholdReached()?"yes":"no"),
|
||||
(isStopThresholdReached()?"yes":"no"),
|
||||
(_inverter->isProducing()?"is":"is NOT"));
|
||||
|
||||
MessageOutput.printf("[DPL::loop] SolarPT %s, Drain Strategy: %i, canUseDirectSolarPower: %s\r\n",
|
||||
(config.PowerLimiter.SolarPassThroughEnabled?"enabled":"disabled"),
|
||||
config.PowerLimiter.BatteryDrainStategy, (canUseDirectSolarPower()?"yes":"no"));
|
||||
|
||||
MessageOutput.printf("[DPL::loop] battery discharging %s, PowerMeter: %d W, target consumption: %d W\r\n",
|
||||
(_batteryDischargeEnabled?"allowed":"prevented"),
|
||||
static_cast<int32_t>(round(PowerMeter.getPowerTotal())),
|
||||
config.PowerLimiter.TargetPowerConsumption);
|
||||
}
|
||||
|
||||
// Calculate and set Power Limit (NOTE: might reset _inverter to nullptr!)
|
||||
int32_t newPowerLimit = calcPowerLimit(_inverter, canUseDirectSolarPower(), _batteryDischargeEnabled);
|
||||
bool limitUpdated = setNewPowerLimit(_inverter, newPowerLimit);
|
||||
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[DPL::loop] ******************* Leaving PL, calculated limit: %d W, requested limit: %d W (%s)\r\n",
|
||||
newPowerLimit, _lastRequestedPowerLimit,
|
||||
(limitUpdated?"updated from calculated":"kept last requested"));
|
||||
}
|
||||
|
||||
_lastCalculation = millis();
|
||||
|
||||
if (!limitUpdated) {
|
||||
// increase polling backoff if system seems to be stable
|
||||
_calculationBackoffMs = std::min<uint32_t>(1024, _calculationBackoffMs * 2);
|
||||
return announceStatus(Status::Stable);
|
||||
}
|
||||
|
||||
_calculationBackoffMs = _calculationBackoffMsDefault;
|
||||
}
|
||||
|
||||
/**
|
||||
* calculate the AC output power (limit) to set, such that the inverter uses
|
||||
* the given power on its DC side, i.e., adjust the power for the inverter's
|
||||
* efficiency.
|
||||
*/
|
||||
int32_t PowerLimiterClass::inverterPowerDcToAc(std::shared_ptr<InverterAbstract> inverter, int32_t dcPower)
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
float inverterEfficiencyPercent = inverter->Statistics()->getChannelFieldValue(
|
||||
TYPE_AC, CH0, FLD_EFF);
|
||||
|
||||
// fall back to hoymiles peak efficiency as per datasheet if inverter
|
||||
// is currently not producing (efficiency is zero in that case)
|
||||
float inverterEfficiencyFactor = (inverterEfficiencyPercent > 0) ? inverterEfficiencyPercent/100 : 0.967;
|
||||
|
||||
// account for losses between solar charger and inverter (cables, junctions...)
|
||||
float lossesFactor = 1.00 - static_cast<float>(config.PowerLimiter.SolarPassThroughLosses)/100;
|
||||
|
||||
return dcPower * inverterEfficiencyFactor * lossesFactor;
|
||||
}
|
||||
|
||||
/**
|
||||
* implements the "unconditional solar passthrough" mode of operation, which
|
||||
* can currently only be set using MQTT. in this mode of operation, the
|
||||
* inverter shall behave as if it was connected to the solar panels directly,
|
||||
* i.e., all solar power (and only solar power) is fed to the AC side,
|
||||
* independent from the power meter reading.
|
||||
*/
|
||||
void PowerLimiterClass::unconditionalSolarPassthrough(std::shared_ptr<InverterAbstract> inverter)
|
||||
{
|
||||
if (!VictronMppt.isDataValid()) {
|
||||
shutdown(Status::NoVeDirect);
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t solarPower = VictronMppt.getPowerOutputWatts();
|
||||
setNewPowerLimit(inverter, inverterPowerDcToAc(inverter, solarPower));
|
||||
announceStatus(Status::UnconditionalSolarPassthrough);
|
||||
}
|
||||
|
||||
uint8_t PowerLimiterClass::getPowerLimiterState() {
|
||||
if (_inverter == nullptr || !_inverter->isReachable()) {
|
||||
return PL_UI_STATE_INACTIVE;
|
||||
}
|
||||
|
||||
if (_inverter->isProducing() && _batteryDischargeEnabled) {
|
||||
return PL_UI_STATE_USE_SOLAR_AND_BATTERY;
|
||||
}
|
||||
|
||||
if (_inverter->isProducing() && !_batteryDischargeEnabled) {
|
||||
return PL_UI_STATE_USE_SOLAR_ONLY;
|
||||
}
|
||||
|
||||
if(!_inverter->isProducing()) {
|
||||
return PL_UI_STATE_CHARGING;
|
||||
}
|
||||
|
||||
return PL_UI_STATE_INACTIVE;
|
||||
}
|
||||
|
||||
int32_t PowerLimiterClass::getLastRequestedPowerLimit() {
|
||||
return _lastRequestedPowerLimit;
|
||||
}
|
||||
|
||||
bool PowerLimiterClass::canUseDirectSolarPower()
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
if (!config.PowerLimiter.SolarPassThroughEnabled
|
||||
|| isBelowStopThreshold()
|
||||
|| !VictronMppt.isDataValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return VictronMppt.getPowerOutputWatts() >= 20; // enough power?
|
||||
}
|
||||
|
||||
|
||||
// Logic table
|
||||
// | Case # | batteryDischargeEnabled | solarPowerEnabled | useFullSolarPassthrough | Result |
|
||||
// | 1 | false | false | doesn't matter | PL = 0 |
|
||||
// | 2 | false | true | doesn't matter | PL = Victron Power |
|
||||
// | 3 | true | doesn't matter | false | PL = PowerMeter value (Battery can supply unlimited energy) |
|
||||
// | 4 | true | false | true | PL = PowerMeter value |
|
||||
// | 5 | true | true | true | PL = max(PowerMeter value, Victron Power) |
|
||||
|
||||
int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr<InverterAbstract> inverter, bool solarPowerEnabled, bool batteryDischargeEnabled)
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
int32_t acPower = 0;
|
||||
int32_t newPowerLimit = round(PowerMeter.getPowerTotal());
|
||||
|
||||
if (!solarPowerEnabled && !batteryDischargeEnabled) {
|
||||
// Case 1 - No energy sources available
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (config.PowerLimiter.IsInverterBehindPowerMeter) {
|
||||
// If the inverter the behind the power meter (part of measurement),
|
||||
// the produced power of this inverter has also to be taken into account.
|
||||
// We don't use FLD_PAC from the statistics, because that
|
||||
// data might be too old and unreliable.
|
||||
acPower = static_cast<int>(inverter->Statistics()->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC));
|
||||
newPowerLimit += acPower;
|
||||
}
|
||||
|
||||
// We're not trying to hit 0 exactly but take an offset into account
|
||||
// This means we never fully compensate the used power with the inverter
|
||||
// Case 3
|
||||
newPowerLimit -= config.PowerLimiter.TargetPowerConsumption;
|
||||
|
||||
// At this point we've calculated the required energy to compensate for household consumption.
|
||||
// If the battery is enabled this can always be supplied since we assume that the battery can supply unlimited power
|
||||
// The next step is to determine if the Solar power as provided by the Victron charger
|
||||
// actually constrains or dictates another inverter power value
|
||||
int32_t adjustedVictronChargePower = inverterPowerDcToAc(inverter, getSolarChargePower());
|
||||
|
||||
// Battery can be discharged and we should output max (Victron solar power || power meter value)
|
||||
if(batteryDischargeEnabled && useFullSolarPassthrough()) {
|
||||
// Case 5
|
||||
newPowerLimit = newPowerLimit > adjustedVictronChargePower ? newPowerLimit : adjustedVictronChargePower;
|
||||
} else {
|
||||
// We check if the PSU is on and disable the Power Limiter in this case.
|
||||
// The PSU should reduce power or shut down first before the Power Limiter kicks in
|
||||
// The only case where this is not desired is if the battery is over the Full Solar Passthrough Threshold
|
||||
// In this case the Power Limiter should start. The PSU will shutdown when the Power Limiter is active
|
||||
if (HuaweiCan.getAutoPowerStatus()) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// We should use Victron solar power only (corrected by efficiency factor)
|
||||
if (solarPowerEnabled && !batteryDischargeEnabled) {
|
||||
// Case 2 - Limit power to solar power only
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[DPL::loop] Consuming Solar Power Only -> adjustedVictronChargePower: %d W, newPowerLimit: %d W\r\n",
|
||||
adjustedVictronChargePower, newPowerLimit);
|
||||
}
|
||||
|
||||
newPowerLimit = std::min(newPowerLimit, adjustedVictronChargePower);
|
||||
}
|
||||
|
||||
return newPowerLimit;
|
||||
}
|
||||
|
||||
void PowerLimiterClass::commitPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t limit, bool enablePowerProduction)
|
||||
{
|
||||
// disable power production as soon as possible.
|
||||
// setting the power limit is less important.
|
||||
if (!enablePowerProduction && inverter->isProducing()) {
|
||||
MessageOutput.println("[DPL::commitPowerLimit] Stopping inverter...");
|
||||
inverter->sendPowerControlRequest(false);
|
||||
}
|
||||
|
||||
inverter->sendActivePowerControlRequest(static_cast<float>(limit),
|
||||
PowerLimitControlType::AbsolutNonPersistent);
|
||||
|
||||
_lastRequestedPowerLimit = limit;
|
||||
_lastPowerLimitMillis = millis();
|
||||
|
||||
// enable power production only after setting the desired limit,
|
||||
// such that an older, greater limit will not cause power spikes.
|
||||
if (enablePowerProduction && !inverter->isProducing()) {
|
||||
MessageOutput.println("[DPL::commitPowerLimit] Starting up inverter...");
|
||||
inverter->sendPowerControlRequest(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* enforces limits and a hystersis on the requested power limit, after scaling
|
||||
* the power limit to the ratio of total and producing inverter channels.
|
||||
* commits the sanitized power limit. returns true if a limit update was
|
||||
* committed, false otherwise.
|
||||
*/
|
||||
bool PowerLimiterClass::setNewPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t newPowerLimit)
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
// Stop the inverter if limit is below threshold.
|
||||
if (newPowerLimit < config.PowerLimiter.LowerPowerLimit) {
|
||||
// the status must not change outside of loop(). this condition is
|
||||
// communicated through log messages already.
|
||||
return shutdown();
|
||||
}
|
||||
|
||||
// enforce configured upper power limit
|
||||
int32_t effPowerLimit = std::min(newPowerLimit, config.PowerLimiter.UpperPowerLimit);
|
||||
|
||||
// scale the power limit by the amount of all inverter channels devided by
|
||||
// the amount of producing inverter channels. the inverters limit each of
|
||||
// the n channels to 1/n of the total power limit. scaling the power limit
|
||||
// ensures the total inverter output is what we are asking for.
|
||||
std::list<ChannelNum_t> dcChnls = inverter->Statistics()->getChannelsByType(TYPE_DC);
|
||||
int dcProdChnls = 0, dcTotalChnls = dcChnls.size();
|
||||
for (auto& c : dcChnls) {
|
||||
if (inverter->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC) > 2.0) {
|
||||
dcProdChnls++;
|
||||
}
|
||||
}
|
||||
if ((dcProdChnls > 0) && (dcProdChnls != dcTotalChnls)) {
|
||||
MessageOutput.printf("[DPL::setNewPowerLimit] %d channels total, %d producing channels, scaling power limit\r\n",
|
||||
dcTotalChnls, dcProdChnls);
|
||||
effPowerLimit = round(effPowerLimit * static_cast<float>(dcTotalChnls) / dcProdChnls);
|
||||
}
|
||||
|
||||
effPowerLimit = std::min<int32_t>(effPowerLimit, inverter->DevInfo()->getMaxPower());
|
||||
|
||||
// Check if the new value is within the limits of the hysteresis
|
||||
auto diff = std::abs(effPowerLimit - _lastRequestedPowerLimit);
|
||||
auto hysteresis = config.PowerLimiter.TargetPowerConsumptionHysteresis;
|
||||
|
||||
// (re-)send power limit in case the last was sent a long time ago. avoids
|
||||
// staleness in case a power limit update was not received by the inverter.
|
||||
auto ageMillis = millis() - _lastPowerLimitMillis;
|
||||
|
||||
if (diff < hysteresis && ageMillis < 60 * 1000) {
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[DPL::setNewPowerLimit] requested: %d W, last limit: %d W, diff: %d W, hysteresis: %d W, age: %ld ms\r\n",
|
||||
newPowerLimit, _lastRequestedPowerLimit, diff, hysteresis, ageMillis);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[DPL::setNewPowerLimit] requested: %d W, (re-)sending limit: %d W\r\n",
|
||||
newPowerLimit, effPowerLimit);
|
||||
}
|
||||
|
||||
commitPowerLimit(inverter, effPowerLimit, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
int32_t PowerLimiterClass::getSolarChargePower()
|
||||
{
|
||||
if (!canUseDirectSolarPower()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return VictronMppt.getPowerOutputWatts();
|
||||
}
|
||||
|
||||
float PowerLimiterClass::getLoadCorrectedVoltage()
|
||||
{
|
||||
if (!_inverter) {
|
||||
// there should be no need to call this method if no target inverter is known
|
||||
MessageOutput.println("DPL getLoadCorrectedVoltage: no inverter (programmer error)");
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
auto channel = static_cast<ChannelNum_t>(config.PowerLimiter.InverterChannelId);
|
||||
float acPower = _inverter->Statistics()->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC);
|
||||
float dcVoltage = _inverter->Statistics()->getChannelFieldValue(TYPE_DC, channel, FLD_UDC);
|
||||
|
||||
if (dcVoltage <= 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return dcVoltage + (acPower * config.PowerLimiter.VoltageLoadCorrectionFactor);
|
||||
}
|
||||
|
||||
bool PowerLimiterClass::testThreshold(float socThreshold, float voltThreshold,
|
||||
std::function<bool(float, float)> compare)
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
// prefer SoC provided through battery interface
|
||||
if (config.Battery.Enabled && socThreshold > 0.0
|
||||
&& Battery.getStats()->isValid()
|
||||
&& Battery.getStats()->getSoCAgeSeconds() < 60) {
|
||||
return compare(Battery.getStats()->getSoC(), socThreshold);
|
||||
}
|
||||
|
||||
// use voltage threshold as fallback
|
||||
if (voltThreshold <= 0.0) { return false; }
|
||||
|
||||
return compare(getLoadCorrectedVoltage(), voltThreshold);
|
||||
}
|
||||
|
||||
bool PowerLimiterClass::isStartThresholdReached()
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
return testThreshold(
|
||||
config.PowerLimiter.BatterySocStartThreshold,
|
||||
config.PowerLimiter.VoltageStartThreshold,
|
||||
[](float a, float b) -> bool { return a >= b; }
|
||||
);
|
||||
}
|
||||
|
||||
bool PowerLimiterClass::isStopThresholdReached()
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
return testThreshold(
|
||||
config.PowerLimiter.BatterySocStopThreshold,
|
||||
config.PowerLimiter.VoltageStopThreshold,
|
||||
[](float a, float b) -> bool { return a <= b; }
|
||||
);
|
||||
}
|
||||
|
||||
bool PowerLimiterClass::isBelowStopThreshold()
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
return testThreshold(
|
||||
config.PowerLimiter.BatterySocStopThreshold,
|
||||
config.PowerLimiter.VoltageStopThreshold,
|
||||
[](float a, float b) -> bool { return a < b; }
|
||||
);
|
||||
}
|
||||
|
||||
/// @brief calculate next inverter restart in millis
|
||||
void PowerLimiterClass::calcNextInverterRestart()
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
// first check if restart is configured at all
|
||||
if (config.PowerLimiter.RestartHour < 0) {
|
||||
_nextInverterRestart = 1;
|
||||
MessageOutput.println("[DPL::calcNextInverterRestart] _nextInverterRestart disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
// read time from timeserver, if time is not synced then return
|
||||
struct tm timeinfo;
|
||||
if (getLocalTime(&timeinfo, 5)) {
|
||||
// calculation first step is offset to next restart in minutes
|
||||
uint16_t dayMinutes = timeinfo.tm_hour * 60 + timeinfo.tm_min;
|
||||
uint16_t targetMinutes = config.PowerLimiter.RestartHour * 60;
|
||||
if (config.PowerLimiter.RestartHour > timeinfo.tm_hour) {
|
||||
// next restart is on the same day
|
||||
_nextInverterRestart = targetMinutes - dayMinutes;
|
||||
} else {
|
||||
// next restart is on next day
|
||||
_nextInverterRestart = 1440 - dayMinutes + targetMinutes;
|
||||
}
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[DPL::calcNextInverterRestart] Localtime read %d %d / configured RestartHour %d\r\n", timeinfo.tm_hour, timeinfo.tm_min, config.PowerLimiter.RestartHour);
|
||||
MessageOutput.printf("[DPL::calcNextInverterRestart] dayMinutes %d / targetMinutes %d\r\n", dayMinutes, targetMinutes);
|
||||
MessageOutput.printf("[DPL::calcNextInverterRestart] next inverter restart in %d minutes\r\n", _nextInverterRestart);
|
||||
}
|
||||
// then convert unit for next restart to milliseconds and add current uptime millis()
|
||||
_nextInverterRestart *= 60000;
|
||||
_nextInverterRestart += millis();
|
||||
} else {
|
||||
MessageOutput.println("[DPL::calcNextInverterRestart] getLocalTime not successful, no calculation");
|
||||
_nextInverterRestart = 0;
|
||||
}
|
||||
MessageOutput.printf("[DPL::calcNextInverterRestart] _nextInverterRestart @ %d millis\r\n", _nextInverterRestart);
|
||||
}
|
||||
|
||||
bool PowerLimiterClass::useFullSolarPassthrough()
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
// We only do full solar PT if general solar PT is enabled
|
||||
if(!config.PowerLimiter.SolarPassThroughEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (testThreshold(config.PowerLimiter.FullSolarPassThroughSoc,
|
||||
config.PowerLimiter.FullSolarPassThroughStartVoltage,
|
||||
[](float a, float b) -> bool { return a >= b; })) {
|
||||
_fullSolarPassThroughEnabled = true;
|
||||
}
|
||||
|
||||
if (testThreshold(config.PowerLimiter.FullSolarPassThroughSoc,
|
||||
config.PowerLimiter.FullSolarPassThroughStopVoltage,
|
||||
[](float a, float b) -> bool { return a < b; })) {
|
||||
_fullSolarPassThroughEnabled = false;
|
||||
}
|
||||
|
||||
return _fullSolarPassThroughEnabled;
|
||||
}
|
||||
222
src/PowerMeter.cpp
Normal file
@ -0,0 +1,222 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022 Thomas Basler and others
|
||||
*/
|
||||
#include "PowerMeter.h"
|
||||
#include "Configuration.h"
|
||||
#include "HttpPowerMeter.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "NetworkSettings.h"
|
||||
#include "SDM.h"
|
||||
#include "MessageOutput.h"
|
||||
#include <ctime>
|
||||
#include <SoftwareSerial.h>
|
||||
|
||||
PowerMeterClass PowerMeter;
|
||||
|
||||
SDM sdm(Serial2, 9600, NOT_A_PIN, SERIAL_8N1, SDM_RX_PIN, SDM_TX_PIN);
|
||||
|
||||
SoftwareSerial inputSerial;
|
||||
|
||||
void PowerMeterClass::init(Scheduler& scheduler)
|
||||
{
|
||||
scheduler.addTask(_loopTask);
|
||||
_loopTask.setCallback(std::bind(&PowerMeterClass::loop, this));
|
||||
_loopTask.setIterations(TASK_FOREVER);
|
||||
_loopTask.enable();
|
||||
|
||||
_lastPowerMeterCheck = 0;
|
||||
_lastPowerMeterUpdate = 0;
|
||||
|
||||
for (auto const& s: _mqttSubscriptions) { MqttSettings.unsubscribe(s.first); }
|
||||
_mqttSubscriptions.clear();
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
if (!config.PowerMeter.Enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch(config.PowerMeter.Source) {
|
||||
case SOURCE_MQTT: {
|
||||
auto subscribe = [this](char const* topic, float* target) {
|
||||
if (strlen(topic) == 0) { return; }
|
||||
MqttSettings.subscribe(topic, 0,
|
||||
std::bind(&PowerMeterClass::onMqttMessage,
|
||||
this, std::placeholders::_1, std::placeholders::_2,
|
||||
std::placeholders::_3, std::placeholders::_4,
|
||||
std::placeholders::_5, std::placeholders::_6)
|
||||
);
|
||||
_mqttSubscriptions.try_emplace(topic, target);
|
||||
};
|
||||
|
||||
subscribe(config.PowerMeter.MqttTopicPowerMeter1, &_powerMeter1Power);
|
||||
subscribe(config.PowerMeter.MqttTopicPowerMeter2, &_powerMeter2Power);
|
||||
subscribe(config.PowerMeter.MqttTopicPowerMeter3, &_powerMeter3Power);
|
||||
break;
|
||||
}
|
||||
|
||||
case SOURCE_SDM1PH:
|
||||
case SOURCE_SDM3PH:
|
||||
sdm.begin();
|
||||
break;
|
||||
|
||||
case SOURCE_HTTP:
|
||||
HttpPowerMeter.init();
|
||||
break;
|
||||
|
||||
case SOURCE_SML:
|
||||
pinMode(SML_RX_PIN, INPUT);
|
||||
inputSerial.begin(9600, SWSERIAL_8N1, SML_RX_PIN, -1, false, 128, 95);
|
||||
inputSerial.enableRx(true);
|
||||
inputSerial.enableTx(false);
|
||||
inputSerial.flush();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PowerMeterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
|
||||
{
|
||||
for (auto const& subscription: _mqttSubscriptions) {
|
||||
if (subscription.first != topic) { continue; }
|
||||
|
||||
std::string value(reinterpret_cast<const char*>(payload), len);
|
||||
try {
|
||||
*subscription.second = std::stof(value);
|
||||
}
|
||||
catch(std::invalid_argument const& e) {
|
||||
MessageOutput.printf("PowerMeterClass: cannot parse payload of topic '%s' as float: %s\r\n",
|
||||
topic, value.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("PowerMeterClass: Updated from '%s', TotalPower: %5.2f\r\n",
|
||||
topic, getPowerTotal());
|
||||
}
|
||||
|
||||
_lastPowerMeterUpdate = millis();
|
||||
}
|
||||
}
|
||||
|
||||
float PowerMeterClass::getPowerTotal(bool forceUpdate)
|
||||
{
|
||||
if (forceUpdate) {
|
||||
CONFIG_T& config = Configuration.get();
|
||||
if (config.PowerMeter.Enabled
|
||||
&& (millis() - _lastPowerMeterUpdate) > (1000)) {
|
||||
readPowerMeter();
|
||||
}
|
||||
}
|
||||
return _powerMeter1Power + _powerMeter2Power + _powerMeter3Power;
|
||||
}
|
||||
|
||||
uint32_t PowerMeterClass::getLastPowerMeterUpdate()
|
||||
{
|
||||
return _lastPowerMeterUpdate;
|
||||
}
|
||||
|
||||
void PowerMeterClass::mqtt()
|
||||
{
|
||||
if (!MqttSettings.getConnected()) {
|
||||
return;
|
||||
} else {
|
||||
String topic = "powermeter";
|
||||
MqttSettings.publish(topic + "/power1", String(_powerMeter1Power));
|
||||
MqttSettings.publish(topic + "/power2", String(_powerMeter2Power));
|
||||
MqttSettings.publish(topic + "/power3", String(_powerMeter3Power));
|
||||
MqttSettings.publish(topic + "/powertotal", String(getPowerTotal()));
|
||||
MqttSettings.publish(topic + "/voltage1", String(_powerMeter1Voltage));
|
||||
MqttSettings.publish(topic + "/voltage2", String(_powerMeter2Voltage));
|
||||
MqttSettings.publish(topic + "/voltage3", String(_powerMeter3Voltage));
|
||||
MqttSettings.publish(topic + "/import", String(_powerMeterImport));
|
||||
MqttSettings.publish(topic + "/export", String(_powerMeterExport));
|
||||
}
|
||||
}
|
||||
|
||||
void PowerMeterClass::loop()
|
||||
{
|
||||
CONFIG_T const& config = Configuration.get();
|
||||
_verboseLogging = config.PowerMeter.VerboseLogging;
|
||||
|
||||
if (!config.PowerMeter.Enabled) { return; }
|
||||
|
||||
if (config.PowerMeter.Source == SOURCE_SML) {
|
||||
if (!smlReadLoop()) {
|
||||
return;
|
||||
} else {
|
||||
_lastPowerMeterUpdate = millis();
|
||||
}
|
||||
}
|
||||
|
||||
if ((millis() - _lastPowerMeterCheck) < (config.PowerMeter.Interval * 1000)) {
|
||||
return;
|
||||
}
|
||||
|
||||
readPowerMeter();
|
||||
|
||||
MessageOutput.printf("PowerMeterClass: TotalPower: %5.2f\r\n", getPowerTotal());
|
||||
|
||||
mqtt();
|
||||
|
||||
_lastPowerMeterCheck = millis();
|
||||
}
|
||||
|
||||
void PowerMeterClass::readPowerMeter()
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
uint8_t _address = config.PowerMeter.SdmAddress;
|
||||
|
||||
if (config.PowerMeter.Source == SOURCE_SDM1PH) {
|
||||
_powerMeter1Power = static_cast<float>(sdm.readVal(SDM_PHASE_1_POWER, _address));
|
||||
_powerMeter2Power = 0.0;
|
||||
_powerMeter3Power = 0.0;
|
||||
_powerMeter1Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_1_VOLTAGE, _address));
|
||||
_powerMeter2Voltage = 0.0;
|
||||
_powerMeter3Voltage = 0.0;
|
||||
_powerMeterImport = static_cast<float>(sdm.readVal(SDM_IMPORT_ACTIVE_ENERGY, _address));
|
||||
_powerMeterExport = static_cast<float>(sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address));
|
||||
_lastPowerMeterUpdate = millis();
|
||||
}
|
||||
else if (config.PowerMeter.Source == SOURCE_SDM3PH) {
|
||||
_powerMeter1Power = static_cast<float>(sdm.readVal(SDM_PHASE_1_POWER, _address));
|
||||
_powerMeter2Power = static_cast<float>(sdm.readVal(SDM_PHASE_2_POWER, _address));
|
||||
_powerMeter3Power = static_cast<float>(sdm.readVal(SDM_PHASE_3_POWER, _address));
|
||||
_powerMeter1Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_1_VOLTAGE, _address));
|
||||
_powerMeter2Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_2_VOLTAGE, _address));
|
||||
_powerMeter3Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_3_VOLTAGE, _address));
|
||||
_powerMeterImport = static_cast<float>(sdm.readVal(SDM_IMPORT_ACTIVE_ENERGY, _address));
|
||||
_powerMeterExport = static_cast<float>(sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address));
|
||||
_lastPowerMeterUpdate = millis();
|
||||
}
|
||||
else if (config.PowerMeter.Source == SOURCE_HTTP) {
|
||||
if (HttpPowerMeter.updateValues()) {
|
||||
_powerMeter1Power = HttpPowerMeter.getPower(1);
|
||||
_powerMeter2Power = HttpPowerMeter.getPower(2);
|
||||
_powerMeter3Power = HttpPowerMeter.getPower(3);
|
||||
_lastPowerMeterUpdate = millis();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool PowerMeterClass::smlReadLoop()
|
||||
{
|
||||
while (inputSerial.available()) {
|
||||
double readVal = 0;
|
||||
unsigned char smlCurrentChar = inputSerial.read();
|
||||
sml_states_t smlCurrentState = smlState(smlCurrentChar);
|
||||
if (smlCurrentState == SML_LISTEND) {
|
||||
for (auto& handler: smlHandlerList) {
|
||||
if (smlOBISCheck(handler.OBIS)) {
|
||||
handler.Fn(readVal);
|
||||
*handler.Arg = readVal;
|
||||
}
|
||||
}
|
||||
} else if (smlCurrentState == SML_FINAL) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
344
src/PylontechCanReceiver.cpp
Normal file
@ -0,0 +1,344 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include "PylontechCanReceiver.h"
|
||||
#include "Configuration.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "PinMapping.h"
|
||||
#include <driver/twai.h>
|
||||
#include <ctime>
|
||||
|
||||
//#define PYLONTECH_DUMMY
|
||||
|
||||
bool PylontechCanReceiver::init(bool verboseLogging)
|
||||
{
|
||||
_verboseLogging = verboseLogging;
|
||||
|
||||
MessageOutput.println("[Pylontech] Initialize interface...");
|
||||
|
||||
const PinMapping_t& pin = PinMapping.get();
|
||||
MessageOutput.printf("[Pylontech] Interface rx = %d, tx = %d\r\n",
|
||||
pin.battery_rx, pin.battery_tx);
|
||||
|
||||
if (pin.battery_rx < 0 || pin.battery_tx < 0) {
|
||||
MessageOutput.println("[Pylontech] Invalid pin config");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto tx = static_cast<gpio_num_t>(pin.battery_tx);
|
||||
auto rx = static_cast<gpio_num_t>(pin.battery_rx);
|
||||
twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(tx, rx, TWAI_MODE_NORMAL);
|
||||
|
||||
// Initialize configuration structures using macro initializers
|
||||
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS();
|
||||
twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();
|
||||
|
||||
// Install TWAI driver
|
||||
esp_err_t twaiLastResult = twai_driver_install(&g_config, &t_config, &f_config);
|
||||
switch (twaiLastResult) {
|
||||
case ESP_OK:
|
||||
MessageOutput.println("[Pylontech] Twai driver installed");
|
||||
break;
|
||||
case ESP_ERR_INVALID_ARG:
|
||||
MessageOutput.println("[Pylontech] Twai driver install - invalid arg");
|
||||
return false;
|
||||
break;
|
||||
case ESP_ERR_NO_MEM:
|
||||
MessageOutput.println("[Pylontech] Twai driver install - no memory");
|
||||
return false;
|
||||
break;
|
||||
case ESP_ERR_INVALID_STATE:
|
||||
MessageOutput.println("[Pylontech] Twai driver install - invalid state");
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Start TWAI driver
|
||||
twaiLastResult = twai_start();
|
||||
switch (twaiLastResult) {
|
||||
case ESP_OK:
|
||||
MessageOutput.println("[Pylontech] Twai driver started");
|
||||
break;
|
||||
case ESP_ERR_INVALID_STATE:
|
||||
MessageOutput.println("[Pylontech] Twai driver start - invalid state");
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PylontechCanReceiver::deinit()
|
||||
{
|
||||
// Stop TWAI driver
|
||||
esp_err_t twaiLastResult = twai_stop();
|
||||
switch (twaiLastResult) {
|
||||
case ESP_OK:
|
||||
MessageOutput.println("[Pylontech] Twai driver stopped");
|
||||
break;
|
||||
case ESP_ERR_INVALID_STATE:
|
||||
MessageOutput.println("[Pylontech] Twai driver stop - invalid state");
|
||||
break;
|
||||
}
|
||||
|
||||
// Uninstall TWAI driver
|
||||
twaiLastResult = twai_driver_uninstall();
|
||||
switch (twaiLastResult) {
|
||||
case ESP_OK:
|
||||
MessageOutput.println("[Pylontech] Twai driver uninstalled");
|
||||
break;
|
||||
case ESP_ERR_INVALID_STATE:
|
||||
MessageOutput.println("[Pylontech] Twai driver uninstall - invalid state");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PylontechCanReceiver::loop()
|
||||
{
|
||||
#ifdef PYLONTECH_DUMMY
|
||||
return dummyData();
|
||||
#endif
|
||||
|
||||
// Check for messages. twai_receive is blocking when there is no data so we return if there are no frames in the buffer
|
||||
twai_status_info_t status_info;
|
||||
esp_err_t twaiLastResult = twai_get_status_info(&status_info);
|
||||
if (twaiLastResult != ESP_OK) {
|
||||
switch (twaiLastResult) {
|
||||
case ESP_ERR_INVALID_ARG:
|
||||
MessageOutput.println("[Pylontech] Twai driver get status - invalid arg");
|
||||
break;
|
||||
case ESP_ERR_INVALID_STATE:
|
||||
MessageOutput.println("[Pylontech] Twai driver get status - invalid state");
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (status_info.msgs_to_rx == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for message to be received, function is blocking
|
||||
twai_message_t rx_message;
|
||||
if (twai_receive(&rx_message, pdMS_TO_TICKS(100)) != ESP_OK) {
|
||||
MessageOutput.println("[Pylontech] Failed to receive message");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (rx_message.identifier) {
|
||||
case 0x351: {
|
||||
_stats->_chargeVoltage = this->scaleValue(this->readUnsignedInt16(rx_message.data), 0.1);
|
||||
_stats->_chargeCurrentLimitation = this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1);
|
||||
_stats->_dischargeCurrentLimitation = this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1);
|
||||
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[Pylontech] chargeVoltage: %f chargeCurrentLimitation: %f dischargeCurrentLimitation: %f\n",
|
||||
_stats->_chargeVoltage, _stats->_chargeCurrentLimitation, _stats->_dischargeCurrentLimitation);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x355: {
|
||||
_stats->setSoC(static_cast<uint8_t>(this->readUnsignedInt16(rx_message.data)));
|
||||
_stats->_stateOfHealth = this->readUnsignedInt16(rx_message.data + 2);
|
||||
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[Pylontech] soc: %d soh: %d\n",
|
||||
_stats->getSoC(), _stats->_stateOfHealth);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x356: {
|
||||
_stats->_voltage = this->scaleValue(this->readSignedInt16(rx_message.data), 0.01);
|
||||
_stats->_current = this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1);
|
||||
_stats->_temperature = this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1);
|
||||
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[Pylontech] voltage: %f current: %f temperature: %f\n",
|
||||
_stats->_voltage, _stats->_current, _stats->_temperature);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x359: {
|
||||
uint16_t alarmBits = rx_message.data[0];
|
||||
_stats->_alarmOverCurrentDischarge = this->getBit(alarmBits, 7);
|
||||
_stats->_alarmUnderTemperature = this->getBit(alarmBits, 4);
|
||||
_stats->_alarmOverTemperature = this->getBit(alarmBits, 3);
|
||||
_stats->_alarmUnderVoltage = this->getBit(alarmBits, 2);
|
||||
_stats->_alarmOverVoltage= this->getBit(alarmBits, 1);
|
||||
|
||||
alarmBits = rx_message.data[1];
|
||||
_stats->_alarmBmsInternal= this->getBit(alarmBits, 3);
|
||||
_stats->_alarmOverCurrentCharge = this->getBit(alarmBits, 0);
|
||||
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[Pylontech] Alarms: %d %d %d %d %d %d %d\n",
|
||||
_stats->_alarmOverCurrentDischarge,
|
||||
_stats->_alarmUnderTemperature,
|
||||
_stats->_alarmOverTemperature,
|
||||
_stats->_alarmUnderVoltage,
|
||||
_stats->_alarmOverVoltage,
|
||||
_stats->_alarmBmsInternal,
|
||||
_stats->_alarmOverCurrentCharge);
|
||||
}
|
||||
|
||||
uint16_t warningBits = rx_message.data[2];
|
||||
_stats->_warningHighCurrentDischarge = this->getBit(warningBits, 7);
|
||||
_stats->_warningLowTemperature = this->getBit(warningBits, 4);
|
||||
_stats->_warningHighTemperature = this->getBit(warningBits, 3);
|
||||
_stats->_warningLowVoltage = this->getBit(warningBits, 2);
|
||||
_stats->_warningHighVoltage = this->getBit(warningBits, 1);
|
||||
|
||||
warningBits = rx_message.data[3];
|
||||
_stats->_warningBmsInternal= this->getBit(warningBits, 3);
|
||||
_stats->_warningHighCurrentCharge = this->getBit(warningBits, 0);
|
||||
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[Pylontech] Warnings: %d %d %d %d %d %d %d\n",
|
||||
_stats->_warningHighCurrentDischarge,
|
||||
_stats->_warningLowTemperature,
|
||||
_stats->_warningHighTemperature,
|
||||
_stats->_warningLowVoltage,
|
||||
_stats->_warningHighVoltage,
|
||||
_stats->_warningBmsInternal,
|
||||
_stats->_warningHighCurrentCharge);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x35E: {
|
||||
String manufacturer(reinterpret_cast<char*>(rx_message.data),
|
||||
rx_message.data_length_code);
|
||||
|
||||
if (manufacturer.isEmpty()) { break; }
|
||||
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[Pylontech] Manufacturer: %s\n", manufacturer.c_str());
|
||||
}
|
||||
|
||||
_stats->setManufacturer(std::move(manufacturer));
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x35C: {
|
||||
uint16_t chargeStatusBits = rx_message.data[0];
|
||||
_stats->_chargeEnabled = this->getBit(chargeStatusBits, 7);
|
||||
_stats->_dischargeEnabled = this->getBit(chargeStatusBits, 6);
|
||||
_stats->_chargeImmediately = this->getBit(chargeStatusBits, 5);
|
||||
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[Pylontech] chargeStatusBits: %d %d %d\n",
|
||||
_stats->_chargeEnabled,
|
||||
_stats->_dischargeEnabled,
|
||||
_stats->_chargeImmediately);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return; // do not update last update timestamp
|
||||
break;
|
||||
}
|
||||
|
||||
_stats->setLastUpdate(millis());
|
||||
}
|
||||
|
||||
uint16_t PylontechCanReceiver::readUnsignedInt16(uint8_t *data)
|
||||
{
|
||||
uint8_t bytes[2];
|
||||
bytes[0] = *data;
|
||||
bytes[1] = *(data + 1);
|
||||
return (bytes[1] << 8) + bytes[0];
|
||||
}
|
||||
|
||||
int16_t PylontechCanReceiver::readSignedInt16(uint8_t *data)
|
||||
{
|
||||
return this->readUnsignedInt16(data);
|
||||
}
|
||||
|
||||
float PylontechCanReceiver::scaleValue(int16_t value, float factor)
|
||||
{
|
||||
return value * factor;
|
||||
}
|
||||
|
||||
bool PylontechCanReceiver::getBit(uint8_t value, uint8_t bit)
|
||||
{
|
||||
return (value & (1 << bit)) >> bit;
|
||||
}
|
||||
|
||||
#ifdef PYLONTECH_DUMMY
|
||||
void PylontechCanReceiver::dummyData()
|
||||
{
|
||||
static uint32_t lastUpdate = millis();
|
||||
static uint8_t issues = 0;
|
||||
|
||||
if (millis() < (lastUpdate + 5 * 1000)) { return; }
|
||||
|
||||
lastUpdate = millis();
|
||||
_stats->setLastUpdate(lastUpdate);
|
||||
|
||||
auto dummyFloat = [](int offset) -> float {
|
||||
return offset + (static_cast<float>((lastUpdate + offset) % 10) / 10);
|
||||
};
|
||||
|
||||
_stats->setManufacturer("Pylontech US3000C");
|
||||
_stats->setSoC(42);
|
||||
_stats->_chargeVoltage = dummyFloat(50);
|
||||
_stats->_chargeCurrentLimitation = dummyFloat(33);
|
||||
_stats->_dischargeCurrentLimitation = dummyFloat(12);
|
||||
_stats->_stateOfHealth = 99;
|
||||
_stats->_voltage = 48.67;
|
||||
_stats->_current = dummyFloat(-1);
|
||||
_stats->_temperature = dummyFloat(20);
|
||||
|
||||
_stats->_chargeEnabled = true;
|
||||
_stats->_dischargeEnabled = true;
|
||||
_stats->_chargeImmediately = false;
|
||||
|
||||
_stats->_warningHighCurrentDischarge = false;
|
||||
_stats->_warningHighCurrentCharge = false;
|
||||
_stats->_warningLowTemperature = false;
|
||||
_stats->_warningHighTemperature = false;
|
||||
_stats->_warningLowVoltage = false;
|
||||
_stats->_warningHighVoltage = false;
|
||||
_stats->_warningBmsInternal = false;
|
||||
|
||||
_stats->_alarmOverCurrentDischarge = false;
|
||||
_stats->_alarmOverCurrentCharge = false;
|
||||
_stats->_alarmUnderTemperature = false;
|
||||
_stats->_alarmOverTemperature = false;
|
||||
_stats->_alarmUnderVoltage = false;
|
||||
_stats->_alarmOverVoltage = false;
|
||||
_stats->_alarmBmsInternal = false;
|
||||
|
||||
if (issues == 1 || issues == 3) {
|
||||
_stats->_warningHighCurrentDischarge = true;
|
||||
_stats->_warningHighCurrentCharge = true;
|
||||
_stats->_warningLowTemperature = true;
|
||||
_stats->_warningHighTemperature = true;
|
||||
_stats->_warningLowVoltage = true;
|
||||
_stats->_warningHighVoltage = true;
|
||||
_stats->_warningBmsInternal = true;
|
||||
}
|
||||
|
||||
if (issues == 2 || issues == 3) {
|
||||
_stats->_alarmOverCurrentDischarge = true;
|
||||
_stats->_alarmOverCurrentCharge = true;
|
||||
_stats->_alarmUnderTemperature = true;
|
||||
_stats->_alarmOverTemperature = true;
|
||||
_stats->_alarmUnderVoltage = true;
|
||||
_stats->_alarmOverVoltage = true;
|
||||
_stats->_alarmBmsInternal = true;
|
||||
}
|
||||
|
||||
if (issues == 4) {
|
||||
_stats->_warningHighCurrentCharge = true;
|
||||
_stats->_warningLowTemperature = true;
|
||||
_stats->_alarmUnderVoltage = true;
|
||||
_stats->_dischargeEnabled = false;
|
||||
_stats->_chargeImmediately = true;
|
||||
}
|
||||
|
||||
issues = (issues + 1) % 5;
|
||||
}
|
||||
#endif
|
||||
139
src/VictronMppt.cpp
Normal file
@ -0,0 +1,139 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include "VictronMppt.h"
|
||||
#include "Configuration.h"
|
||||
#include "PinMapping.h"
|
||||
#include "MessageOutput.h"
|
||||
|
||||
VictronMpptClass VictronMppt;
|
||||
|
||||
void VictronMpptClass::init(Scheduler& scheduler)
|
||||
{
|
||||
scheduler.addTask(_loopTask);
|
||||
_loopTask.setCallback(std::bind(&VictronMpptClass::loop, this));
|
||||
_loopTask.setIterations(TASK_FOREVER);
|
||||
_loopTask.enable();
|
||||
|
||||
this->updateSettings();
|
||||
}
|
||||
|
||||
void VictronMpptClass::updateSettings()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
|
||||
_controllers.clear();
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
if (!config.Vedirect.Enabled) { return; }
|
||||
|
||||
const PinMapping_t& pin = PinMapping.get();
|
||||
int8_t rx = pin.victron_rx;
|
||||
int8_t tx = pin.victron_tx;
|
||||
|
||||
MessageOutput.printf("[VictronMppt] rx = %d, tx = %d\r\n", rx, tx);
|
||||
|
||||
if (rx < 0) {
|
||||
MessageOutput.println("[VictronMppt] invalid pin config");
|
||||
return;
|
||||
}
|
||||
|
||||
auto upController = std::make_unique<VeDirectMpptController>();
|
||||
upController->init(rx, tx, &MessageOutput, config.Vedirect.VerboseLogging);
|
||||
_controllers.push_back(std::move(upController));
|
||||
}
|
||||
|
||||
void VictronMpptClass::loop()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
|
||||
for (auto const& upController : _controllers) {
|
||||
upController->loop();
|
||||
}
|
||||
}
|
||||
|
||||
bool VictronMpptClass::isDataValid() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
|
||||
for (auto const& upController : _controllers) {
|
||||
if (!upController->isDataValid()) { return false; }
|
||||
}
|
||||
|
||||
return !_controllers.empty();
|
||||
}
|
||||
|
||||
uint32_t VictronMpptClass::getDataAgeMillis() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
|
||||
if (_controllers.empty()) { return 0; }
|
||||
|
||||
auto now = millis();
|
||||
|
||||
auto iter = _controllers.cbegin();
|
||||
uint32_t age = now - (*iter)->getLastUpdate();
|
||||
++iter;
|
||||
|
||||
while (iter != _controllers.end()) {
|
||||
age = std::min<uint32_t>(age, now - (*iter)->getLastUpdate());
|
||||
++iter;
|
||||
}
|
||||
|
||||
return age;
|
||||
}
|
||||
|
||||
VeDirectMpptController::spData_t VictronMpptClass::getData(size_t idx) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
|
||||
if (_controllers.empty() || idx >= _controllers.size()) {
|
||||
MessageOutput.printf("ERROR: MPPT controller index %d is out of bounds (%d controllers)\r\n",
|
||||
idx, _controllers.size());
|
||||
return std::make_shared<VeDirectMpptController::veMpptStruct>();
|
||||
}
|
||||
|
||||
return _controllers[idx]->getData();
|
||||
}
|
||||
|
||||
int32_t VictronMpptClass::getPowerOutputWatts() const
|
||||
{
|
||||
int32_t sum = 0;
|
||||
|
||||
for (const auto& upController : _controllers) {
|
||||
sum += upController->getData()->P;
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
int32_t VictronMpptClass::getPanelPowerWatts() const
|
||||
{
|
||||
int32_t sum = 0;
|
||||
|
||||
for (const auto& upController : _controllers) {
|
||||
sum += upController->getData()->PPV;
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
double VictronMpptClass::getYieldTotal() const
|
||||
{
|
||||
double sum = 0;
|
||||
|
||||
for (const auto& upController : _controllers) {
|
||||
sum += upController->getData()->H19;
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
double VictronMpptClass::getYieldDay() const
|
||||
{
|
||||
double sum = 0;
|
||||
|
||||
for (const auto& upController : _controllers) {
|
||||
sum += upController->getData()->H20;
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
36
src/VictronSmartShunt.cpp
Normal file
@ -0,0 +1,36 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include "VictronSmartShunt.h"
|
||||
#include "Configuration.h"
|
||||
#include "PinMapping.h"
|
||||
#include "MessageOutput.h"
|
||||
|
||||
|
||||
bool VictronSmartShunt::init(bool verboseLogging)
|
||||
{
|
||||
MessageOutput.println("[VictronSmartShunt] Initialize interface...");
|
||||
|
||||
const PinMapping_t& pin = PinMapping.get();
|
||||
MessageOutput.printf("[VictronSmartShunt] Interface rx = %d, tx = %d\r\n",
|
||||
pin.battery_rx, pin.battery_tx);
|
||||
|
||||
if (pin.battery_rx < 0) {
|
||||
MessageOutput.println("[VictronSmartShunt] Invalid pin config");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto tx = static_cast<gpio_num_t>(pin.battery_tx);
|
||||
auto rx = static_cast<gpio_num_t>(pin.battery_rx);
|
||||
|
||||
VeDirectShunt.init(rx, tx, &MessageOutput, verboseLogging);
|
||||
return true;
|
||||
}
|
||||
|
||||
void VictronSmartShunt::loop()
|
||||
{
|
||||
VeDirectShunt.loop();
|
||||
|
||||
if (VeDirectShunt.getLastUpdate() <= _lastUpdate) { return; }
|
||||
|
||||
_stats->updateFrom(VeDirectShunt.veFrame);
|
||||
_lastUpdate = VeDirectShunt.getLastUpdate();
|
||||
}
|
||||
@ -34,6 +34,14 @@ void WebApiClass::init(Scheduler& scheduler)
|
||||
_webApiWebapp.init(_server);
|
||||
_webApiWsConsole.init(_server);
|
||||
_webApiWsLive.init(_server);
|
||||
_webApiBattery.init(_server);
|
||||
_webApiPowerMeter.init(_server);
|
||||
_webApiPowerLimiter.init(_server);
|
||||
_webApiWsVedirectLive.init(_server);
|
||||
_webApiVedirect.init(_server);
|
||||
_webApiWsHuaweiLive.init(_server);
|
||||
_webApiHuaweiClass.init(_server);
|
||||
_webApiWsBatteryLive.init(_server);
|
||||
|
||||
_server.begin();
|
||||
|
||||
@ -45,6 +53,7 @@ void WebApiClass::init(Scheduler& scheduler)
|
||||
|
||||
void WebApiClass::loop()
|
||||
{
|
||||
_webApiBattery.loop();
|
||||
_webApiConfig.loop();
|
||||
_webApiDevice.loop();
|
||||
_webApiDevInfo.loop();
|
||||
@ -59,11 +68,18 @@ void WebApiClass::loop()
|
||||
_webApiNetwork.loop();
|
||||
_webApiNtp.loop();
|
||||
_webApiPower.loop();
|
||||
_webApiPowerMeter.loop();
|
||||
_webApiPowerLimiter.loop();
|
||||
_webApiSecurity.loop();
|
||||
_webApiSysstatus.loop();
|
||||
_webApiWebapp.loop();
|
||||
_webApiWsConsole.loop();
|
||||
_webApiWsLive.loop();
|
||||
_webApiWsVedirectLive.loop();
|
||||
_webApiVedirect.loop();
|
||||
_webApiWsHuaweiLive.loop();
|
||||
_webApiHuaweiClass.loop();
|
||||
_webApiWsBatteryLive.loop();
|
||||
}
|
||||
|
||||
bool WebApiClass::checkCredentials(AsyncWebServerRequest* request)
|
||||
|
||||
305
src/WebApi_Huawei.cpp
Normal file
@ -0,0 +1,305 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022 Thomas Basler and others
|
||||
*/
|
||||
#include "WebApi_Huawei.h"
|
||||
#include "Huawei_can.h"
|
||||
#include "Configuration.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "PinMapping.h"
|
||||
#include "WebApi.h"
|
||||
#include "WebApi_errors.h"
|
||||
#include <AsyncJson.h>
|
||||
#include <Hoymiles.h>
|
||||
|
||||
void WebApiHuaweiClass::init(AsyncWebServer& server)
|
||||
{
|
||||
using std::placeholders::_1;
|
||||
|
||||
_server = &server;
|
||||
|
||||
_server->on("/api/huawei/status", HTTP_GET, std::bind(&WebApiHuaweiClass::onStatus, this, _1));
|
||||
_server->on("/api/huawei/config", HTTP_GET, std::bind(&WebApiHuaweiClass::onAdminGet, this, _1));
|
||||
_server->on("/api/huawei/config", HTTP_POST, std::bind(&WebApiHuaweiClass::onAdminPost, this, _1));
|
||||
_server->on("/api/huawei/limit/config", HTTP_POST, std::bind(&WebApiHuaweiClass::onPost, this, _1));
|
||||
}
|
||||
|
||||
void WebApiHuaweiClass::loop()
|
||||
{
|
||||
}
|
||||
|
||||
void WebApiHuaweiClass::getJsonData(JsonVariant& root) {
|
||||
const RectifierParameters_t * rp = HuaweiCan.get();
|
||||
|
||||
root["data_age"] = (millis() - HuaweiCan.getLastUpdate()) / 1000;
|
||||
root["input_voltage"]["v"] = rp->input_voltage;
|
||||
root["input_voltage"]["u"] = "V";
|
||||
root["input_current"]["v"] = rp->input_current;
|
||||
root["input_current"]["u"] = "A";
|
||||
root["input_power"]["v"] = rp->input_power;
|
||||
root["input_power"]["u"] = "W";
|
||||
root["output_voltage"]["v"] = rp->output_voltage;
|
||||
root["output_voltage"]["u"] = "V";
|
||||
root["output_current"]["v"] = rp->output_current;
|
||||
root["output_current"]["u"] = "A";
|
||||
root["max_output_current"]["v"] = rp->max_output_current;
|
||||
root["max_output_current"]["u"] = "A";
|
||||
root["output_power"]["v"] = rp->output_power;
|
||||
root["output_power"]["u"] = "W";
|
||||
root["input_temp"]["v"] = rp->input_temp;
|
||||
root["input_temp"]["u"] = "°C";
|
||||
root["output_temp"]["v"] = rp->output_temp;
|
||||
root["output_temp"]["u"] = "°C";
|
||||
root["efficiency"]["v"] = rp->efficiency * 100;
|
||||
root["efficiency"]["u"] = "%";
|
||||
|
||||
}
|
||||
|
||||
void WebApiHuaweiClass::onStatus(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentialsReadonly(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& root = response->getRoot();
|
||||
getJsonData(root);
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
void WebApiHuaweiClass::onPost(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& retMsg = response->getRoot();
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
float value;
|
||||
uint8_t online = true;
|
||||
float minimal_voltage;
|
||||
|
||||
if (error) {
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.containsKey("online")) {
|
||||
online = root["online"].as<bool>();
|
||||
if (online) {
|
||||
minimal_voltage = HUAWEI_MINIMAL_ONLINE_VOLTAGE;
|
||||
} else {
|
||||
minimal_voltage = HUAWEI_MINIMAL_OFFLINE_VOLTAGE;
|
||||
}
|
||||
} else {
|
||||
retMsg["message"] = "Could not read info if data should be set for online/offline operation!";
|
||||
retMsg["code"] = WebApiError::LimitInvalidType;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.containsKey("voltage_valid")) {
|
||||
if (root["voltage_valid"].as<bool>()) {
|
||||
if (root["voltage"].as<float>() < minimal_voltage || root["voltage"].as<float>() > 58) {
|
||||
retMsg["message"] = "voltage not in range between 42 (online)/48 (offline and 58V !";
|
||||
retMsg["code"] = WebApiError::LimitInvalidLimit;
|
||||
retMsg["param"]["max"] = 58;
|
||||
retMsg["param"]["min"] = minimal_voltage;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
} else {
|
||||
value = root["voltage"].as<float>();
|
||||
if (online) {
|
||||
HuaweiCan.setValue(value, HUAWEI_ONLINE_VOLTAGE);
|
||||
} else {
|
||||
HuaweiCan.setValue(value, HUAWEI_OFFLINE_VOLTAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (root.containsKey("current_valid")) {
|
||||
if (root["current_valid"].as<bool>()) {
|
||||
if (root["current"].as<float>() < 0 || root["current"].as<float>() > 60) {
|
||||
retMsg["message"] = "current must be in range between 0 and 60!";
|
||||
retMsg["code"] = WebApiError::LimitInvalidLimit;
|
||||
retMsg["param"]["max"] = 60;
|
||||
retMsg["param"]["min"] = 0;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
} else {
|
||||
value = root["current"].as<float>();
|
||||
if (online) {
|
||||
HuaweiCan.setValue(value, HUAWEI_ONLINE_CURRENT);
|
||||
} else {
|
||||
HuaweiCan.setValue(value, HUAWEI_OFFLINE_CURRENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
retMsg["type"] = "success";
|
||||
retMsg["message"] = "Settings saved!";
|
||||
retMsg["code"] = WebApiError::GenericSuccess;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void WebApiHuaweiClass::onAdminGet(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentialsReadonly(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& root = response->getRoot();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
root["enabled"] = config.Huawei.Enabled;
|
||||
root["can_controller_frequency"] = config.Huawei.CAN_Controller_Frequency;
|
||||
root["auto_power_enabled"] = config.Huawei.Auto_Power_Enabled;
|
||||
root["voltage_limit"] = static_cast<int>(config.Huawei.Auto_Power_Voltage_Limit * 100) / 100.0;
|
||||
root["enable_voltage_limit"] = static_cast<int>(config.Huawei.Auto_Power_Enable_Voltage_Limit * 100) / 100.0;
|
||||
root["lower_power_limit"] = config.Huawei.Auto_Power_Lower_Power_Limit;
|
||||
root["upper_power_limit"] = config.Huawei.Auto_Power_Upper_Power_Limit;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& retMsg = response->getRoot();
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("enabled")) ||
|
||||
!(root.containsKey("can_controller_frequency")) ||
|
||||
!(root.containsKey("auto_power_enabled")) ||
|
||||
!(root.containsKey("voltage_limit")) ||
|
||||
!(root.containsKey("lower_power_limit")) ||
|
||||
!(root.containsKey("upper_power_limit"))) {
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
config.Huawei.Enabled = root["enabled"].as<bool>();
|
||||
config.Huawei.CAN_Controller_Frequency = root["can_controller_frequency"].as<uint32_t>();
|
||||
config.Huawei.Auto_Power_Enabled = root["auto_power_enabled"].as<bool>();
|
||||
config.Huawei.Auto_Power_Voltage_Limit = root["voltage_limit"].as<float>();
|
||||
config.Huawei.Auto_Power_Enable_Voltage_Limit = root["enable_voltage_limit"].as<float>();
|
||||
config.Huawei.Auto_Power_Lower_Power_Limit = root["lower_power_limit"].as<float>();
|
||||
config.Huawei.Auto_Power_Upper_Power_Limit = root["upper_power_limit"].as<float>();
|
||||
|
||||
WebApi.writeConfig(retMsg);
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
|
||||
// TODO(schlimmchen): HuaweiCan has no real concept of the fact that the
|
||||
// config might change. at least not regarding CAN parameters. until that
|
||||
// changes, the ESP must restart for configuration changes to take effect.
|
||||
yield();
|
||||
delay(1000);
|
||||
yield();
|
||||
ESP.restart();
|
||||
|
||||
const PinMapping_t& pin = PinMapping.get();
|
||||
// Properly turn this on
|
||||
if (config.Huawei.Enabled) {
|
||||
MessageOutput.println("Initialize Huawei AC charger interface... ");
|
||||
if (PinMapping.isValidHuaweiConfig()) {
|
||||
MessageOutput.printf("Huawei AC-charger miso = %d, mosi = %d, clk = %d, irq = %d, cs = %d, power_pin = %d\r\n", pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power);
|
||||
HuaweiCan.updateSettings(pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power);
|
||||
MessageOutput.println("done");
|
||||
} else {
|
||||
MessageOutput.println("Invalid pin config");
|
||||
}
|
||||
}
|
||||
|
||||
// Properly turn this off
|
||||
if (!config.Huawei.Enabled) {
|
||||
HuaweiCan.setValue(0, HUAWEI_ONLINE_CURRENT);
|
||||
delay(500);
|
||||
HuaweiCan.setMode(HUAWEI_MODE_OFF);
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.Huawei.Auto_Power_Enabled) {
|
||||
HuaweiCan.setMode(HUAWEI_MODE_AUTO_INT);
|
||||
return;
|
||||
}
|
||||
|
||||
HuaweiCan.setMode(HUAWEI_MODE_AUTO_EXT);
|
||||
}
|
||||
118
src/WebApi_battery.cpp
Normal file
@ -0,0 +1,118 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022 Thomas Basler and others
|
||||
*/
|
||||
|
||||
#include "ArduinoJson.h"
|
||||
#include "AsyncJson.h"
|
||||
#include "Battery.h"
|
||||
#include "Configuration.h"
|
||||
#include "PylontechCanReceiver.h"
|
||||
#include "WebApi.h"
|
||||
#include "WebApi_battery.h"
|
||||
#include "WebApi_errors.h"
|
||||
#include "helper.h"
|
||||
|
||||
void WebApiBatteryClass::init(AsyncWebServer& server)
|
||||
{
|
||||
using std::placeholders::_1;
|
||||
|
||||
_server = &server;
|
||||
|
||||
_server->on("/api/battery/status", HTTP_GET, std::bind(&WebApiBatteryClass::onStatus, this, _1));
|
||||
_server->on("/api/battery/config", HTTP_GET, std::bind(&WebApiBatteryClass::onAdminGet, this, _1));
|
||||
_server->on("/api/battery/config", HTTP_POST, std::bind(&WebApiBatteryClass::onAdminPost, this, _1));
|
||||
}
|
||||
|
||||
void WebApiBatteryClass::loop()
|
||||
{
|
||||
}
|
||||
|
||||
void WebApiBatteryClass::onStatus(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentialsReadonly(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& root = response->getRoot();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
root["enabled"] = config.Battery.Enabled;
|
||||
root["verbose_logging"] = config.Battery.VerboseLogging;
|
||||
root["provider"] = config.Battery.Provider;
|
||||
root["jkbms_interface"] = config.Battery.JkBmsInterface;
|
||||
root["jkbms_polling_interval"] = config.Battery.JkBmsPollingInterval;
|
||||
root["mqtt_topic"] = config.Battery.MqttTopic;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
void WebApiBatteryClass::onAdminGet(AsyncWebServerRequest* request)
|
||||
{
|
||||
onStatus(request);
|
||||
}
|
||||
|
||||
void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& retMsg = response->getRoot();
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!root.containsKey("enabled") || !root.containsKey("provider")) {
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
config.Battery.Enabled = root["enabled"].as<bool>();
|
||||
config.Battery.VerboseLogging = root["verbose_logging"].as<bool>();
|
||||
config.Battery.Provider = root["provider"].as<uint8_t>();
|
||||
config.Battery.JkBmsInterface = root["jkbms_interface"].as<uint8_t>();
|
||||
config.Battery.JkBmsPollingInterval = root["jkbms_polling_interval"].as<uint8_t>();
|
||||
strlcpy(config.Battery.MqttTopic, root["mqtt_topic"].as<String>().c_str(), sizeof(config.Battery.MqttTopic));
|
||||
|
||||
WebApi.writeConfig(retMsg);
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
|
||||
Battery.updateSettings();
|
||||
}
|
||||
@ -92,6 +92,24 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
|
||||
led["brightness"] = config.Led_Single[i].Brightness;
|
||||
}
|
||||
|
||||
JsonObject victronPinObj = curPin.createNestedObject("victron");
|
||||
victronPinObj["rx"] = pin.victron_rx;
|
||||
victronPinObj["tx"] = pin.victron_tx;
|
||||
|
||||
JsonObject batteryPinObj = curPin.createNestedObject("battery");
|
||||
batteryPinObj["rx"] = pin.battery_rx;
|
||||
batteryPinObj["rxen"] = pin.battery_rxen;
|
||||
batteryPinObj["tx"] = pin.battery_tx;
|
||||
batteryPinObj["txen"] = pin.battery_txen;
|
||||
|
||||
JsonObject huaweiPinObj = curPin.createNestedObject("huawei");
|
||||
huaweiPinObj["miso"] = pin.huawei_miso;
|
||||
huaweiPinObj["mosi"] = pin.huawei_mosi;
|
||||
huaweiPinObj["clk"] = pin.huawei_clk;
|
||||
huaweiPinObj["irq"] = pin.huawei_irq;
|
||||
huaweiPinObj["cs"] = pin.huawei_cs;
|
||||
huaweiPinObj["power"] = pin.huawei_power;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
@ -52,6 +52,7 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request)
|
||||
((uint32_t)(config.Dtu.Serial & 0xFFFFFFFF)));
|
||||
root["serial"] = buffer;
|
||||
root["pollinterval"] = config.Dtu.PollInterval;
|
||||
root["verbose_logging"] = config.Dtu.VerboseLogging;
|
||||
root["nrf_enabled"] = Hoymiles.getRadioNrf()->isInitialized();
|
||||
root["nrf_palevel"] = config.Dtu.Nrf.PaLevel;
|
||||
root["cmt_enabled"] = Hoymiles.getRadioCmt()->isInitialized();
|
||||
@ -114,10 +115,11 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("serial")
|
||||
if (!(root.containsKey("serial")
|
||||
&& root.containsKey("pollinterval")
|
||||
&& root.containsKey("nrf_palevel")
|
||||
&& root.containsKey("cmt_palevel")
|
||||
&& root.containsKey("verbose_logging")
|
||||
&& root.containsKey("nrf_palevel")
|
||||
&& root.containsKey("cmt_palevel")
|
||||
&& root.containsKey("cmt_frequency")
|
||||
&& root.containsKey("cmt_country"))) {
|
||||
retMsg["message"] = "Values are missing!";
|
||||
@ -186,6 +188,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
|
||||
// Interpret the string as a hex value and convert it to uint64_t
|
||||
config.Dtu.Serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
|
||||
config.Dtu.PollInterval = root["pollinterval"].as<uint32_t>();
|
||||
config.Dtu.VerboseLogging = root["verbose_logging"].as<bool>();
|
||||
config.Dtu.Nrf.PaLevel = root["nrf_palevel"].as<uint8_t>();
|
||||
config.Dtu.Cmt.PaLevel = root["cmt_palevel"].as<int8_t>();
|
||||
config.Dtu.Cmt.Frequency = root["cmt_frequency"].as<uint32_t>();
|
||||
|
||||