Compare commits
789 Commits
master
...
2024.05.06
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35491cafed | ||
|
|
2f7e1f3f70 | ||
|
|
b7a8bdf07b | ||
|
|
1dd64a57fd | ||
|
|
d2990bd8fd | ||
|
|
6620ab487a | ||
|
|
686b5df64e | ||
|
|
744df41b01 | ||
|
|
18dab3cf1c | ||
|
|
dae50ec3b7 | ||
|
|
4cf596eb5a | ||
|
|
4e36c8c9ea | ||
|
|
d3b306e2fc | ||
|
|
84e83f2dbb | ||
|
|
64738a6246 | ||
|
|
fdc5054480 | ||
|
|
f0df583c13 | ||
|
|
74330a5617 | ||
|
|
eb9bfd1ac6 | ||
|
|
1d4bea24ff | ||
|
|
4cd690de66 | ||
|
|
abe01ae36f | ||
|
|
ede1abb5e6 | ||
|
|
247cfe712e | ||
|
|
e92701ccdf | ||
|
|
4bc4defe66 | ||
|
|
5fcf09d0a0 | ||
|
|
52d7ac9581 | ||
|
|
7e307114e5 | ||
|
|
cf1ea42f8b | ||
|
|
4c2822cdbc | ||
|
|
8b3a1bef47 | ||
|
|
f634f58788 | ||
|
|
a9c3e05f05 | ||
|
|
165a9bc168 | ||
|
|
0ed09aeb4c | ||
|
|
b9ad1e3054 | ||
|
|
3934906001 | ||
|
|
21cdc69625 | ||
|
|
6b8c93d2e6 | ||
|
|
aadd7303ac | ||
|
|
ff44267e73 | ||
|
|
8c6e925ca4 | ||
|
|
43f553d2d4 | ||
|
|
92a7f27919 | ||
|
|
b299b9dc6c | ||
|
|
ad125ea804 | ||
|
|
187f197e32 | ||
|
|
8abf614047 | ||
|
|
da96273085 | ||
|
|
91a0992964 | ||
|
|
811b64adb5 | ||
|
|
0169b29cfd | ||
|
|
06f39f8396 | ||
|
|
12f7caf998 | ||
|
|
b8d0998a49 | ||
|
|
c41d6f5138 | ||
|
|
054a677575 | ||
|
|
8bfb5c6523 | ||
|
|
1fb2d4262e | ||
|
|
0154da91ab | ||
|
|
1c51c2de40 | ||
|
|
d935283d1f | ||
|
|
13e42051b6 | ||
|
|
03435f66ee | ||
|
|
bbaed260f5 | ||
|
|
b449dd1196 | ||
|
|
8e5e8d169d | ||
|
|
cffc5b1d26 | ||
|
|
5259fc172a | ||
|
|
cd339a3a14 | ||
|
|
7d4a30dde4 | ||
|
|
4f0385285c | ||
|
|
45c7243937 | ||
|
|
f6680bd664 | ||
|
|
900326742c | ||
|
|
13bc943dd5 | ||
|
|
7d6b7252bf | ||
|
|
75541be248 | ||
|
|
21c19f4b7f | ||
|
|
7ebd4f4632 | ||
|
|
2efa1b35b0 | ||
|
|
5c6b4a8f12 | ||
|
|
8895791145 | ||
|
|
cf27bd29d7 | ||
|
|
56353e4f00 | ||
|
|
784e369482 | ||
|
|
fba5c02346 | ||
|
|
803b30ca11 | ||
|
|
19859ed601 | ||
|
|
c46980d6af | ||
|
|
80edbec769 | ||
|
|
91f8f61e63 | ||
|
|
be15050aed | ||
|
|
c6f81806d6 | ||
|
|
b11b1dbcba | ||
|
|
490a38f909 | ||
|
|
c42d68812c | ||
|
|
b0795a2131 | ||
|
|
64ad4bded1 | ||
|
|
50635ee2ce | ||
|
|
e432f0eca3 | ||
|
|
fe7e622e2d | ||
|
|
78e70cc6c5 | ||
|
|
8b6e57cda7 | ||
|
|
c560d1d90e | ||
|
|
f0f8702b4b | ||
|
|
c72ae561c7 | ||
|
|
4e7dfba85c | ||
|
|
1eb75c322d | ||
|
|
9240663552 | ||
|
|
c930018764 | ||
|
|
6df358242c | ||
|
|
7c069b1cc4 | ||
|
|
3595725f8a | ||
|
|
30bfffb848 | ||
|
|
921302bf73 | ||
|
|
b3d8e984bc | ||
|
|
b794f46ef0 | ||
|
|
7c84621ea9 | ||
|
|
df5b416b3f | ||
|
|
1865113842 | ||
|
|
c2b49931be | ||
|
|
db51680712 | ||
|
|
1916d6d6a8 | ||
|
|
7094bca7db | ||
|
|
ebacc2f25f | ||
|
|
f709bb7a78 | ||
|
|
f305b40be0 | ||
|
|
7439ad0934 | ||
|
|
75e3d03ea4 | ||
|
|
8392c7cd8c | ||
|
|
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 |
49
.github/workflows/build.yml
vendored
@ -1,10 +1,15 @@
|
|||||||
name: OpenDTU Build
|
name: OpenDTU-onBattery Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- docs/**
|
- docs/**
|
||||||
- '**/*.md'
|
- '**/*.md'
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- development
|
||||||
|
tags-ignore:
|
||||||
|
- 'v**'
|
||||||
pull_request:
|
pull_request:
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- docs/**
|
- docs/**
|
||||||
@ -55,6 +60,15 @@ jobs:
|
|||||||
- name: Get tags
|
- name: Get tags
|
||||||
run: git fetch --force --tags origin
|
run: git fetch --force --tags origin
|
||||||
|
|
||||||
|
- name: Create and switch to a meaningful branch for pull-requests
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
run: |
|
||||||
|
OWNER=${{ github.repository_owner }}
|
||||||
|
NAME=${{ github.event.repository.name }}
|
||||||
|
ID=${{ github.event.pull_request.number }}
|
||||||
|
DATE=$(date +'%Y%m%d%H%M')
|
||||||
|
git switch -c ${OWNER}/${NAME}/pr${ID}-${DATE}
|
||||||
|
|
||||||
- name: Cache pip
|
- name: Cache pip
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@ -96,26 +110,43 @@ jobs:
|
|||||||
run: pio run -e ${{ matrix.environment }}
|
run: pio run -e ${{ matrix.environment }}
|
||||||
|
|
||||||
- name: Rename Firmware
|
- 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
|
- 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
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: opendtu-${{ matrix.environment }}
|
name: opendtu-onbattery-${{ matrix.environment }}
|
||||||
path: |
|
path: |
|
||||||
.pio/build/${{ matrix.environment }}/opendtu-${{ matrix.environment }}.bin
|
.pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.bin
|
||||||
.pio/build/${{ matrix.environment }}/opendtu-${{ matrix.environment }}.factory.bin
|
.pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.factory.bin
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Create Release
|
name: Create Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [get_default_envs, build]
|
needs: [get_default_envs, build]
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/2')
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- 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
|
- name: Build Changelog
|
||||||
id: github_release
|
id: github_release
|
||||||
@ -135,7 +166,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
ls -R
|
ls -R
|
||||||
cd artifacts
|
cd artifacts
|
||||||
for i in */; do cp ${i}opendtu-*.bin ./; done
|
for i in */; do cp ${i}opendtu-onbattery-*.bin ./; done
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"template": "${{CHANGELOG}}",
|
"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",
|
"empty_template": "- no changes",
|
||||||
"label_extractor": [
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
110
README.md
@ -1,80 +1,56 @@
|
|||||||
# 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)
|
# OpenDTU-onBattery
|
||||||
[](https://github.com/tbnobody/OpenDTU/actions/workflows/cpplint.yml)
|
|
||||||
[](https://github.com/tbnobody/OpenDTU/actions/workflows/yarnlint.yml)
|
|
||||||
|
|
||||||
## !! 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).
|
## What is OpenDTU-onBattery
|
||||||
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.
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
* 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
|
## Documentation
|
||||||
|
|
||||||
The documentation can be found [here](https://tbnobody.github.io/OpenDTU-docs/).
|
[Full documentation of OpenDTU-onBattery extensions can be found at the project's wiki](https://github.com/helgeerbe/OpenDTU-OnBattery/wiki).
|
||||||
Please feel free to support and create a PR in [this](https://github.com/tbnobody/OpenDTU-docs) repository to make the documentation even better.
|
|
||||||
|
|
||||||
## Breaking changes
|
For documentation of openDTU core functionality I refer to the original [repo](https://github.com/tbnobody/OpenDTU) and its [documentation](https://tbnobody.github.io/OpenDTU-docs/).
|
||||||
|
|
||||||
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
|
To find out what's new or improved have a look at the [changelog](https://github.com/helgeerbe/OpenDTU-OnBattery/releases).
|
||||||
* 1b637f08 2024-01-30 BREAKING CHANGE: Web API Endpoint /api/livedata/status and /api/prometheus/metrics
|
|
||||||
* e1564780 2024-01-30 BREAKING CHANGE: Web API Endpoint /api/livedata/status and /api/prometheus/metrics
|
|
||||||
* f0b5542c 2024-01-30 BREAKING CHANGE: Web API Endpoint /api/livedata/status and /api/prometheus/metrics
|
|
||||||
* c27ecc36 2024-01-29 BREAKING CHANGE: Web API Endpoint /api/livedata/status
|
|
||||||
* 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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Currently supported Inverters
|
## Acknowledgment
|
||||||
|
|
||||||
| Model | Required RF Module | DC Inputs | MPP-Tracker | AC Phases |
|
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!
|
||||||
| ---------------------| ------------------ | --------- | ----------- | --------- |
|
|
||||||
| Hoymiles HM-300-1T | NRF24L01+ | 1 | 1 | 1 |
|
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.
|
||||||
| 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 |
|
|
||||||
| E-Star HERF-800 | NRF24L01+ | 2 | 2 | 1 |
|
|
||||||
| E-Star HERF-1600 | NRF24L01+ | 4 | 2 | 1 |
|
|
||||||
| E-Star HERF-1800 | NRF24L01+ | 4 | 2 | 1 |
|
|
||||||
|
|||||||
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
|
# Device Profiles
|
||||||
|
|
||||||
This documentation will has been moved and can be found here: <https://tbnobody.github.io/OpenDTU-docs/firmware/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
|
# Web API
|
||||||
|
|
||||||
This documentation will has been moved and can be found here: <https://tbnobody.github.io/OpenDTU-docs/firmware/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 |
189
docs/hardware_flash.md
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
See [Wiki](https://github.com/helgeerbe/OpenDTU-OnBattery/wiki/Huawei-AC-PSU) for details.
|
||||||
|
|
||||||
|
### 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 |
35
include/Battery.h
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#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;
|
||||||
|
virtual bool usesHwPort2() const { return false; }
|
||||||
|
};
|
||||||
|
|
||||||
|
class BatteryClass {
|
||||||
|
public:
|
||||||
|
void init(Scheduler&);
|
||||||
|
void updateSettings();
|
||||||
|
|
||||||
|
std::shared_ptr<BatteryStats const> getStats() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void loop();
|
||||||
|
|
||||||
|
Task _loopTask;
|
||||||
|
mutable std::mutex _mutex;
|
||||||
|
std::unique_ptr<BatteryProvider> _upProvider = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern BatteryClass Battery;
|
||||||
183
include/BatteryStats.h
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
// 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"
|
||||||
|
#include <cfloat>
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
uint8_t getSoC() const { return _soc; }
|
||||||
|
uint32_t getSoCAgeSeconds() const { return (millis() - _lastUpdateSoC) / 1000; }
|
||||||
|
|
||||||
|
float getVoltage() const { return _voltage; }
|
||||||
|
uint32_t getVoltageAgeSeconds() const { return (millis() - _lastUpdateVoltage) / 1000; }
|
||||||
|
|
||||||
|
// convert stats to JSON for web application live view
|
||||||
|
virtual void getLiveViewData(JsonVariant& root) const;
|
||||||
|
|
||||||
|
void mqttLoop();
|
||||||
|
|
||||||
|
// the interval at which all battery datums will be re-published, even
|
||||||
|
// if they did not change. used to calculate Home Assistent expiration.
|
||||||
|
virtual uint32_t getMqttFullPublishIntervalMs() const;
|
||||||
|
|
||||||
|
bool isSoCValid() const { return _lastUpdateSoC > 0; }
|
||||||
|
bool isVoltageValid() const { return _lastUpdateVoltage > 0; }
|
||||||
|
|
||||||
|
// returns true if the battery reached a critically low voltage/SoC,
|
||||||
|
// such that it is in need of charging to prevent degredation.
|
||||||
|
virtual bool getImmediateChargingRequest() const { return false; };
|
||||||
|
|
||||||
|
virtual float getChargeCurrent() const { return 0; };
|
||||||
|
virtual float getChargeCurrentLimitation() const { return FLT_MAX; };
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void mqttPublish() const;
|
||||||
|
|
||||||
|
void setSoC(float soc, uint8_t precision, uint32_t timestamp) {
|
||||||
|
_soc = soc;
|
||||||
|
_socPrecision = precision;
|
||||||
|
_lastUpdateSoC = _lastUpdate = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setVoltage(float voltage, uint32_t timestamp) {
|
||||||
|
_voltage = voltage;
|
||||||
|
_lastUpdateVoltage = _lastUpdate = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _manufacturer = "unknown";
|
||||||
|
uint32_t _lastUpdate = 0;
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint32_t _lastMqttPublish = 0;
|
||||||
|
float _soc = 0;
|
||||||
|
uint8_t _socPrecision = 0; // decimal places
|
||||||
|
uint32_t _lastUpdateSoC = 0;
|
||||||
|
float _voltage = 0; // total battery pack voltage
|
||||||
|
uint32_t _lastUpdateVoltage = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PylontechBatteryStats : public BatteryStats {
|
||||||
|
friend class PylontechCanReceiver;
|
||||||
|
|
||||||
|
public:
|
||||||
|
void getLiveViewData(JsonVariant& root) const final;
|
||||||
|
void mqttPublish() const final;
|
||||||
|
bool getImmediateChargingRequest() const { return _chargeImmediately; } ;
|
||||||
|
float getChargeCurrent() const { return _current; } ;
|
||||||
|
float getChargeCurrentLimitation() const { return _chargeCurrentLimitation; } ;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void setManufacturer(String&& m) { _manufacturer = std::move(m); }
|
||||||
|
void setLastUpdate(uint32_t ts) { _lastUpdate = ts; }
|
||||||
|
|
||||||
|
float _chargeVoltage;
|
||||||
|
float _chargeCurrentLimitation;
|
||||||
|
float _dischargeCurrentLimitation;
|
||||||
|
uint16_t _stateOfHealth;
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
uint32_t getMqttFullPublishIntervalMs() const final { return 60 * 1000; }
|
||||||
|
|
||||||
|
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::data_t const& shuntData);
|
||||||
|
|
||||||
|
private:
|
||||||
|
float _current;
|
||||||
|
float _temperature;
|
||||||
|
bool _tempPresent;
|
||||||
|
uint8_t _chargeCycles;
|
||||||
|
uint32_t _timeToGo;
|
||||||
|
float _chargedEnergy;
|
||||||
|
float _dischargedEnergy;
|
||||||
|
String _modelName;
|
||||||
|
int32_t _instantaneousPower;
|
||||||
|
float _consumedAmpHours;
|
||||||
|
int32_t _lastFullCharge;
|
||||||
|
|
||||||
|
bool _alarmLowVoltage;
|
||||||
|
bool _alarmHighVoltage;
|
||||||
|
bool _alarmLowSOC;
|
||||||
|
bool _alarmLowTemperature;
|
||||||
|
bool _alarmHighTemperature;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MqttBatteryStats : public BatteryStats {
|
||||||
|
friend class MqttBattery;
|
||||||
|
|
||||||
|
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 { }
|
||||||
|
|
||||||
|
// if the voltage is subscribed to at all, it alone does not warrant a
|
||||||
|
// card in the live view, since the SoC is already displayed at the top
|
||||||
|
void getLiveViewData(JsonVariant& root) const final { }
|
||||||
|
};
|
||||||
@ -18,7 +18,7 @@
|
|||||||
#define MQTT_MAX_HOSTNAME_STRLEN 128
|
#define MQTT_MAX_HOSTNAME_STRLEN 128
|
||||||
#define MQTT_MAX_USERNAME_STRLEN 64
|
#define MQTT_MAX_USERNAME_STRLEN 64
|
||||||
#define MQTT_MAX_PASSWORD_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_LWTVALUE_STRLEN 20
|
||||||
#define MQTT_MAX_CERT_STRLEN 2560
|
#define MQTT_MAX_CERT_STRLEN 2560
|
||||||
|
|
||||||
@ -30,6 +30,15 @@
|
|||||||
|
|
||||||
#define DEV_MAX_MAPPING_NAME_STRLEN 63
|
#define DEV_MAX_MAPPING_NAME_STRLEN 63
|
||||||
|
|
||||||
|
#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
|
||||||
|
|
||||||
struct CHANNEL_CONFIG_T {
|
struct CHANNEL_CONFIG_T {
|
||||||
uint16_t MaxChannelPower;
|
uint16_t MaxChannelPower;
|
||||||
char Name[CHAN_MAX_NAME_STRLEN];
|
char Name[CHAN_MAX_NAME_STRLEN];
|
||||||
@ -51,6 +60,23 @@ struct INVERTER_CONFIG_T {
|
|||||||
CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT];
|
CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct POWERMETER_HTTP_PHASE_CONFIG_T {
|
||||||
|
enum Auth { None, Basic, Digest };
|
||||||
|
enum Unit { Watts = 0, MilliWatts = 1, KiloWatts = 2 };
|
||||||
|
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];
|
||||||
|
Unit PowerUnit;
|
||||||
|
bool SignInverted;
|
||||||
|
};
|
||||||
|
using PowerMeterHttpConfig = struct POWERMETER_HTTP_PHASE_CONFIG_T;
|
||||||
|
|
||||||
struct CONFIG_T {
|
struct CONFIG_T {
|
||||||
struct {
|
struct {
|
||||||
uint32_t Version;
|
uint32_t Version;
|
||||||
@ -86,6 +112,7 @@ struct CONFIG_T {
|
|||||||
struct {
|
struct {
|
||||||
bool Enabled;
|
bool Enabled;
|
||||||
char Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1];
|
char Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1];
|
||||||
|
bool VerboseLogging;
|
||||||
uint32_t Port;
|
uint32_t Port;
|
||||||
char Username[MQTT_MAX_USERNAME_STRLEN + 1];
|
char Username[MQTT_MAX_USERNAME_STRLEN + 1];
|
||||||
char Password[MQTT_MAX_PASSWORD_STRLEN + 1];
|
char Password[MQTT_MAX_PASSWORD_STRLEN + 1];
|
||||||
@ -129,6 +156,7 @@ struct CONFIG_T {
|
|||||||
uint32_t Frequency;
|
uint32_t Frequency;
|
||||||
uint8_t CountryMode;
|
uint8_t CountryMode;
|
||||||
} Cmt;
|
} Cmt;
|
||||||
|
bool VerboseLogging;
|
||||||
} Dtu;
|
} Dtu;
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
@ -152,6 +180,81 @@ struct CONFIG_T {
|
|||||||
uint8_t Brightness;
|
uint8_t Brightness;
|
||||||
} Led_Single[PINMAPPING_LED_COUNT];
|
} 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;
|
||||||
|
PowerMeterHttpConfig Http_Phase[POWERMETER_MAX_PHASES];
|
||||||
|
} PowerMeter;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
bool Enabled;
|
||||||
|
bool VerboseLogging;
|
||||||
|
bool SolarPassThroughEnabled;
|
||||||
|
uint8_t SolarPassThroughLosses;
|
||||||
|
bool BatteryAlwaysUseAtNight;
|
||||||
|
uint32_t Interval;
|
||||||
|
bool IsInverterBehindPowerMeter;
|
||||||
|
bool IsInverterSolarPowered;
|
||||||
|
uint64_t InverterId;
|
||||||
|
uint8_t InverterChannelId;
|
||||||
|
int32_t TargetPowerConsumption;
|
||||||
|
int32_t TargetPowerConsumptionHysteresis;
|
||||||
|
int32_t LowerPowerLimit;
|
||||||
|
int32_t BaseLoadLimit;
|
||||||
|
int32_t UpperPowerLimit;
|
||||||
|
bool IgnoreSoc;
|
||||||
|
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 MqttSocTopic[MQTT_MAX_TOPIC_STRLEN + 1];
|
||||||
|
char MqttVoltageTopic[MQTT_MAX_TOPIC_STRLEN + 1];
|
||||||
|
} Battery;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
bool Enabled;
|
||||||
|
bool VerboseLogging;
|
||||||
|
uint32_t CAN_Controller_Frequency;
|
||||||
|
bool Auto_Power_Enabled;
|
||||||
|
bool Auto_Power_BatterySoC_Limits_Enabled;
|
||||||
|
bool Emergency_Charge_Enabled;
|
||||||
|
float Auto_Power_Voltage_Limit;
|
||||||
|
float Auto_Power_Enable_Voltage_Limit;
|
||||||
|
float Auto_Power_Lower_Power_Limit;
|
||||||
|
float Auto_Power_Upper_Power_Limit;
|
||||||
|
uint8_t Auto_Power_Stop_BatterySoC_Threshold;
|
||||||
|
float Auto_Power_Target_Power_Consumption;
|
||||||
|
} Huawei;
|
||||||
|
|
||||||
|
|
||||||
INVERTER_CONFIG_T Inverter[INV_MAX_COUNT];
|
INVERTER_CONFIG_T Inverter[INV_MAX_COUNT];
|
||||||
char Dev_PinMapping[DEV_MAX_MAPPING_NAME_STRLEN + 1];
|
char Dev_PinMapping[DEV_MAX_MAPPING_NAME_STRLEN + 1];
|
||||||
};
|
};
|
||||||
|
|||||||
34
include/HttpPowerMeter.h
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
#include "Configuration.h"
|
||||||
|
|
||||||
|
using Auth_t = PowerMeterHttpConfig::Auth;
|
||||||
|
using Unit_t = PowerMeterHttpConfig::Unit;
|
||||||
|
|
||||||
|
class HttpPowerMeterClass {
|
||||||
|
public:
|
||||||
|
void init();
|
||||||
|
bool updateValues();
|
||||||
|
float getPower(int8_t phase);
|
||||||
|
char httpPowerMeterError[256];
|
||||||
|
bool queryPhase(int phase, PowerMeterHttpConfig const& config);
|
||||||
|
|
||||||
|
private:
|
||||||
|
float power[POWERMETER_MAX_PHASES];
|
||||||
|
HTTPClient httpClient;
|
||||||
|
String httpResponse;
|
||||||
|
bool httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config);
|
||||||
|
bool extractUrlComponents(String url, String& _protocol, String& _hostname, String& _uri, uint16_t& uint16_t, String& _base64Authorization);
|
||||||
|
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, String jsonPath, Unit_t unit, bool signInverted);
|
||||||
|
void prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue);
|
||||||
|
String sha256(const String& data);
|
||||||
|
};
|
||||||
|
|
||||||
|
extern HttpPowerMeterClass HttpPowerMeter;
|
||||||
158
include/Huawei_can.h
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
// 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() const { return _lastUpdateReceivedMillis; };
|
||||||
|
bool getAutoPowerStatus() const { return _autoPowerEnabled; };
|
||||||
|
uint8_t getMode() const { return _mode; };
|
||||||
|
|
||||||
|
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;
|
||||||
|
bool _batteryEmergencyCharging = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern HuaweiCanClass HuaweiCan;
|
||||||
|
extern HuaweiCanCommClass HuaweiCanComm;
|
||||||
79
include/JkBmsController.h
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
#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; }
|
||||||
|
bool usesHwPort2() const final {
|
||||||
|
return ARDUINO_USB_CDC_ON_BOOT != 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
#pragma once
|
||||||
|
|
||||||
#include <AsyncWebSocket.h>
|
#include <AsyncWebSocket.h>
|
||||||
#include <HardwareSerial.h>
|
|
||||||
#include <Stream.h>
|
|
||||||
#include <TaskSchedulerDeclarations.h>
|
#include <TaskSchedulerDeclarations.h>
|
||||||
|
#include <Print.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <vector>
|
||||||
#define BUFFER_SIZE 500
|
#include <unordered_map>
|
||||||
|
#include <queue>
|
||||||
|
|
||||||
class MessageOutputClass : public Print {
|
class MessageOutputClass : public Print {
|
||||||
public:
|
public:
|
||||||
@ -22,13 +23,19 @@ private:
|
|||||||
|
|
||||||
Task _loopTask;
|
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;
|
AsyncWebSocket* _ws = nullptr;
|
||||||
char _buffer[BUFFER_SIZE];
|
|
||||||
uint16_t _buff_pos = 0;
|
|
||||||
uint32_t _lastSend = 0;
|
|
||||||
bool _forceSend = false;
|
|
||||||
|
|
||||||
std::mutex _msgLock;
|
std::mutex _msgLock;
|
||||||
|
|
||||||
|
void serialWrite(message_t const& m);
|
||||||
};
|
};
|
||||||
|
|
||||||
extern MessageOutputClass MessageOutput;
|
extern MessageOutputClass MessageOutput;
|
||||||
|
|||||||
27
include/MqttBattery.h
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#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;
|
||||||
|
String _voltageTopic;
|
||||||
|
std::shared_ptr<MqttBatteryStats> _stats = std::make_shared<MqttBatteryStats>();
|
||||||
|
|
||||||
|
std::optional<float> getFloat(std::string const& src, char const* topic);
|
||||||
|
void onMqttMessageSoC(espMqttClientTypes::MessageProperties const& properties,
|
||||||
|
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total);
|
||||||
|
void onMqttMessageVoltage(espMqttClientTypes::MessageProperties const& properties,
|
||||||
|
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total);
|
||||||
|
};
|
||||||
25
include/MqttHandleBatteryHass.h
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <TaskSchedulerDeclarations.h>
|
||||||
|
|
||||||
|
class MqttHandleBatteryHassClass {
|
||||||
|
public:
|
||||||
|
void init(Scheduler& scheduler);
|
||||||
|
void forceUpdate() { _doPublish = true; }
|
||||||
|
|
||||||
|
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 _doPublish = true;
|
||||||
|
String serial = "0001"; // pseudo-serial, can be replaced in future with real serialnumber
|
||||||
|
};
|
||||||
|
|
||||||
|
extern MqttHandleBatteryHassClass MqttHandleBatteryHass;
|
||||||
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;
|
||||||
43
include/MqttHandlePowerLimiter.h
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// 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();
|
||||||
|
|
||||||
|
enum class MqttPowerLimiterCommand : unsigned {
|
||||||
|
Mode,
|
||||||
|
BatterySoCStartThreshold,
|
||||||
|
BatterySoCStopThreshold,
|
||||||
|
FullSolarPassthroughSoC,
|
||||||
|
VoltageStartThreshold,
|
||||||
|
VoltageStopThreshold,
|
||||||
|
FullSolarPassThroughStartVoltage,
|
||||||
|
FullSolarPassThroughStopVoltage
|
||||||
|
};
|
||||||
|
|
||||||
|
void onMqttCmd(MqttPowerLimiterCommand command, 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;
|
||||||
26
include/MqttHandlePowerLimiterHass.h
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <TaskSchedulerDeclarations.h>
|
||||||
|
|
||||||
|
class MqttHandlePowerLimiterHassClass {
|
||||||
|
public:
|
||||||
|
void init(Scheduler& scheduler);
|
||||||
|
void publishConfig();
|
||||||
|
void forceUpdate();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void loop();
|
||||||
|
void publish(const String& subtopic, const String& payload);
|
||||||
|
void publishNumber(const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, const int16_t min, const int16_t max);
|
||||||
|
void publishSelect(const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic);
|
||||||
|
void createDeviceInfo(JsonObject& object);
|
||||||
|
|
||||||
|
Task _loopTask;
|
||||||
|
|
||||||
|
bool _wasConnected = false;
|
||||||
|
bool _updateForced = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern MqttHandlePowerLimiterHassClass MqttHandlePowerLimiterHass;
|
||||||
40
include/MqttHandleVedirect.h
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "VeDirectMpptController.h"
|
||||||
|
#include "Configuration.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <map>
|
||||||
|
#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();
|
||||||
|
std::map<std::string, VeDirectMpptController::data_t> _kvFrames;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
void publish_mppt_data(const VeDirectMpptController::data_t &mpptData,
|
||||||
|
const VeDirectMpptController::data_t &frame) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern MqttHandleVedirectClass MqttHandleVedirect;
|
||||||
33
include/MqttHandleVedirectHass.h
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// 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,
|
||||||
|
const VeDirectMpptController::data_t &mpptData);
|
||||||
|
void publishSensor(const char *caption, const char *icon, const char *subTopic,
|
||||||
|
const char *deviceClass, const char *stateClass,
|
||||||
|
const char *unitOfMeasurement,
|
||||||
|
const VeDirectMpptController::data_t &mpptData);
|
||||||
|
void createDeviceInfo(JsonObject &object,
|
||||||
|
const VeDirectMpptController::data_t &mpptData);
|
||||||
|
|
||||||
|
Task _loopTask;
|
||||||
|
|
||||||
|
bool _wasConnected = false;
|
||||||
|
bool _updateForced = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern MqttHandleVedirectHassClass MqttHandleVedirectHass;
|
||||||
@ -11,6 +11,7 @@ class MqttSettingsClass {
|
|||||||
public:
|
public:
|
||||||
MqttSettingsClass();
|
MqttSettingsClass();
|
||||||
void init();
|
void init();
|
||||||
|
void loop();
|
||||||
void performReconnect();
|
void performReconnect();
|
||||||
bool getConnected();
|
bool getConnected();
|
||||||
void publish(const String& subtopic, const String& payload);
|
void publish(const String& subtopic, const String& payload);
|
||||||
@ -37,6 +38,7 @@ private:
|
|||||||
Ticker _mqttReconnectTimer;
|
Ticker _mqttReconnectTimer;
|
||||||
MqttSubscribeParser _mqttSubscribeParser;
|
MqttSubscribeParser _mqttSubscribeParser;
|
||||||
std::mutex _clientLock;
|
std::mutex _clientLock;
|
||||||
|
bool _verboseLogging = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern MqttSettingsClass MqttSettings;
|
extern MqttSettingsClass MqttSettings;
|
||||||
@ -39,6 +39,25 @@ struct PinMapping_t {
|
|||||||
uint8_t display_cs;
|
uint8_t display_cs;
|
||||||
uint8_t display_reset;
|
uint8_t display_reset;
|
||||||
int8_t led[PINMAPPING_LED_COUNT];
|
int8_t led[PINMAPPING_LED_COUNT];
|
||||||
|
|
||||||
|
// OpenDTU-OnBattery-specific pins below
|
||||||
|
int8_t victron_tx;
|
||||||
|
int8_t victron_rx;
|
||||||
|
int8_t victron_tx2;
|
||||||
|
int8_t victron_rx2;
|
||||||
|
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 powermeter_rx;
|
||||||
|
int8_t powermeter_tx;
|
||||||
|
int8_t powermeter_dere;
|
||||||
};
|
};
|
||||||
|
|
||||||
class PinMappingClass {
|
class PinMappingClass {
|
||||||
@ -50,6 +69,7 @@ public:
|
|||||||
bool isValidNrf24Config() const;
|
bool isValidNrf24Config() const;
|
||||||
bool isValidCmt2300Config() const;
|
bool isValidCmt2300Config() const;
|
||||||
bool isValidEthConfig() const;
|
bool isValidEthConfig() const;
|
||||||
|
bool isValidHuaweiConfig() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
PinMapping_t _pinMapping;
|
PinMapping_t _pinMapping;
|
||||||
|
|||||||
104
include/PowerLimiter.h
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
// 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 <optional>
|
||||||
|
#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
|
||||||
|
|
||||||
|
class PowerLimiterClass {
|
||||||
|
public:
|
||||||
|
enum class Status : unsigned {
|
||||||
|
Initializing,
|
||||||
|
DisabledByConfig,
|
||||||
|
DisabledByMqtt,
|
||||||
|
WaitingForValidTimestamp,
|
||||||
|
PowerMeterPending,
|
||||||
|
InverterInvalid,
|
||||||
|
InverterChanged,
|
||||||
|
InverterOffline,
|
||||||
|
InverterCommandsDisabled,
|
||||||
|
InverterLimitPending,
|
||||||
|
InverterPowerCmdPending,
|
||||||
|
InverterDevInfoPending,
|
||||||
|
InverterStatsPending,
|
||||||
|
CalculatedLimitBelowMinLimit,
|
||||||
|
UnconditionalSolarPassthrough,
|
||||||
|
NoVeDirect,
|
||||||
|
NoEnergy,
|
||||||
|
HuaweiPsu,
|
||||||
|
Stable,
|
||||||
|
};
|
||||||
|
|
||||||
|
void init(Scheduler& scheduler);
|
||||||
|
uint8_t getInverterUpdateTimeouts() const { return _inverterUpdateTimeouts; }
|
||||||
|
uint8_t getPowerLimiterState();
|
||||||
|
int32_t getLastRequestedPowerLimit() { return _lastRequestedPowerLimit; }
|
||||||
|
|
||||||
|
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;
|
||||||
|
bool _shutdownPending = false;
|
||||||
|
std::optional<uint32_t> _oInverterStatsMillis = std::nullopt;
|
||||||
|
std::optional<uint32_t> _oUpdateStartMillis = std::nullopt;
|
||||||
|
std::optional<int32_t> _oTargetPowerLimitWatts = std::nullopt;
|
||||||
|
std::optional<bool> _oTargetPowerState = std::nullopt;
|
||||||
|
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;
|
||||||
|
uint8_t _inverterUpdateTimeouts = 0;
|
||||||
|
|
||||||
|
frozen::string const& getStatusText(Status status);
|
||||||
|
void announceStatus(Status status);
|
||||||
|
bool shutdown(Status status);
|
||||||
|
bool shutdown() { return shutdown(_lastStatus); }
|
||||||
|
float getBatteryVoltage(bool log = false);
|
||||||
|
int32_t inverterPowerDcToAc(std::shared_ptr<InverterAbstract> inverter, int32_t dcPower);
|
||||||
|
void unconditionalSolarPassthrough(std::shared_ptr<InverterAbstract> inverter);
|
||||||
|
bool canUseDirectSolarPower();
|
||||||
|
bool calcPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t solarPower, bool batteryPower);
|
||||||
|
bool updateInverter();
|
||||||
|
bool setNewPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t newPowerLimit);
|
||||||
|
int32_t getSolarPower();
|
||||||
|
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;
|
||||||
76
include/PowerMeter.h
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Configuration.h"
|
||||||
|
#include <espMqttClient.h>
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <map>
|
||||||
|
#include <list>
|
||||||
|
#include <mutex>
|
||||||
|
#include "SDM.h"
|
||||||
|
#include "sml.h"
|
||||||
|
#include <TaskSchedulerDeclarations.h>
|
||||||
|
#include <SoftwareSerial.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const unsigned char OBIS[6];
|
||||||
|
void (*Fn)(double&);
|
||||||
|
float* Arg;
|
||||||
|
} OBISHandler;
|
||||||
|
|
||||||
|
class PowerMeterClass {
|
||||||
|
public:
|
||||||
|
enum class Source : unsigned {
|
||||||
|
MQTT = 0,
|
||||||
|
SDM1PH = 1,
|
||||||
|
SDM3PH = 2,
|
||||||
|
HTTP = 3,
|
||||||
|
SML = 4,
|
||||||
|
SMAHM2 = 5
|
||||||
|
};
|
||||||
|
void init(Scheduler& scheduler);
|
||||||
|
float getPowerTotal(bool forceUpdate = true);
|
||||||
|
uint32_t getLastPowerMeterUpdate();
|
||||||
|
bool isDataValid();
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
mutable std::mutex _mutex;
|
||||||
|
|
||||||
|
std::unique_ptr<SDM> _upSdm = nullptr;
|
||||||
|
std::unique_ptr<SoftwareSerial> _upSmlSerial = nullptr;
|
||||||
|
|
||||||
|
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>();
|
||||||
|
};
|
||||||
36
include/SMA_HM.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Holger-Steffen Stapf
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <TaskSchedulerDeclarations.h>
|
||||||
|
|
||||||
|
class SMA_HMClass {
|
||||||
|
public:
|
||||||
|
void init(Scheduler& scheduler, bool verboseLogging);
|
||||||
|
void loop();
|
||||||
|
void event1();
|
||||||
|
float getPowerTotal() const { return _powerMeterPower; }
|
||||||
|
float getPowerL1() const { return _powerMeterL1; }
|
||||||
|
float getPowerL2() const { return _powerMeterL2; }
|
||||||
|
float getPowerL3() const { return _powerMeterL3; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Soutput(int kanal, int index, int art, int tarif,
|
||||||
|
char const* name, float value, uint32_t timestamp);
|
||||||
|
|
||||||
|
uint8_t* decodeGroup(uint8_t* offset, uint16_t grouplen);
|
||||||
|
|
||||||
|
bool _verboseLogging = false;
|
||||||
|
float _powerMeterPower = 0.0;
|
||||||
|
float _powerMeterL1 = 0.0;
|
||||||
|
float _powerMeterL2 = 0.0;
|
||||||
|
float _powerMeterL3 = 0.0;
|
||||||
|
uint32_t _previousMillis = 0;
|
||||||
|
uint32_t _serial = 0;
|
||||||
|
Task _loopTask;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern SMA_HMClass SMA_HM;
|
||||||
27
include/SerialPortManager.h
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
class SerialPortManagerClass {
|
||||||
|
public:
|
||||||
|
bool allocateMpptPort(int port);
|
||||||
|
bool allocateBatteryPort(int port);
|
||||||
|
void invalidateBatteryPort();
|
||||||
|
void invalidateMpptPorts();
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum Owner {
|
||||||
|
BATTERY,
|
||||||
|
MPPT
|
||||||
|
};
|
||||||
|
|
||||||
|
std::map<uint8_t, Owner> allocatedPorts;
|
||||||
|
|
||||||
|
bool allocatePort(uint8_t port, Owner owner);
|
||||||
|
void invalidate(Owner owner);
|
||||||
|
|
||||||
|
static const char* print(Owner owner);
|
||||||
|
};
|
||||||
|
|
||||||
|
extern SerialPortManagerClass SerialPortManager;
|
||||||
61
include/VictronMppt.h
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "VeDirectMpptController.h"
|
||||||
|
#include "Configuration.h"
|
||||||
|
#include <TaskSchedulerDeclarations.h>
|
||||||
|
|
||||||
|
class VictronMpptClass {
|
||||||
|
public:
|
||||||
|
VictronMpptClass() = default;
|
||||||
|
~VictronMpptClass() = default;
|
||||||
|
|
||||||
|
void init(Scheduler& scheduler);
|
||||||
|
void updateSettings();
|
||||||
|
|
||||||
|
bool isDataValid() const;
|
||||||
|
bool isDataValid(size_t idx) const;
|
||||||
|
|
||||||
|
// returns the data age of all controllers,
|
||||||
|
// i.e, the youngest data's age is returned.
|
||||||
|
uint32_t getDataAgeMillis() const;
|
||||||
|
uint32_t getDataAgeMillis(size_t idx) const;
|
||||||
|
|
||||||
|
size_t controllerAmount() const { return _controllers.size(); }
|
||||||
|
std::optional<VeDirectMpptController::data_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
|
||||||
|
float getYieldTotal() const;
|
||||||
|
|
||||||
|
// sum of today's yield of all MPPT charge controllers in kWh
|
||||||
|
float getYieldDay() const;
|
||||||
|
|
||||||
|
// minimum of all MPPT charge controllers' output voltages in V
|
||||||
|
float getOutputVoltage() 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;
|
||||||
|
|
||||||
|
bool initController(int8_t rx, int8_t tx, bool logging, int hwSerialPort);
|
||||||
|
};
|
||||||
|
|
||||||
|
extern VictronMpptClass VictronMppt;
|
||||||
20
include/VictronSmartShunt.h
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// 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; }
|
||||||
|
bool usesHwPort2() const final {
|
||||||
|
return ARDUINO_USB_CDC_ON_BOOT != 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "WebApi_battery.h"
|
||||||
#include "WebApi_config.h"
|
#include "WebApi_config.h"
|
||||||
#include "WebApi_device.h"
|
#include "WebApi_device.h"
|
||||||
#include "WebApi_devinfo.h"
|
#include "WebApi_devinfo.h"
|
||||||
@ -16,6 +17,8 @@
|
|||||||
#include "WebApi_network.h"
|
#include "WebApi_network.h"
|
||||||
#include "WebApi_ntp.h"
|
#include "WebApi_ntp.h"
|
||||||
#include "WebApi_power.h"
|
#include "WebApi_power.h"
|
||||||
|
#include "WebApi_powermeter.h"
|
||||||
|
#include "WebApi_powerlimiter.h"
|
||||||
#include "WebApi_prometheus.h"
|
#include "WebApi_prometheus.h"
|
||||||
#include "WebApi_security.h"
|
#include "WebApi_security.h"
|
||||||
#include "WebApi_sysstatus.h"
|
#include "WebApi_sysstatus.h"
|
||||||
@ -23,6 +26,11 @@
|
|||||||
#include "WebApi_ws_console.h"
|
#include "WebApi_ws_console.h"
|
||||||
#include "WebApi_ws_live.h"
|
#include "WebApi_ws_live.h"
|
||||||
#include <AsyncJson.h>
|
#include <AsyncJson.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 <ESPAsyncWebServer.h>
|
||||||
#include <TaskSchedulerDeclarations.h>
|
#include <TaskSchedulerDeclarations.h>
|
||||||
|
|
||||||
@ -45,6 +53,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
AsyncWebServer _server;
|
AsyncWebServer _server;
|
||||||
|
|
||||||
|
WebApiBatteryClass _webApiBattery;
|
||||||
WebApiConfigClass _webApiConfig;
|
WebApiConfigClass _webApiConfig;
|
||||||
WebApiDeviceClass _webApiDevice;
|
WebApiDeviceClass _webApiDevice;
|
||||||
WebApiDevInfoClass _webApiDevInfo;
|
WebApiDevInfoClass _webApiDevInfo;
|
||||||
@ -59,12 +68,19 @@ private:
|
|||||||
WebApiNetworkClass _webApiNetwork;
|
WebApiNetworkClass _webApiNetwork;
|
||||||
WebApiNtpClass _webApiNtp;
|
WebApiNtpClass _webApiNtp;
|
||||||
WebApiPowerClass _webApiPower;
|
WebApiPowerClass _webApiPower;
|
||||||
|
WebApiPowerMeterClass _webApiPowerMeter;
|
||||||
|
WebApiPowerLimiterClass _webApiPowerLimiter;
|
||||||
WebApiPrometheusClass _webApiPrometheus;
|
WebApiPrometheusClass _webApiPrometheus;
|
||||||
WebApiSecurityClass _webApiSecurity;
|
WebApiSecurityClass _webApiSecurity;
|
||||||
WebApiSysstatusClass _webApiSysstatus;
|
WebApiSysstatusClass _webApiSysstatus;
|
||||||
WebApiWebappClass _webApiWebapp;
|
WebApiWebappClass _webApiWebapp;
|
||||||
WebApiWsConsoleClass _webApiWsConsole;
|
WebApiWsConsoleClass _webApiWsConsole;
|
||||||
WebApiWsLiveClass _webApiWsLive;
|
WebApiWsLiveClass _webApiWsLive;
|
||||||
|
WebApiWsVedirectLiveClass _webApiWsVedirectLive;
|
||||||
|
WebApiVedirectClass _webApiVedirect;
|
||||||
|
WebApiHuaweiClass _webApiHuaweiClass;
|
||||||
|
WebApiWsHuaweiLiveClass _webApiWsHuaweiLive;
|
||||||
|
WebApiWsBatteryLiveClass _webApiWsBatteryLive;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern WebApiClass WebApi;
|
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 <TaskSchedulerDeclarations.h>
|
||||||
|
#include <AsyncJson.h>
|
||||||
|
|
||||||
|
class WebApiHuaweiClass {
|
||||||
|
public:
|
||||||
|
void init(AsyncWebServer& server, Scheduler& scheduler);
|
||||||
|
void getJsonData(JsonVariant& root);
|
||||||
|
private:
|
||||||
|
void onStatus(AsyncWebServerRequest* request);
|
||||||
|
void onAdminGet(AsyncWebServerRequest* request);
|
||||||
|
void onAdminPost(AsyncWebServerRequest* request);
|
||||||
|
void onPost(AsyncWebServerRequest* request);
|
||||||
|
|
||||||
|
AsyncWebServer* _server;
|
||||||
|
};
|
||||||
17
include/WebApi_battery.h
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
#include <TaskSchedulerDeclarations.h>
|
||||||
|
|
||||||
|
class WebApiBatteryClass {
|
||||||
|
public:
|
||||||
|
void init(AsyncWebServer& server, Scheduler& scheduler);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void onStatus(AsyncWebServerRequest* request);
|
||||||
|
void onAdminGet(AsyncWebServerRequest* request);
|
||||||
|
void onAdminPost(AsyncWebServerRequest* request);
|
||||||
|
|
||||||
|
AsyncWebServer* _server;
|
||||||
|
};
|
||||||
19
include/WebApi_powerlimiter.h
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
#include <TaskSchedulerDeclarations.h>
|
||||||
|
|
||||||
|
|
||||||
|
class WebApiPowerLimiterClass {
|
||||||
|
public:
|
||||||
|
void init(AsyncWebServer& server, Scheduler& scheduler);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void onStatus(AsyncWebServerRequest* request);
|
||||||
|
void onMetaData(AsyncWebServerRequest* request);
|
||||||
|
void onAdminGet(AsyncWebServerRequest* request);
|
||||||
|
void onAdminPost(AsyncWebServerRequest* request);
|
||||||
|
|
||||||
|
AsyncWebServer* _server;
|
||||||
|
};
|
||||||
21
include/WebApi_powermeter.h
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
#include <TaskSchedulerDeclarations.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include "Configuration.h"
|
||||||
|
|
||||||
|
class WebApiPowerMeterClass {
|
||||||
|
public:
|
||||||
|
void init(AsyncWebServer& server, Scheduler& scheduler);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void onStatus(AsyncWebServerRequest* request);
|
||||||
|
void onAdminGet(AsyncWebServerRequest* request);
|
||||||
|
void onAdminPost(AsyncWebServerRequest* request);
|
||||||
|
void decodeJsonPhaseConfig(JsonObject const& json, PowerMeterHttpConfig& config) const;
|
||||||
|
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>
|
||||||
|
#include <TaskSchedulerDeclarations.h>
|
||||||
|
|
||||||
|
|
||||||
|
class WebApiVedirectClass {
|
||||||
|
public:
|
||||||
|
void init(AsyncWebServer& server, Scheduler& scheduler);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void onVedirectStatus(AsyncWebServerRequest* request);
|
||||||
|
void onVedirectAdminGet(AsyncWebServerRequest* request);
|
||||||
|
void onVedirectAdminPost(AsyncWebServerRequest* request);
|
||||||
|
|
||||||
|
AsyncWebServer* _server;
|
||||||
|
};
|
||||||
29
include/WebApi_ws_Huawei.h
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ArduinoJson.h"
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
#include <TaskSchedulerDeclarations.h>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
class WebApiWsHuaweiLiveClass {
|
||||||
|
public:
|
||||||
|
WebApiWsHuaweiLiveClass();
|
||||||
|
void init(AsyncWebServer& server, Scheduler& scheduler);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void generateCommonJsonResponse(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;
|
||||||
|
|
||||||
|
std::mutex _mutex;
|
||||||
|
|
||||||
|
Task _wsCleanupTask;
|
||||||
|
void wsCleanupTaskCb();
|
||||||
|
|
||||||
|
Task _sendDataTask;
|
||||||
|
void sendDataTaskCb();
|
||||||
|
};
|
||||||
32
include/WebApi_ws_battery.h
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ArduinoJson.h"
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
#include <TaskSchedulerDeclarations.h>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
class WebApiWsBatteryLiveClass {
|
||||||
|
public:
|
||||||
|
WebApiWsBatteryLiveClass();
|
||||||
|
void init(AsyncWebServer& server, Scheduler& scheduler);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void generateCommonJsonResponse(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 _lastUpdateCheck = 0;
|
||||||
|
static constexpr uint16_t _responseSize = 1024 + 512;
|
||||||
|
|
||||||
|
std::mutex _mutex;
|
||||||
|
|
||||||
|
Task _wsCleanupTask;
|
||||||
|
void wsCleanupTaskCb();
|
||||||
|
|
||||||
|
Task _sendDataTask;
|
||||||
|
void sendDataTaskCb();
|
||||||
|
};
|
||||||
@ -17,6 +17,9 @@ private:
|
|||||||
static void generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr<InverterAbstract> inv);
|
static void generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr<InverterAbstract> inv);
|
||||||
static void generateCommonJsonResponse(JsonVariant& root);
|
static void generateCommonJsonResponse(JsonVariant& root);
|
||||||
|
|
||||||
|
void generateOnBatteryJsonResponse(JsonVariant& root, bool all);
|
||||||
|
void sendOnBatteryStats();
|
||||||
|
|
||||||
static void addField(JsonObject& root, std::shared_ptr<InverterAbstract> inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, String topic = "");
|
static void addField(JsonObject& root, std::shared_ptr<InverterAbstract> inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, String topic = "");
|
||||||
static void addTotalField(JsonObject& root, const String& name, const float value, const String& unit, const uint8_t digits);
|
static void addTotalField(JsonObject& root, const String& name, const float value, const String& unit, const uint8_t digits);
|
||||||
|
|
||||||
@ -25,6 +28,12 @@ private:
|
|||||||
|
|
||||||
AsyncWebSocket _ws;
|
AsyncWebSocket _ws;
|
||||||
|
|
||||||
|
uint32_t _lastPublishOnBatteryFull = 0;
|
||||||
|
uint32_t _lastPublishVictron = 0;
|
||||||
|
uint32_t _lastPublishHuawei = 0;
|
||||||
|
uint32_t _lastPublishBattery = 0;
|
||||||
|
uint32_t _lastPublishPowerMeter = 0;
|
||||||
|
|
||||||
uint32_t _lastPublishStats[INV_MAX_COUNT] = { 0 };
|
uint32_t _lastPublishStats[INV_MAX_COUNT] = { 0 };
|
||||||
|
|
||||||
std::mutex _mutex;
|
std::mutex _mutex;
|
||||||
|
|||||||
37
include/WebApi_ws_vedirect_live.h
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ArduinoJson.h"
|
||||||
|
#include "Configuration.h"
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
#include <TaskSchedulerDeclarations.h>
|
||||||
|
#include <VeDirectMpptController.h>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
class WebApiWsVedirectLiveClass {
|
||||||
|
public:
|
||||||
|
WebApiWsVedirectLiveClass();
|
||||||
|
void init(AsyncWebServer& server, Scheduler& scheduler);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void generateCommonJsonResponse(JsonVariant& root, bool fullUpdate);
|
||||||
|
static void populateJson(const JsonObject &root, const VeDirectMpptController::data_t &mpptData);
|
||||||
|
void onLivedataStatus(AsyncWebServerRequest* request);
|
||||||
|
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
|
||||||
|
bool hasUpdate(size_t idx);
|
||||||
|
|
||||||
|
AsyncWebServer* _server;
|
||||||
|
AsyncWebSocket _ws;
|
||||||
|
|
||||||
|
uint32_t _lastFullPublish = 0;
|
||||||
|
uint32_t _lastPublish = 0;
|
||||||
|
uint16_t responseSize() const;
|
||||||
|
|
||||||
|
std::mutex _mutex;
|
||||||
|
|
||||||
|
Task _wsCleanupTask;
|
||||||
|
void wsCleanupTaskCb();
|
||||||
|
|
||||||
|
Task _sendDataTask;
|
||||||
|
void sendDataTaskCb();
|
||||||
|
};
|
||||||
@ -108,3 +108,55 @@
|
|||||||
#define LED_BRIGHTNESS 100U
|
#define LED_BRIGHTNESS 100U
|
||||||
|
|
||||||
#define MAX_INVERTER_LIMIT 2250
|
#define MAX_INVERTER_LIMIT 2250
|
||||||
|
|
||||||
|
// values specific to downstream project OpenDTU-OnBattery start here:
|
||||||
|
#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_ALWAYS_USE_AT_NIGHT false
|
||||||
|
#define POWERLIMITER_INTERVAL 10
|
||||||
|
#define POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER true
|
||||||
|
#define POWERLIMITER_IS_INVERTER_SOLAR_POWERED false
|
||||||
|
#define POWERLIMITER_INVERTER_ID 0ULL
|
||||||
|
#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_BASE_LOAD_LIMIT 100
|
||||||
|
#define POWERLIMITER_UPPER_POWER_LIMIT 800
|
||||||
|
#define POWERLIMITER_IGNORE_SOC false
|
||||||
|
#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 HUAWEI_AUTO_POWER_STOP_BATTERYSOC_THRESHOLD 95
|
||||||
|
#define HUAWEI_AUTO_POWER_TARGET_POWER_CONSUMPTION 0
|
||||||
|
|
||||||
|
#define VERBOSE_LOGGING true
|
||||||
|
|||||||
BIN
lib/.DS_Store
vendored
Normal file
BIN
lib/Hoymiles/.DS_Store
vendored
Normal file
@ -269,6 +269,11 @@ void HoymilesClass::setPollInterval(const uint32_t interval)
|
|||||||
_pollInterval = interval;
|
_pollInterval = interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void HoymilesClass::setVerboseLogging(bool verboseLogging)
|
||||||
|
{
|
||||||
|
_verboseLogging = verboseLogging;
|
||||||
|
}
|
||||||
|
|
||||||
void HoymilesClass::setMessageOutput(Print* output)
|
void HoymilesClass::setMessageOutput(Print* output)
|
||||||
{
|
{
|
||||||
_messageOutput = output;
|
_messageOutput = output;
|
||||||
@ -278,3 +283,18 @@ Print* HoymilesClass::getMessageOutput()
|
|||||||
{
|
{
|
||||||
return _messageOutput;
|
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);
|
void setMessageOutput(Print* output);
|
||||||
Print* getMessageOutput();
|
Print* getMessageOutput();
|
||||||
|
Print* getVerboseMessageOutput();
|
||||||
|
|
||||||
std::shared_ptr<InverterAbstract> addInverter(const char* name, const uint64_t serial);
|
std::shared_ptr<InverterAbstract> addInverter(const char* name, const uint64_t serial);
|
||||||
std::shared_ptr<InverterAbstract> getInverterByPos(const uint8_t pos);
|
std::shared_ptr<InverterAbstract> getInverterByPos(const uint8_t pos);
|
||||||
@ -35,6 +36,7 @@ public:
|
|||||||
|
|
||||||
uint32_t PollInterval() const;
|
uint32_t PollInterval() const;
|
||||||
void setPollInterval(const uint32_t interval);
|
void setPollInterval(const uint32_t interval);
|
||||||
|
void setVerboseLogging(bool verboseLogging);
|
||||||
|
|
||||||
bool isAllRadioIdle() const;
|
bool isAllRadioIdle() const;
|
||||||
|
|
||||||
@ -46,6 +48,7 @@ private:
|
|||||||
std::mutex _mutex;
|
std::mutex _mutex;
|
||||||
|
|
||||||
uint32_t _pollInterval = 0;
|
uint32_t _pollInterval = 0;
|
||||||
|
bool _verboseLogging = true;
|
||||||
uint32_t _lastPoll = 0;
|
uint32_t _lastPoll = 0;
|
||||||
|
|
||||||
Print* _messageOutput = &Serial;
|
Print* _messageOutput = &Serial;
|
||||||
|
|||||||
@ -54,7 +54,7 @@ void HoymilesRadio::sendLastPacketAgain()
|
|||||||
void HoymilesRadio::handleReceivedPackage()
|
void HoymilesRadio::handleReceivedPackage()
|
||||||
{
|
{
|
||||||
if (_busyFlag && _rxTimeout.occured()) {
|
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());
|
std::shared_ptr<InverterAbstract> inv = Hoymiles.getInverterBySerial(_commandQueue.front().get()->getTargetAddress());
|
||||||
|
|
||||||
if (nullptr != inv) {
|
if (nullptr != inv) {
|
||||||
@ -117,10 +117,10 @@ void HoymilesRadio::handleReceivedPackage()
|
|||||||
void HoymilesRadio::dumpBuf(const uint8_t buf[], const uint8_t len, const bool appendNewline)
|
void HoymilesRadio::dumpBuf(const uint8_t buf[], const uint8_t len, const bool appendNewline)
|
||||||
{
|
{
|
||||||
for (uint8_t i = 0; i < len; i++) {
|
for (uint8_t i = 0; i < len; i++) {
|
||||||
Hoymiles.getMessageOutput()->printf("%02X ", buf[i]);
|
Hoymiles.getVerboseMessageOutput()->printf("%02X ", buf[i]);
|
||||||
}
|
}
|
||||||
if (appendNewline) {
|
if (appendNewline) {
|
||||||
Hoymiles.getMessageOutput()->println("");
|
Hoymiles.getVerboseMessageOutput()->println("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -126,7 +126,7 @@ void HoymilesRadio_CMT::loop()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_packetReceived) {
|
if (_packetReceived) {
|
||||||
Hoymiles.getMessageOutput()->println("Interrupt received");
|
Hoymiles.getVerboseMessageOutput()->println("Interrupt received");
|
||||||
while (_radio->available()) {
|
while (_radio->available()) {
|
||||||
if (!(_rxBuffer.size() > FRAGMENT_BUFFER_SIZE)) {
|
if (!(_rxBuffer.size() > FRAGMENT_BUFFER_SIZE)) {
|
||||||
fragment_t f;
|
fragment_t f;
|
||||||
@ -165,9 +165,9 @@ void HoymilesRadio_CMT::loop()
|
|||||||
|
|
||||||
if (nullptr != inv) {
|
if (nullptr != inv) {
|
||||||
// Save packet in inverter rx buffer
|
// 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);
|
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);
|
inv->addRxFragment(f.fragment, f.len);
|
||||||
} else {
|
} else {
|
||||||
@ -274,9 +274,9 @@ void HoymilesRadio_CMT::sendEsbPacket(CommandAbstract& cmd)
|
|||||||
cmtSwitchDtuFreq(getInvBootFrequency());
|
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.getCommandName().c_str(), getFrequencyFromChannel(_radio->getChannel()) / 1000000.0);
|
||||||
cmd.dumpDataPayload(Hoymiles.getMessageOutput());
|
cmd.dumpDataPayload(Hoymiles.getVerboseMessageOutput());
|
||||||
|
|
||||||
if (!_radio->write(cmd.getDataPayload(), cmd.getDataSize())) {
|
if (!_radio->write(cmd.getDataPayload(), cmd.getDataSize())) {
|
||||||
Hoymiles.getMessageOutput()->println("TX SPI Timeout");
|
Hoymiles.getMessageOutput()->println("TX SPI Timeout");
|
||||||
|
|||||||
@ -48,7 +48,7 @@ void HoymilesRadio_NRF::loop()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_packetReceived) {
|
if (_packetReceived) {
|
||||||
Hoymiles.getMessageOutput()->println("Interrupt received");
|
Hoymiles.getVerboseMessageOutput()->println("Interrupt received");
|
||||||
while (_radio->available()) {
|
while (_radio->available()) {
|
||||||
if (!(_rxBuffer.size() > FRAGMENT_BUFFER_SIZE)) {
|
if (!(_rxBuffer.size() > FRAGMENT_BUFFER_SIZE)) {
|
||||||
fragment_t f;
|
fragment_t f;
|
||||||
@ -76,9 +76,9 @@ void HoymilesRadio_NRF::loop()
|
|||||||
|
|
||||||
if (nullptr != inv) {
|
if (nullptr != inv) {
|
||||||
// Save packet in inverter rx buffer
|
// 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);
|
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);
|
inv->addRxFragment(f.fragment, f.len);
|
||||||
} else {
|
} else {
|
||||||
@ -183,9 +183,9 @@ void HoymilesRadio_NRF::sendEsbPacket(CommandAbstract& cmd)
|
|||||||
openWritingPipe(s);
|
openWritingPipe(s);
|
||||||
_radio->setRetries(3, 15);
|
_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.getCommandName().c_str(), _radio->getChannel());
|
||||||
cmd.dumpDataPayload(Hoymiles.getMessageOutput());
|
cmd.dumpDataPayload(Hoymiles.getVerboseMessageOutput());
|
||||||
_radio->write(cmd.getDataPayload(), cmd.getDataSize());
|
_radio->write(cmd.getDataPayload(), cmd.getDataSize());
|
||||||
|
|
||||||
_radio->setRetries(0, 0);
|
_radio->setRetries(0, 0);
|
||||||
|
|||||||
@ -127,6 +127,10 @@ bool HM_Abstract::sendActivePowerControlRequest(float limit, const PowerLimitCon
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (CMD_PENDING == SystemConfigPara()->getLastLimitCommandSuccess()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (type == PowerLimitControlType::RelativNonPersistent || type == PowerLimitControlType::RelativPersistent) {
|
if (type == PowerLimitControlType::RelativNonPersistent || type == PowerLimitControlType::RelativPersistent) {
|
||||||
limit = min<float>(100, limit);
|
limit = min<float>(100, limit);
|
||||||
}
|
}
|
||||||
@ -154,6 +158,10 @@ bool HM_Abstract::sendPowerControlRequest(const bool turnOn)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (CMD_PENDING == PowerCommand()->getLastPowerCommandSuccess()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (turnOn) {
|
if (turnOn) {
|
||||||
_powerState = 1;
|
_powerState = 1;
|
||||||
} else {
|
} 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
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
260
lib/VeDirectFrameHandler/VeDirectData.cpp
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
#include "VeDirectData.h"
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This function returns the product id (PID) as readable text.
|
||||||
|
*/
|
||||||
|
frozen::string const& veStruct::getPidAsString() const
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* this map is rendered from [1], which is more recent than [2]. Phoenix
|
||||||
|
* inverters are not included in the map. unfortunately, the documents do
|
||||||
|
* not fully align. PID 0xA07F is only present in [1]. PIDs 0xA048, 0xA110,
|
||||||
|
* and 0xA111 are only present in [2]. PIDs 0xA06D and 0xA078 are rev3 in
|
||||||
|
* [1] but rev2 in [2].
|
||||||
|
*
|
||||||
|
* [1] https://www.victronenergy.com/upload/documents/VE.Direct-Protocol-3.33.pdf
|
||||||
|
* [2] https://www.victronenergy.com/upload/documents/BlueSolar-HEX-protocol.pdf
|
||||||
|
*/
|
||||||
|
static constexpr frozen::map<uint16_t, frozen::string, 105> values = {
|
||||||
|
{ 0x0203, "BMV-700" },
|
||||||
|
{ 0x0204, "BMV-702" },
|
||||||
|
{ 0x0205, "BMV-700H" },
|
||||||
|
{ 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 150|70" },
|
||||||
|
{ 0xA047, "BlueSolar MPPT 150|100" },
|
||||||
|
{ 0xA048, "BlueSolar MPPT 75|50 rev2" },
|
||||||
|
{ 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|100 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" },
|
||||||
|
{ 0xA072, "BlueSolar MPPT 150|45 rev3" },
|
||||||
|
{ 0xA073, "SmartSolar MPPT 150|45 rev3" },
|
||||||
|
{ 0xA074, "SmartSolar MPPT 75|10 rev2" },
|
||||||
|
{ 0xA075, "SmartSolar MPPT 75|15 rev2" },
|
||||||
|
{ 0xA076, "BlueSolar MPPT 100|30 rev3" },
|
||||||
|
{ 0xA077, "BlueSolar MPPT 100|50 rev3" },
|
||||||
|
{ 0xA078, "BlueSolar MPPT 150|35 rev3" },
|
||||||
|
{ 0xA079, "BlueSolar MPPT 75|10 rev2" },
|
||||||
|
{ 0xA07A, "BlueSolar MPPT 75|15 rev2" },
|
||||||
|
{ 0xA07B, "BlueSolar MPPT 100|15 rev2" },
|
||||||
|
{ 0xA07C, "BlueSolar MPPT 75|10 rev3" },
|
||||||
|
{ 0xA07D, "BlueSolar MPPT 75|15 rev3" },
|
||||||
|
{ 0xA07E, "SmartSolar MPPT 100|30 12V" },
|
||||||
|
{ 0xA07F, "All-In-1 SmartSolar MPPT 75|15 12V" },
|
||||||
|
{ 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|70" },
|
||||||
|
{ 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" },
|
||||||
|
{ 0xA111, "SmartSolar MPPT RS 450|200" },
|
||||||
|
{ 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" },
|
||||||
|
{ 0xA117, "BlueSolar MPPT VE.Can 150|100 rev2" },
|
||||||
|
{ 0xA340, "Phoenix Smart IP43 Charger 12|50 (1+1)" },
|
||||||
|
{ 0xA341, "Phoenix Smart IP43 Charger 12|50 (3)" },
|
||||||
|
{ 0xA342, "Phoenix Smart IP43 Charger 24|25 (1+1)" },
|
||||||
|
{ 0xA343, "Phoenix Smart IP43 Charger 24|25 (3)" },
|
||||||
|
{ 0xA344, "Phoenix Smart IP43 Charger 12|30 (1+1)" },
|
||||||
|
{ 0xA345, "Phoenix Smart IP43 Charger 12|30 (3)" },
|
||||||
|
{ 0xA346, "Phoenix Smart IP43 Charger 24|16 (1+1)" },
|
||||||
|
{ 0xA347, "Phoenix Smart IP43 Charger 24|16 (3)" },
|
||||||
|
{ 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, "Smart BuckBoost 12V/12V-50A" },
|
||||||
|
};
|
||||||
|
|
||||||
|
return getAsString(values, productID_PID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This function returns the state of operations (CS) as readable text.
|
||||||
|
*/
|
||||||
|
frozen::string const& 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, currentState_CS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This function returns the state of MPPT (MPPT) as readable text.
|
||||||
|
*/
|
||||||
|
frozen::string const& 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, stateOfTracker_MPPT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This function returns error state (ERR) as readable text.
|
||||||
|
*/
|
||||||
|
frozen::string const& 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, errorCode_ERR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This function returns the off reason (OR) as readable text.
|
||||||
|
*/
|
||||||
|
frozen::string const& 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, offReason_OR);
|
||||||
|
}
|
||||||
|
|
||||||
|
frozen::string const& VeDirectHexData::getResponseAsString() const
|
||||||
|
{
|
||||||
|
using Response = VeDirectHexResponse;
|
||||||
|
static constexpr frozen::map<Response, frozen::string, 7> values = {
|
||||||
|
{ Response::DONE, "Done" },
|
||||||
|
{ Response::UNKNOWN, "Unknown" },
|
||||||
|
{ Response::ERROR, "Error" },
|
||||||
|
{ Response::PING, "Ping" },
|
||||||
|
{ Response::GET, "Get" },
|
||||||
|
{ Response::SET, "Set" },
|
||||||
|
{ Response::ASYNC, "Async" }
|
||||||
|
};
|
||||||
|
|
||||||
|
return getAsString(values, rsp);
|
||||||
|
}
|
||||||
|
|
||||||
|
frozen::string const& VeDirectHexData::getRegisterAsString() const
|
||||||
|
{
|
||||||
|
using Register = VeDirectHexRegister;
|
||||||
|
static constexpr frozen::map<Register, frozen::string, 11> values = {
|
||||||
|
{ Register::DeviceMode, "Device Mode" },
|
||||||
|
{ Register::DeviceState, "Device State" },
|
||||||
|
{ Register::RemoteControlUsed, "Remote Control Used" },
|
||||||
|
{ Register::PanelVoltage, "Panel Voltage" },
|
||||||
|
{ Register::ChargerVoltage, "Charger Voltage" },
|
||||||
|
{ Register::NetworkTotalDcInputPower, "Network Total DC Input Power" },
|
||||||
|
{ Register::ChargeControllerTemperature, "Charger Controller Temperature" },
|
||||||
|
{ Register::SmartBatterySenseTemperature, "Smart Battery Sense Temperature" },
|
||||||
|
{ Register::NetworkInfo, "Network Info" },
|
||||||
|
{ Register::NetworkMode, "Network Mode" },
|
||||||
|
{ Register::NetworkStatus, "Network Status" }
|
||||||
|
};
|
||||||
|
|
||||||
|
return getAsString(values, addr);
|
||||||
|
}
|
||||||
139
lib/VeDirectFrameHandler/VeDirectData.h
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <frozen/string.h>
|
||||||
|
#include <frozen/map.h>
|
||||||
|
|
||||||
|
#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
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint16_t productID_PID = 0; // product id
|
||||||
|
char serialNr_SER[VE_MAX_VALUE_LEN]; // serial number
|
||||||
|
char firmwareNr_FW[VE_MAX_VALUE_LEN]; // firmware release number
|
||||||
|
uint32_t batteryVoltage_V_mV = 0; // battery voltage in mV
|
||||||
|
int32_t batteryCurrent_I_mA = 0; // battery current in mA (can be negative)
|
||||||
|
float mpptEfficiency_Percent = 0; // efficiency in percent (calculated, moving average)
|
||||||
|
|
||||||
|
frozen::string const& getPidAsString() const; // product ID as string
|
||||||
|
} veStruct;
|
||||||
|
|
||||||
|
struct veMpptStruct : veStruct {
|
||||||
|
uint8_t stateOfTracker_MPPT; // state of MPP tracker
|
||||||
|
uint16_t panelPower_PPV_W; // panel power in W
|
||||||
|
uint32_t panelVoltage_VPV_mV; // panel voltage in mV
|
||||||
|
uint32_t panelCurrent_mA; // panel current in mA (calculated)
|
||||||
|
int16_t batteryOutputPower_W; // battery output power in W (calculated, can be negative if load output is used)
|
||||||
|
uint32_t loadCurrent_IL_mA; // Load current in mA (Available only for models with a load output)
|
||||||
|
bool loadOutputState_LOAD; // virtual load output state (on if battery voltage reaches upper limit, off if battery reaches lower limit)
|
||||||
|
uint8_t currentState_CS; // current state of operation e.g. OFF or Bulk
|
||||||
|
uint8_t errorCode_ERR; // error code
|
||||||
|
uint32_t offReason_OR; // off reason
|
||||||
|
uint16_t daySequenceNr_HSDS; // day sequence number 1...365
|
||||||
|
uint32_t yieldTotal_H19_Wh; // yield total resetable Wh
|
||||||
|
uint32_t yieldToday_H20_Wh; // yield today Wh
|
||||||
|
uint16_t maxPowerToday_H21_W; // maximum power today W
|
||||||
|
uint32_t yieldYesterday_H22_Wh; // yield yesterday Wh
|
||||||
|
uint16_t maxPowerYesterday_H23_W; // maximum power yesterday W
|
||||||
|
|
||||||
|
// these are values communicated through the HEX protocol. the pair's first
|
||||||
|
// value is the timestamp the respective info was last received. if it is
|
||||||
|
// zero, the value is deemed invalid. the timestamp is reset if no current
|
||||||
|
// value could be retrieved.
|
||||||
|
std::pair<uint32_t, int32_t> MpptTemperatureMilliCelsius;
|
||||||
|
std::pair<uint32_t, int32_t> SmartBatterySenseTemperatureMilliCelsius;
|
||||||
|
std::pair<uint32_t, uint32_t> NetworkTotalDcInputPowerMilliWatts;
|
||||||
|
std::pair<uint32_t, uint8_t> NetworkInfo;
|
||||||
|
std::pair<uint32_t, uint8_t> NetworkMode;
|
||||||
|
std::pair<uint32_t, uint8_t> NetworkStatus;
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
struct veShuntStruct : veStruct {
|
||||||
|
int32_t T; // Battery temperature
|
||||||
|
bool tempPresent; // 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
|
||||||
|
uint16_t alarmReason_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
|
||||||
|
int8_t dcMonitorMode_MON; // DC monitor mode
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class VeDirectHexCommand : uint8_t {
|
||||||
|
ENTER_BOOT = 0x0,
|
||||||
|
PING = 0x1,
|
||||||
|
RSV1 = 0x2,
|
||||||
|
APP_VERSION = 0x3,
|
||||||
|
PRODUCT_ID = 0x4,
|
||||||
|
RSV2 = 0x5,
|
||||||
|
RESTART = 0x6,
|
||||||
|
GET = 0x7,
|
||||||
|
SET = 0x8,
|
||||||
|
RSV3 = 0x9,
|
||||||
|
ASYNC = 0xA,
|
||||||
|
RSV4 = 0xB,
|
||||||
|
RSV5 = 0xC,
|
||||||
|
RSV6 = 0xD,
|
||||||
|
RSV7 = 0xE,
|
||||||
|
RSV8 = 0xF
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class VeDirectHexResponse : uint8_t {
|
||||||
|
DONE = 0x1,
|
||||||
|
UNKNOWN = 0x3,
|
||||||
|
ERROR = 0x4,
|
||||||
|
PING = 0x5,
|
||||||
|
GET = 0x7,
|
||||||
|
SET = 0x8,
|
||||||
|
ASYNC = 0xA
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class VeDirectHexRegister : uint16_t {
|
||||||
|
DeviceMode = 0x0200,
|
||||||
|
DeviceState = 0x0201,
|
||||||
|
RemoteControlUsed = 0x0202,
|
||||||
|
PanelVoltage = 0xEDBB,
|
||||||
|
ChargerVoltage = 0xEDD5,
|
||||||
|
NetworkTotalDcInputPower = 0x2027,
|
||||||
|
ChargeControllerTemperature = 0xEDDB,
|
||||||
|
SmartBatterySenseTemperature = 0xEDEC,
|
||||||
|
NetworkInfo = 0x200D,
|
||||||
|
NetworkMode = 0x200E,
|
||||||
|
NetworkStatus = 0x200F,
|
||||||
|
HistoryTotal = 0x104F,
|
||||||
|
HistoryMPPTD30 = 0x10BE
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VeDirectHexData {
|
||||||
|
VeDirectHexResponse rsp; // hex response code
|
||||||
|
VeDirectHexRegister addr; // register address
|
||||||
|
uint8_t flags; // flags
|
||||||
|
uint32_t value; // integer value of register
|
||||||
|
char text[VE_MAX_HEX_LEN]; // text/string response
|
||||||
|
|
||||||
|
frozen::string const& getResponseAsString() const;
|
||||||
|
frozen::string const& getRegisterAsString() const;
|
||||||
|
};
|
||||||
321
lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
/* 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
|
||||||
|
* 2024.03.08 - 0.4 - adds the ability to send hex commands and disassemble hex messages
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "VeDirectFrameHandler.h"
|
||||||
|
|
||||||
|
// The name of the record that contains the checksum.
|
||||||
|
static constexpr char checksumTagName[] = "CHECKSUM";
|
||||||
|
|
||||||
|
class Silent : public Print {
|
||||||
|
public:
|
||||||
|
size_t write(uint8_t c) final { return 0; }
|
||||||
|
};
|
||||||
|
|
||||||
|
static Silent MessageOutputDummy;
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
VeDirectFrameHandler<T>::VeDirectFrameHandler() :
|
||||||
|
_msgOut(&MessageOutputDummy),
|
||||||
|
_lastUpdate(0),
|
||||||
|
_state(State::IDLE),
|
||||||
|
_checksum(0),
|
||||||
|
_textPointer(0),
|
||||||
|
_hexSize(0),
|
||||||
|
_name(""),
|
||||||
|
_value(""),
|
||||||
|
_debugIn(0),
|
||||||
|
_lastByteMillis(0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
void VeDirectFrameHandler<T>::init(char const* who, int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort)
|
||||||
|
{
|
||||||
|
_vedirectSerial = std::make_unique<HardwareSerial>(hwSerialPort);
|
||||||
|
_vedirectSerial->end(); // make sure the UART will be re-initialized
|
||||||
|
_vedirectSerial->begin(19200, SERIAL_8N1, rx, tx);
|
||||||
|
_vedirectSerial->flush();
|
||||||
|
_canSend = (tx != -1);
|
||||||
|
_msgOut = msgOut;
|
||||||
|
_verboseLogging = verboseLogging;
|
||||||
|
_debugIn = 0;
|
||||||
|
snprintf(_logId, sizeof(_logId), "[VE.Direct %s %d/%d]", who, rx, tx);
|
||||||
|
if (_verboseLogging) { _msgOut->printf("%s init complete\r\n", _logId); }
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
void VeDirectFrameHandler<T>::dumpDebugBuffer() {
|
||||||
|
_msgOut->printf("%s serial input (%d Bytes):", _logId, _debugIn);
|
||||||
|
for (int i = 0; i < _debugIn; ++i) {
|
||||||
|
if (i % 16 == 0) {
|
||||||
|
_msgOut->printf("\r\n%s", _logId);
|
||||||
|
}
|
||||||
|
_msgOut->printf(" %02x", _debugBuffer[i]);
|
||||||
|
}
|
||||||
|
_msgOut->println("");
|
||||||
|
_debugIn = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
void VeDirectFrameHandler<T>::reset()
|
||||||
|
{
|
||||||
|
_checksum = 0;
|
||||||
|
_state = State::IDLE;
|
||||||
|
_textData.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
void VeDirectFrameHandler<T>::loop()
|
||||||
|
{
|
||||||
|
while ( _vedirectSerial->available()) {
|
||||||
|
rxData(_vedirectSerial->read());
|
||||||
|
_lastByteMillis = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
// there will never be a large gap between two bytes.
|
||||||
|
// if such a large gap is observed, reset the state machine so it tries
|
||||||
|
// to decode a new frame / hex messages once more data arrives.
|
||||||
|
if ((State::IDLE != _state) && ((millis() - _lastByteMillis) > 500)) {
|
||||||
|
_msgOut->printf("%s Resetting state machine (was %d) after timeout\r\n",
|
||||||
|
_logId, static_cast<unsigned>(_state));
|
||||||
|
if (_verboseLogging) { dumpDebugBuffer(); }
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
template<typename T>
|
||||||
|
void VeDirectFrameHandler<T>::rxData(uint8_t inbyte)
|
||||||
|
{
|
||||||
|
if (_verboseLogging) {
|
||||||
|
_debugBuffer[_debugIn] = inbyte;
|
||||||
|
_debugIn = (_debugIn + 1) % _debugBuffer.size();
|
||||||
|
if (0 == _debugIn) {
|
||||||
|
_msgOut->printf("%s ERROR: debug buffer overrun!\r\n", _logId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( (inbyte == ':') && (_state != State::CHECKSUM) ) {
|
||||||
|
_prevState = _state; //hex frame can interrupt TEXT
|
||||||
|
_state = State::RECORD_HEX;
|
||||||
|
_hexSize = 0;
|
||||||
|
}
|
||||||
|
if (_state != State::RECORD_HEX) {
|
||||||
|
_checksum += inbyte;
|
||||||
|
}
|
||||||
|
inbyte = toupper(inbyte);
|
||||||
|
|
||||||
|
switch(_state) {
|
||||||
|
case State::IDLE:
|
||||||
|
/* wait for \n of the start of an record */
|
||||||
|
switch(inbyte) {
|
||||||
|
case '\n':
|
||||||
|
_state = State::RECORD_BEGIN;
|
||||||
|
break;
|
||||||
|
case '\r': /* Skip */
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case State::RECORD_BEGIN:
|
||||||
|
_textPointer = _name;
|
||||||
|
*_textPointer++ = inbyte;
|
||||||
|
_state = State::RECORD_NAME;
|
||||||
|
break;
|
||||||
|
case State::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 = State::CHECKSUM;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_textPointer = _value; /* Reset value pointer */
|
||||||
|
_state = 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 State::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
|
||||||
|
_textData.push_back({_name, _value});
|
||||||
|
}
|
||||||
|
_state = 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 State::CHECKSUM:
|
||||||
|
{
|
||||||
|
if (_verboseLogging) { dumpDebugBuffer(); }
|
||||||
|
if (_checksum == 0) {
|
||||||
|
for (auto const& event : _textData) {
|
||||||
|
processTextData(event.first, event.second);
|
||||||
|
}
|
||||||
|
_lastUpdate = millis();
|
||||||
|
frameValidEvent();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_msgOut->printf("%s checksum 0x%02x != 0x00, invalid frame\r\n", _logId, _checksum);
|
||||||
|
}
|
||||||
|
reset();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case State::RECORD_HEX:
|
||||||
|
_state = hexRxEvent(inbyte);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This function is called every time a new name/value is successfully parsed. It writes the values to the temporary buffer.
|
||||||
|
*/
|
||||||
|
template<typename T>
|
||||||
|
void VeDirectFrameHandler<T>::processTextData(std::string const& name, std::string const& value) {
|
||||||
|
if (_verboseLogging) {
|
||||||
|
_msgOut->printf("%s Text Data '%s' = '%s'\r\n",
|
||||||
|
_logId, name.c_str(), value.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processTextDataDerived(name, value)) { return; }
|
||||||
|
|
||||||
|
if (name == "PID") {
|
||||||
|
_tmpFrame.productID_PID = strtol(value.c_str(), nullptr, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name == "SER") {
|
||||||
|
strcpy(_tmpFrame.serialNr_SER, value.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name == "FW") {
|
||||||
|
strcpy(_tmpFrame.firmwareNr_FW, value.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name == "V") {
|
||||||
|
_tmpFrame.batteryVoltage_V_mV = atol(value.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name == "I") {
|
||||||
|
_tmpFrame.batteryCurrent_I_mA = atol(value.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_msgOut->printf("%s Unknown text data '%s' (value '%s')\r\n",
|
||||||
|
_logId, name.c_str(), value.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* hexRxEvent
|
||||||
|
* This function records hex answers or async messages
|
||||||
|
*/
|
||||||
|
template<typename T>
|
||||||
|
typename VeDirectFrameHandler<T>::State VeDirectFrameHandler<T>::hexRxEvent(uint8_t inbyte)
|
||||||
|
{
|
||||||
|
State ret = State::RECORD_HEX; // default - continue recording until end of frame
|
||||||
|
|
||||||
|
switch (inbyte) {
|
||||||
|
case '\n':
|
||||||
|
// now we can analyse the hex message
|
||||||
|
_hexBuffer[_hexSize] = '\0';
|
||||||
|
VeDirectHexData data;
|
||||||
|
if (disassembleHexData(data) && !hexDataHandler(data) && _verboseLogging) {
|
||||||
|
_msgOut->printf("%s Unhandled Hex %s Response, addr: 0x%04X (%s), "
|
||||||
|
"value: 0x%08X, flags: 0x%02X\r\n", _logId,
|
||||||
|
data.getResponseAsString().data(),
|
||||||
|
static_cast<unsigned>(data.addr),
|
||||||
|
data.getRegisterAsString().data(),
|
||||||
|
data.value, data.flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
// restore previous state
|
||||||
|
ret=_prevState;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
_hexBuffer[_hexSize++]=inbyte;
|
||||||
|
|
||||||
|
if (_hexSize>=VE_MAX_HEX_LEN) { // oops -buffer overflow - something went wrong, we abort
|
||||||
|
_msgOut->printf("%s hexRx buffer overflow - aborting read\r\n", _logId);
|
||||||
|
_hexSize=0;
|
||||||
|
ret = State::IDLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
bool VeDirectFrameHandler<T>::isDataValid() const
|
||||||
|
{
|
||||||
|
// VE.Direct text frame data is valid if we receive a device serialnumber and
|
||||||
|
// the data is not older as 10 seconds
|
||||||
|
// we accept a glitch where the data is valid for ten seconds when serialNr_SER != "" and (millis() - _lastUpdate) overflows
|
||||||
|
return strlen(_tmpFrame.serialNr_SER) > 0 && (millis() - _lastUpdate) < (10 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
uint32_t VeDirectFrameHandler<T>::getLastUpdate() const
|
||||||
|
{
|
||||||
|
return _lastUpdate;
|
||||||
|
}
|
||||||
92
lib/VeDirectFrameHandler/VeDirectFrameHandler.h
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
/* 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
|
||||||
|
* 2024.03.08 - 0.4 - adds the ability to send hex commands and disassemble hex messages
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <array>
|
||||||
|
#include <memory>
|
||||||
|
#include <utility>
|
||||||
|
#include <deque>
|
||||||
|
#include "VeDirectData.h"
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
class VeDirectFrameHandler {
|
||||||
|
public:
|
||||||
|
virtual void loop(); // main loop to read ve.direct data
|
||||||
|
uint32_t getLastUpdate() const; // timestamp of last successful frame read
|
||||||
|
bool isDataValid() const; // return true if data valid and not outdated
|
||||||
|
T const& getData() const { return _tmpFrame; }
|
||||||
|
bool sendHexCommand(VeDirectHexCommand cmd, VeDirectHexRegister addr, uint32_t value = 0, uint8_t valsize = 0);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
VeDirectFrameHandler();
|
||||||
|
void init(char const* who, int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort);
|
||||||
|
virtual bool hexDataHandler(VeDirectHexData const &data) { return false; } // handles the disassembeled hex response
|
||||||
|
|
||||||
|
bool _verboseLogging;
|
||||||
|
Print* _msgOut;
|
||||||
|
uint32_t _lastUpdate;
|
||||||
|
|
||||||
|
T _tmpFrame;
|
||||||
|
|
||||||
|
bool _canSend;
|
||||||
|
char _logId[32];
|
||||||
|
|
||||||
|
private:
|
||||||
|
void reset();
|
||||||
|
void dumpDebugBuffer();
|
||||||
|
void rxData(uint8_t inbyte); // byte of serial data
|
||||||
|
void processTextData(std::string const& name, std::string const& value);
|
||||||
|
virtual bool processTextDataDerived(std::string const& name, std::string const& value) = 0;
|
||||||
|
virtual void frameValidEvent() { }
|
||||||
|
bool disassembleHexData(VeDirectHexData &data); //return true if disassembling was possible
|
||||||
|
|
||||||
|
std::unique_ptr<HardwareSerial> _vedirectSerial;
|
||||||
|
|
||||||
|
enum class State {
|
||||||
|
IDLE = 1,
|
||||||
|
RECORD_BEGIN = 2,
|
||||||
|
RECORD_NAME = 3,
|
||||||
|
RECORD_VALUE = 4,
|
||||||
|
CHECKSUM = 5,
|
||||||
|
RECORD_HEX = 6
|
||||||
|
};
|
||||||
|
State _state;
|
||||||
|
State _prevState;
|
||||||
|
|
||||||
|
State hexRxEvent(uint8_t inbyte);
|
||||||
|
|
||||||
|
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 _hexBuffer[VE_MAX_HEX_LEN]; // buffer for received hex frames
|
||||||
|
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; // time of last parsed byte
|
||||||
|
|
||||||
|
/**
|
||||||
|
* not every frame contains every value the device is communicating, i.e.,
|
||||||
|
* a set of values can be fragmented across multiple frames. frames can be
|
||||||
|
* invalid. in order to only process data from valid frames, we add data
|
||||||
|
* to this queue and only process it once the frame was found to be valid.
|
||||||
|
* this also handles fragmentation nicely, since there is no need to reset
|
||||||
|
* our data buffer. we simply update the interpreted data from this event
|
||||||
|
* queue, which is fine as we know the source frame was valid.
|
||||||
|
*/
|
||||||
|
std::deque<std::pair<std::string, std::string>> _textData;
|
||||||
|
};
|
||||||
|
|
||||||
|
template class VeDirectFrameHandler<veMpptStruct>;
|
||||||
|
template class VeDirectFrameHandler<veShuntStruct>;
|
||||||
226
lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
/* VeDirectFrame
|
||||||
|
HexHandler.cpp
|
||||||
|
*
|
||||||
|
* Library to read/write from Victron devices using VE.Direct Hex protocol.
|
||||||
|
* Add on to Victron framehandler reference implementation.
|
||||||
|
*
|
||||||
|
* How to use:
|
||||||
|
* 1. Use sendHexCommand() to send hex messages. Use the Victron documentation to find the parameter.
|
||||||
|
* 2. The from class "VeDirectFrameHandler" derived class X must overwrite the function
|
||||||
|
* void VeDirectFrameHandler::hexDataHandler(VeDirectHexData const &data)
|
||||||
|
* to handle the received hex messages. All hex messages will be forwarted to function hexDataHandler()
|
||||||
|
* 3. Analyse the content of data (struct VeDirectHexData) to check if a message fits.
|
||||||
|
*
|
||||||
|
* 2024.03.08 - 0.4 - adds the ability to send hex commands and to parse hex messages
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "VeDirectFrameHandler.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* calcHexFrameCheckSum()
|
||||||
|
* help function to calculate the hex checksum
|
||||||
|
*/
|
||||||
|
#define ascii2hex(v) (v-48-(v>='A'?7:0))
|
||||||
|
#define hex2byte(b) (ascii2hex(*(b)))*16+((ascii2hex(*(b+1))))
|
||||||
|
static uint8_t calcHexFrameCheckSum(const char* buffer, int size) {
|
||||||
|
uint8_t checksum=0x55-ascii2hex(buffer[1]);
|
||||||
|
for (int i=2; i<size; i+=2)
|
||||||
|
checksum -= hex2byte(buffer+i);
|
||||||
|
return (checksum);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* AsciiHexLE2Int()
|
||||||
|
* help function to convert AsciiHex Little Endian to uint32_t
|
||||||
|
* ascii: pointer to Ascii Hex Little Endian data
|
||||||
|
* anz: 1,2,4 or 8 nibble
|
||||||
|
*/
|
||||||
|
static uint32_t AsciiHexLE2Int(const char *ascii, const uint8_t anz) {
|
||||||
|
char help[9] = {};
|
||||||
|
|
||||||
|
// sort from little endian format to normal format
|
||||||
|
switch (anz) {
|
||||||
|
case 1:
|
||||||
|
help[0] = ascii[0];
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
case 4:
|
||||||
|
case 8:
|
||||||
|
for (uint8_t i = 0; i < anz; i += 2) {
|
||||||
|
help[i] = ascii[anz-i-2];
|
||||||
|
help[i+1] = ascii[anz-i-1];
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return (static_cast<uint32_t>(strtoul(help, nullptr, 16)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* disassembleHexData()
|
||||||
|
* analysis the hex message and extract: response, address, flags and value/text
|
||||||
|
* buffer: pointer to message (ascii hex little endian format)
|
||||||
|
* data: disassembeled message
|
||||||
|
* return: true = successful disassembeld, false = hex sum fault or message
|
||||||
|
* do not aligin with VE.Diekt syntax
|
||||||
|
*/
|
||||||
|
template<typename T>
|
||||||
|
bool VeDirectFrameHandler<T>::disassembleHexData(VeDirectHexData &data) {
|
||||||
|
bool state = false;
|
||||||
|
char * buffer = _hexBuffer;
|
||||||
|
auto len = strlen(buffer);
|
||||||
|
|
||||||
|
// reset hex data first
|
||||||
|
data = {};
|
||||||
|
|
||||||
|
if ((len > 3) && (calcHexFrameCheckSum(buffer, len) == 0x00)) {
|
||||||
|
data.rsp = static_cast<VeDirectHexResponse>(AsciiHexLE2Int(buffer+1, 1));
|
||||||
|
|
||||||
|
using Response = VeDirectHexResponse;
|
||||||
|
switch (data.rsp) {
|
||||||
|
case Response::DONE:
|
||||||
|
case Response::ERROR:
|
||||||
|
case Response::PING:
|
||||||
|
case Response::UNKNOWN:
|
||||||
|
strncpy(data.text, buffer+2, len-4);
|
||||||
|
state = true;
|
||||||
|
break;
|
||||||
|
case Response::GET:
|
||||||
|
case Response::SET:
|
||||||
|
case Response::ASYNC:
|
||||||
|
data.addr = static_cast<VeDirectHexRegister>(AsciiHexLE2Int(buffer+2, 4));
|
||||||
|
|
||||||
|
// future option: Up to now we do not use historical data
|
||||||
|
if ((data.addr >= VeDirectHexRegister::HistoryTotal) && (data.addr <= VeDirectHexRegister::HistoryMPPTD30)) {
|
||||||
|
state = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// future option: to analyse the flags here?
|
||||||
|
data.flags = AsciiHexLE2Int(buffer+6, 2);
|
||||||
|
|
||||||
|
if (len == 12) { // 8bit value
|
||||||
|
data.value = AsciiHexLE2Int(buffer+8, 2);
|
||||||
|
state = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len == 14) { // 16bit value
|
||||||
|
data.value = AsciiHexLE2Int(buffer+8, 4);
|
||||||
|
state = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len == 18) { // 32bit value
|
||||||
|
data.value = AsciiHexLE2Int(buffer+8, 8);
|
||||||
|
state = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break; // something went wrong
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state)
|
||||||
|
_msgOut->printf("%s failed to disassemble the hex message: %s\r\n", _logId, buffer);
|
||||||
|
|
||||||
|
return (state);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* uint2toHexLEString()
|
||||||
|
* help function to convert up to 32 bits into little endian hex String
|
||||||
|
* ascii: pointer to Ascii Hex Little Endian data
|
||||||
|
* anz: 1,2,4 or 8 nibble
|
||||||
|
*/
|
||||||
|
static String Int2HexLEString(uint32_t value, uint8_t anz) {
|
||||||
|
char hexchar[] = "0123456789ABCDEF";
|
||||||
|
char help[9] = {};
|
||||||
|
|
||||||
|
switch (anz) {
|
||||||
|
case 1:
|
||||||
|
help[0] = hexchar[(value & 0x0000000F)];
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
case 4:
|
||||||
|
case 8:
|
||||||
|
for (uint8_t i = 0; i < anz; i += 2) {
|
||||||
|
help[i] = hexchar[(value>>((1+1*i)*4)) & 0x0000000F];
|
||||||
|
help[i+1] = hexchar[(value>>((1*i)*4)) & 0x0000000F];
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
;
|
||||||
|
}
|
||||||
|
return String(help);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* sendHexCommand()
|
||||||
|
* send the hex commend after assembling the command string
|
||||||
|
* cmd: command
|
||||||
|
* addr: register address, default 0
|
||||||
|
* value: value to write into a register, default 0
|
||||||
|
* valsize: size of the value, 8, 16 or 32 bit, default 0
|
||||||
|
* return: true = message assembeld and send, false = it was not possible to put the message together
|
||||||
|
* SAMPLE: ping command: sendHexCommand(PING),
|
||||||
|
* read total DC input power sendHexCommand(GET, 0xEDEC)
|
||||||
|
* set Charge current limit 10A sendHexCommand(SET, 0x2015, 64, 16)
|
||||||
|
*
|
||||||
|
* WARNING: some values are stored in non-volatile memory. Continuous writing, for example from a control loop, will
|
||||||
|
* lead to early failure.
|
||||||
|
* On MPPT for example 0xEDE0 - 0xEDFF. Check the Vivtron doc "BlueSolar-HEX-protocol.pdf"
|
||||||
|
*/
|
||||||
|
template<typename T>
|
||||||
|
bool VeDirectFrameHandler<T>::sendHexCommand(VeDirectHexCommand cmd, VeDirectHexRegister addr, uint32_t value, uint8_t valsize) {
|
||||||
|
bool ret = false;
|
||||||
|
uint8_t flags = 0x00; // always 0x00
|
||||||
|
|
||||||
|
String txData = ":" + Int2HexLEString(static_cast<uint32_t>(cmd), 1); // add the command nibble
|
||||||
|
|
||||||
|
using Command = VeDirectHexCommand;
|
||||||
|
switch (cmd) {
|
||||||
|
case Command::PING:
|
||||||
|
case Command::APP_VERSION:
|
||||||
|
case Command::PRODUCT_ID:
|
||||||
|
ret = true;
|
||||||
|
break;
|
||||||
|
case Command::GET:
|
||||||
|
case Command::ASYNC:
|
||||||
|
txData += Int2HexLEString(static_cast<uint16_t>(addr), 4);
|
||||||
|
txData += Int2HexLEString(flags, 2); // add the flags (2 nibble)
|
||||||
|
ret = true;
|
||||||
|
break;
|
||||||
|
case Command::SET:
|
||||||
|
txData += Int2HexLEString(static_cast<uint16_t>(addr), 4);
|
||||||
|
txData += Int2HexLEString(flags, 2); // add the flags (2 nibble)
|
||||||
|
if ((valsize == 8) || (valsize == 16) || (valsize == 32)) {
|
||||||
|
txData += Int2HexLEString(value, valsize/4); // add value (2-8 nibble)
|
||||||
|
ret = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ret = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ret) {
|
||||||
|
// add the checksum (2 nibble)
|
||||||
|
txData += Int2HexLEString(calcHexFrameCheckSum(txData.c_str(), txData.length()), 2);
|
||||||
|
String send = txData + "\n"; // hex command end byte
|
||||||
|
_vedirectSerial->write(send.c_str(), send.length());
|
||||||
|
|
||||||
|
if (_verboseLogging) {
|
||||||
|
auto blen = _vedirectSerial->availableForWrite();
|
||||||
|
_msgOut->printf("%s Sending Hex Command: %s, Free FIFO-Buffer: %u\r\n",
|
||||||
|
_logId, txData.c_str(), blen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ret)
|
||||||
|
_msgOut->printf("%s send hex command fault: %s\r\n", _logId, txData.c_str());
|
||||||
|
|
||||||
|
return (ret);
|
||||||
|
}
|
||||||
257
lib/VeDirectFrameHandler/VeDirectMpptController.cpp
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
/* VeDirectMpptController.cpp
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* 2020.08.20 - 0.0 - ???
|
||||||
|
* 2024.03.18 - 0.1 - add of: - temperature from "Smart Battery Sense" connected over VE.Smart network
|
||||||
|
* - temperature from internal MPPT sensor
|
||||||
|
* - "total DC input power" from MPPT's connected over VE.Smart network
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "VeDirectMpptController.h"
|
||||||
|
|
||||||
|
//#define PROCESS_NETWORK_STATE
|
||||||
|
|
||||||
|
void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort)
|
||||||
|
{
|
||||||
|
VeDirectFrameHandler::init("MPPT", rx, tx, msgOut, verboseLogging, hwSerialPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VeDirectMpptController::processTextDataDerived(std::string const& name, std::string const& value)
|
||||||
|
{
|
||||||
|
if (name == "IL") {
|
||||||
|
_tmpFrame.loadCurrent_IL_mA = atol(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "LOAD") {
|
||||||
|
_tmpFrame.loadOutputState_LOAD = (value == "ON");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "CS") {
|
||||||
|
_tmpFrame.currentState_CS = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "ERR") {
|
||||||
|
_tmpFrame.errorCode_ERR = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "OR") {
|
||||||
|
_tmpFrame.offReason_OR = strtol(value.c_str(), nullptr, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "MPPT") {
|
||||||
|
_tmpFrame.stateOfTracker_MPPT = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "HSDS") {
|
||||||
|
_tmpFrame.daySequenceNr_HSDS = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "VPV") {
|
||||||
|
_tmpFrame.panelVoltage_VPV_mV = atol(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "PPV") {
|
||||||
|
_tmpFrame.panelPower_PPV_W = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H19") {
|
||||||
|
_tmpFrame.yieldTotal_H19_Wh = atol(value.c_str()) * 10;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H20") {
|
||||||
|
_tmpFrame.yieldToday_H20_Wh = atol(value.c_str()) * 10;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H21") {
|
||||||
|
_tmpFrame.maxPowerToday_H21_W = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H22") {
|
||||||
|
_tmpFrame.yieldYesterday_H22_Wh = atol(value.c_str()) * 10;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H23") {
|
||||||
|
_tmpFrame.maxPowerYesterday_H23_W = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* frameValidEvent
|
||||||
|
* This function is called at the end of the received frame.
|
||||||
|
*/
|
||||||
|
void VeDirectMpptController::frameValidEvent() {
|
||||||
|
// power into the battery, (+) means charging, (-) means discharging
|
||||||
|
_tmpFrame.batteryOutputPower_W = static_cast<int16_t>((_tmpFrame.batteryVoltage_V_mV / 1000.0f) * (_tmpFrame.batteryCurrent_I_mA / 1000.0f));
|
||||||
|
|
||||||
|
// calculation of the panel current
|
||||||
|
if ((_tmpFrame.panelVoltage_VPV_mV > 0) && (_tmpFrame.panelPower_PPV_W >= 1)) {
|
||||||
|
_tmpFrame.panelCurrent_mA = static_cast<uint32_t>(_tmpFrame.panelPower_PPV_W * 1000000.0f / _tmpFrame.panelVoltage_VPV_mV);
|
||||||
|
} else {
|
||||||
|
_tmpFrame.panelCurrent_mA = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculation of the MPPT efficiency
|
||||||
|
float totalPower_W = (_tmpFrame.loadCurrent_IL_mA / 1000.0f + _tmpFrame.batteryCurrent_I_mA / 1000.0f) * _tmpFrame.batteryVoltage_V_mV /1000.0f;
|
||||||
|
if (_tmpFrame.panelPower_PPV_W > 0) {
|
||||||
|
_efficiency.addNumber(totalPower_W * 100.0f / _tmpFrame.panelPower_PPV_W);
|
||||||
|
_tmpFrame.mpptEfficiency_Percent = _efficiency.getAverage();
|
||||||
|
} else {
|
||||||
|
_tmpFrame.mpptEfficiency_Percent = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_canSend) { return; }
|
||||||
|
|
||||||
|
// Copy from the "VE.Direct Protocol" documentation
|
||||||
|
// For firmware version v1.52 and below, when no VE.Direct queries are sent to the device, the
|
||||||
|
// charger periodically sends human readable (TEXT) data to the serial port. For firmware
|
||||||
|
// versions v1.53 and above, the charger always periodically sends TEXT data to the serial port.
|
||||||
|
// --> We just use hex commandes for firmware >= 1.53 to keep text messages alive
|
||||||
|
if (atoi(_tmpFrame.firmwareNr_FW) < 153) { return; }
|
||||||
|
|
||||||
|
using Command = VeDirectHexCommand;
|
||||||
|
using Register = VeDirectHexRegister;
|
||||||
|
|
||||||
|
sendHexCommand(Command::GET, Register::ChargeControllerTemperature);
|
||||||
|
sendHexCommand(Command::GET, Register::SmartBatterySenseTemperature);
|
||||||
|
sendHexCommand(Command::GET, Register::NetworkTotalDcInputPower);
|
||||||
|
|
||||||
|
#ifdef PROCESS_NETWORK_STATE
|
||||||
|
sendHexCommand(Command::GET, Register::NetworkInfo);
|
||||||
|
sendHexCommand(Command::GET, Register::NetworkMode);
|
||||||
|
sendHexCommand(Command::GET, Register::NetworkStatus);
|
||||||
|
#endif // PROCESS_NETWORK_STATE
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void VeDirectMpptController::loop()
|
||||||
|
{
|
||||||
|
VeDirectFrameHandler::loop();
|
||||||
|
|
||||||
|
auto resetTimestamp = [this](auto& pair) {
|
||||||
|
if (pair.first > 0 && (millis() - pair.first) > (10 * 1000)) {
|
||||||
|
pair.first = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
resetTimestamp(_tmpFrame.MpptTemperatureMilliCelsius);
|
||||||
|
resetTimestamp(_tmpFrame.SmartBatterySenseTemperatureMilliCelsius);
|
||||||
|
resetTimestamp(_tmpFrame.NetworkTotalDcInputPowerMilliWatts);
|
||||||
|
|
||||||
|
#ifdef PROCESS_NETWORK_STATE
|
||||||
|
resetTimestamp(_tmpFrame.NetworkInfo);
|
||||||
|
resetTimestamp(_tmpFrame.NetworkMode);
|
||||||
|
resetTimestamp(_tmpFrame.NetworkStatus);
|
||||||
|
#endif // PROCESS_NETWORK_STATE
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* hexDataHandler()
|
||||||
|
* analyse the content of VE.Direct hex messages
|
||||||
|
* Handels the received hex data from the MPPT
|
||||||
|
*/
|
||||||
|
bool VeDirectMpptController::hexDataHandler(VeDirectHexData const &data) {
|
||||||
|
if (data.rsp != VeDirectHexResponse::GET &&
|
||||||
|
data.rsp != VeDirectHexResponse::ASYNC) { return false; }
|
||||||
|
|
||||||
|
auto regLog = static_cast<uint16_t>(data.addr);
|
||||||
|
|
||||||
|
switch (data.addr) {
|
||||||
|
case VeDirectHexRegister::ChargeControllerTemperature:
|
||||||
|
_tmpFrame.MpptTemperatureMilliCelsius =
|
||||||
|
{ millis(), static_cast<int32_t>(data.value) * 10 };
|
||||||
|
|
||||||
|
if (_verboseLogging) {
|
||||||
|
_msgOut->printf("%s Hex Data: MPPT Temperature (0x%04X): %.2f°C\r\n",
|
||||||
|
_logId, regLog,
|
||||||
|
_tmpFrame.MpptTemperatureMilliCelsius.second / 1000.0);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case VeDirectHexRegister::SmartBatterySenseTemperature:
|
||||||
|
if (data.value == 0xFFFF) {
|
||||||
|
if (_verboseLogging) {
|
||||||
|
_msgOut->printf("%s Hex Data: Smart Battery Sense Temperature is not available\r\n", _logId);
|
||||||
|
}
|
||||||
|
return true; // we know what to do with it, and we decided to ignore the value
|
||||||
|
}
|
||||||
|
|
||||||
|
_tmpFrame.SmartBatterySenseTemperatureMilliCelsius =
|
||||||
|
{ millis(), static_cast<int32_t>(data.value) * 10 - 272150 };
|
||||||
|
|
||||||
|
if (_verboseLogging) {
|
||||||
|
_msgOut->printf("%s Hex Data: Smart Battery Sense Temperature (0x%04X): %.2f°C\r\n",
|
||||||
|
_logId, regLog,
|
||||||
|
_tmpFrame.SmartBatterySenseTemperatureMilliCelsius.second / 1000.0);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case VeDirectHexRegister::NetworkTotalDcInputPower:
|
||||||
|
if (data.value == 0xFFFFFFFF) {
|
||||||
|
if (_verboseLogging) {
|
||||||
|
_msgOut->printf("%s Hex Data: Network total DC power value "
|
||||||
|
"indicates non-networked controller\r\n", _logId);
|
||||||
|
}
|
||||||
|
_tmpFrame.NetworkTotalDcInputPowerMilliWatts = { 0, 0 };
|
||||||
|
return true; // we know what to do with it, and we decided to ignore the value
|
||||||
|
}
|
||||||
|
|
||||||
|
_tmpFrame.NetworkTotalDcInputPowerMilliWatts =
|
||||||
|
{ millis(), data.value * 10 };
|
||||||
|
|
||||||
|
if (_verboseLogging) {
|
||||||
|
_msgOut->printf("%s Hex Data: Network Total DC Power (0x%04X): %.2fW\r\n",
|
||||||
|
_logId, regLog,
|
||||||
|
_tmpFrame.NetworkTotalDcInputPowerMilliWatts.second / 1000.0);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
#ifdef PROCESS_NETWORK_STATE
|
||||||
|
case VeDirectHexRegister::NetworkInfo:
|
||||||
|
_tmpFrame.NetworkInfo =
|
||||||
|
{ millis(), static_cast<uint8_t>(data.value) };
|
||||||
|
|
||||||
|
if (_verboseLogging) {
|
||||||
|
_msgOut->printf("%s Hex Data: Network Info (0x%04X): 0x%X\r\n",
|
||||||
|
_logId, regLog, data.value);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case VeDirectHexRegister::NetworkMode:
|
||||||
|
_tmpFrame.NetworkMode =
|
||||||
|
{ millis(), static_cast<uint8_t>(data.value) };
|
||||||
|
|
||||||
|
if (_verboseLogging) {
|
||||||
|
_msgOut->printf("%s Hex Data: Network Mode (0x%04X): 0x%X\r\n",
|
||||||
|
_logId, regLog, data.value);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case VeDirectHexRegister::NetworkStatus:
|
||||||
|
_tmpFrame.NetworkStatus =
|
||||||
|
{ millis(), static_cast<uint8_t>(data.value) };
|
||||||
|
|
||||||
|
if (_verboseLogging) {
|
||||||
|
_msgOut->printf("%s Hex Data: Network Status (0x%04X): 0x%X\r\n",
|
||||||
|
_logId, regLog, data.value);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
break;
|
||||||
|
#endif // PROCESS_NETWORK_STATE
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
54
lib/VeDirectFrameHandler/VeDirectMpptController.h
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "VeDirectData.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
float getAverage() const {
|
||||||
|
if (_count == 0) { return 0.0; }
|
||||||
|
return static_cast<float>(_sum) / _count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::array<T, WINDOW_SIZE> _window;
|
||||||
|
T _sum;
|
||||||
|
size_t _index;
|
||||||
|
size_t _count;
|
||||||
|
};
|
||||||
|
|
||||||
|
class VeDirectMpptController : public VeDirectFrameHandler<veMpptStruct> {
|
||||||
|
public:
|
||||||
|
VeDirectMpptController() = default;
|
||||||
|
|
||||||
|
void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort);
|
||||||
|
|
||||||
|
using data_t = veMpptStruct;
|
||||||
|
|
||||||
|
void loop() final;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool hexDataHandler(VeDirectHexData const &data) final;
|
||||||
|
bool processTextDataDerived(std::string const& name, std::string const& value) final;
|
||||||
|
void frameValidEvent() final;
|
||||||
|
MovingAverage<float, 5> _efficiency;
|
||||||
|
};
|
||||||
125
lib/VeDirectFrameHandler/VeDirectShuntController.cpp
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include "VeDirectShuntController.h"
|
||||||
|
|
||||||
|
VeDirectShuntController VeDirectShunt;
|
||||||
|
|
||||||
|
void VeDirectShuntController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging)
|
||||||
|
{
|
||||||
|
VeDirectFrameHandler::init("SmartShunt", rx, tx, msgOut, verboseLogging,
|
||||||
|
((ARDUINO_USB_CDC_ON_BOOT != 1)?2:0));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VeDirectShuntController::processTextDataDerived(std::string const& name, std::string const& value)
|
||||||
|
{
|
||||||
|
if (name == "T") {
|
||||||
|
_tmpFrame.T = atoi(value.c_str());
|
||||||
|
_tmpFrame.tempPresent = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "P") {
|
||||||
|
_tmpFrame.P = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "CE") {
|
||||||
|
_tmpFrame.CE = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "SOC") {
|
||||||
|
_tmpFrame.SOC = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "TTG") {
|
||||||
|
_tmpFrame.TTG = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "ALARM") {
|
||||||
|
_tmpFrame.ALARM = (value == "ON");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "AR") {
|
||||||
|
_tmpFrame.alarmReason_AR = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H1") {
|
||||||
|
_tmpFrame.H1 = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H2") {
|
||||||
|
_tmpFrame.H2 = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H3") {
|
||||||
|
_tmpFrame.H3 = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H4") {
|
||||||
|
_tmpFrame.H4 = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H5") {
|
||||||
|
_tmpFrame.H5 = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H6") {
|
||||||
|
_tmpFrame.H6 = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H7") {
|
||||||
|
_tmpFrame.H7 = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H8") {
|
||||||
|
_tmpFrame.H8 = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H9") {
|
||||||
|
_tmpFrame.H9 = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H10") {
|
||||||
|
_tmpFrame.H10 = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H11") {
|
||||||
|
_tmpFrame.H11 = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H12") {
|
||||||
|
_tmpFrame.H12 = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H13") {
|
||||||
|
_tmpFrame.H13 = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H14") {
|
||||||
|
_tmpFrame.H14 = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H15") {
|
||||||
|
_tmpFrame.H15 = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H16") {
|
||||||
|
_tmpFrame.H16 = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H17") {
|
||||||
|
_tmpFrame.H17 = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "H18") {
|
||||||
|
_tmpFrame.H18 = atoi(value.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "BMV") {
|
||||||
|
// This field contains a textual description of the BMV model,
|
||||||
|
// for example 602S or 702. It is deprecated, refer to the field PID instead.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name == "MON") {
|
||||||
|
_tmpFrame.dcMonitorMode_MON = static_cast<int8_t>(atoi(value.c_str()));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
19
lib/VeDirectFrameHandler/VeDirectShuntController.h
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "VeDirectData.h"
|
||||||
|
#include "VeDirectFrameHandler.h"
|
||||||
|
|
||||||
|
class VeDirectShuntController : public VeDirectFrameHandler<veShuntStruct> {
|
||||||
|
public:
|
||||||
|
VeDirectShuntController() = default;
|
||||||
|
|
||||||
|
void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging);
|
||||||
|
|
||||||
|
using data_t = veShuntStruct;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool processTextDataDerived(std::string const& name, std::string const& value) final;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern VeDirectShuntController VeDirectShunt;
|
||||||
@ -15,13 +15,22 @@ if missing_pkgs:
|
|||||||
|
|
||||||
from dulwich import porcelain
|
from dulwich import porcelain
|
||||||
|
|
||||||
|
|
||||||
def get_firmware_specifier_build_flag():
|
def get_firmware_specifier_build_flag():
|
||||||
try:
|
try:
|
||||||
build_version = porcelain.describe('.') # '.' refers to the repository root dir
|
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_version = "g0000000"
|
||||||
build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version + "\\\""
|
try:
|
||||||
print ("Firmware Revision: " + build_version)
|
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)
|
return (build_flag)
|
||||||
|
|
||||||
env.Append(
|
env.Append(
|
||||||
|
|||||||
@ -44,6 +44,9 @@ lib_deps =
|
|||||||
olikraus/U8g2 @ ^2.35.17
|
olikraus/U8g2 @ ^2.35.17
|
||||||
buelowp/sunset @ ^1.1.7
|
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
|
||||||
|
|
||||||
extra_scripts =
|
extra_scripts =
|
||||||
pre:pio-scripts/auto_firmware_version.py
|
pre:pio-scripts/auto_firmware_version.py
|
||||||
@ -153,14 +156,23 @@ build_flags = ${env.build_flags}
|
|||||||
[env:d1_mini_esp32]
|
[env:d1_mini_esp32]
|
||||||
board = wemos_d1_mini32
|
board = wemos_d1_mini32
|
||||||
build_flags =
|
build_flags =
|
||||||
${env.build_flags}
|
${env.build_flags}
|
||||||
-DHOYMILES_PIN_MISO=19
|
-DHOYMILES_PIN_MISO=19
|
||||||
-DHOYMILES_PIN_MOSI=23
|
-DHOYMILES_PIN_MOSI=23
|
||||||
-DHOYMILES_PIN_SCLK=18
|
-DHOYMILES_PIN_SCLK=18
|
||||||
-DHOYMILES_PIN_IRQ=16
|
-DHOYMILES_PIN_IRQ=16
|
||||||
-DHOYMILES_PIN_CE=17
|
-DHOYMILES_PIN_CE=17
|
||||||
-DHOYMILES_PIN_CS=5
|
-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]
|
[env:wt32_eth01]
|
||||||
; http://www.wireless-tag.com/portfolio/wt32-eth01/
|
; http://www.wireless-tag.com/portfolio/wt32-eth01/
|
||||||
|
|||||||
@ -30,5 +30,12 @@
|
|||||||
; -DHOYMILES_PIN_IRQ=4
|
; -DHOYMILES_PIN_IRQ=4
|
||||||
; -DHOYMILES_PIN_CE=5
|
; -DHOYMILES_PIN_CE=5
|
||||||
; -DHOYMILES_PIN_CS=6
|
; -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
|
;monitor_port = /dev/ttyACM0
|
||||||
;upload_port = /dev/ttyACM0
|
;upload_port = /dev/ttyACM0
|
||||||
|
|||||||
90
src/Battery.cpp
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#include "Battery.h"
|
||||||
|
#include "MessageOutput.h"
|
||||||
|
#include "PylontechCanReceiver.h"
|
||||||
|
#include "JkBmsController.h"
|
||||||
|
#include "VictronSmartShunt.h"
|
||||||
|
#include "MqttBattery.h"
|
||||||
|
#include "SerialPortManager.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;
|
||||||
|
}
|
||||||
|
SerialPortManager.invalidateBatteryPort();
|
||||||
|
|
||||||
|
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>();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
_upProvider = std::make_unique<JkBms::Controller>();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
_upProvider = std::make_unique<MqttBattery>();
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
_upProvider = std::make_unique<VictronSmartShunt>();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
MessageOutput.printf("Unknown battery provider: %d\r\n", config.Battery.Provider);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(_upProvider->usesHwPort2()) {
|
||||||
|
if (!SerialPortManager.allocateBatteryPort(2)) {
|
||||||
|
MessageOutput.printf("[Battery] Serial port %d already in use. Initialization aborted!\r\n", 2);
|
||||||
|
_upProvider = nullptr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_upProvider->init(verboseLogging)) {
|
||||||
|
SerialPortManager.invalidateBatteryPort();
|
||||||
|
_upProvider = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BatteryClass::loop()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_mutex);
|
||||||
|
|
||||||
|
if (!_upProvider) { return; }
|
||||||
|
|
||||||
|
_upProvider->loop();
|
||||||
|
|
||||||
|
_upProvider->getStats()->mqttLoop();
|
||||||
|
}
|
||||||
439
src/BatteryStats.cpp
Normal file
@ -0,0 +1,439 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#include <vector>
|
||||||
|
#include <algorithm>
|
||||||
|
#include "BatteryStats.h"
|
||||||
|
#include "Configuration.h"
|
||||||
|
#include "MqttSettings.h"
|
||||||
|
#include "JkBmsDataPoints.h"
|
||||||
|
#include "MqttSettings.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BatteryStats::updateAvailable(uint32_t since) const
|
||||||
|
{
|
||||||
|
if (_lastUpdate == 0) { return false; } // no data at all processed yet
|
||||||
|
|
||||||
|
auto constexpr halfOfAllMillis = std::numeric_limits<uint32_t>::max() / 2;
|
||||||
|
return (_lastUpdate - since) < halfOfAllMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BatteryStats::getLiveViewData(JsonVariant& root) const
|
||||||
|
{
|
||||||
|
root["manufacturer"] = _manufacturer;
|
||||||
|
root["data_age"] = getAgeSeconds();
|
||||||
|
|
||||||
|
addLiveViewValue(root, "SoC", _soc, "%", _socPrecision);
|
||||||
|
addLiveViewValue(root, "voltage", _voltage, "V", 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "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 oCurrent = _dataPoints.get<Label::BatteryCurrentMilliAmps>();
|
||||||
|
if (oCurrent.has_value()) {
|
||||||
|
addLiveViewValue(root, "current",
|
||||||
|
static_cast<float>(*oCurrent) / 1000, "A", 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto oVoltage = _dataPoints.get<Label::BatteryVoltageMilliVolt>();
|
||||||
|
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::mqttLoop()
|
||||||
|
{
|
||||||
|
auto& config = Configuration.get();
|
||||||
|
|
||||||
|
if (!MqttSettings.getConnected()
|
||||||
|
|| (millis() - _lastMqttPublish) < (config.Mqtt.PublishInterval * 1000)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mqttPublish();
|
||||||
|
|
||||||
|
_lastMqttPublish = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t BatteryStats::getMqttFullPublishIntervalMs() const
|
||||||
|
{
|
||||||
|
auto& config = Configuration.get();
|
||||||
|
|
||||||
|
// this is the default interval, see mqttLoop(). mqttPublish()
|
||||||
|
// implementations in derived classes may choose to publish some values
|
||||||
|
// with a lower frequency and hence implement this method with a different
|
||||||
|
// return value.
|
||||||
|
return config.Mqtt.PublishInterval * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BatteryStats::mqttPublish() const
|
||||||
|
{
|
||||||
|
MqttSettings.publish("battery/manufacturer", _manufacturer);
|
||||||
|
MqttSettings.publish("battery/dataAge", String(getAgeSeconds()));
|
||||||
|
MqttSettings.publish("battery/stateOfCharge", String(_soc));
|
||||||
|
MqttSettings.publish("battery/voltage", String(_voltage));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PylontechBatteryStats::mqttPublish() const
|
||||||
|
{
|
||||||
|
BatteryStats::mqttPublish();
|
||||||
|
|
||||||
|
MqttSettings.publish("battery/settings/chargeVoltage", String(_chargeVoltage));
|
||||||
|
MqttSettings.publish("battery/settings/chargeCurrentLimitation", String(_chargeCurrentLimitation));
|
||||||
|
MqttSettings.publish("battery/settings/dischargeCurrentLimitation", String(_dischargeCurrentLimitation));
|
||||||
|
MqttSettings.publish("battery/stateOfHealth", String(_stateOfHealth));
|
||||||
|
MqttSettings.publish("battery/current", String(_current));
|
||||||
|
MqttSettings.publish("battery/temperature", String(_temperature));
|
||||||
|
MqttSettings.publish("battery/alarm/overCurrentDischarge", String(_alarmOverCurrentDischarge));
|
||||||
|
MqttSettings.publish("battery/alarm/overCurrentCharge", String(_alarmOverCurrentCharge));
|
||||||
|
MqttSettings.publish("battery/alarm/underTemperature", String(_alarmUnderTemperature));
|
||||||
|
MqttSettings.publish("battery/alarm/overTemperature", String(_alarmOverTemperature));
|
||||||
|
MqttSettings.publish("battery/alarm/underVoltage", String(_alarmUnderVoltage));
|
||||||
|
MqttSettings.publish("battery/alarm/overVoltage", String(_alarmOverVoltage));
|
||||||
|
MqttSettings.publish("battery/alarm/bmsInternal", String(_alarmBmsInternal));
|
||||||
|
MqttSettings.publish("battery/warning/highCurrentDischarge", String(_warningHighCurrentDischarge));
|
||||||
|
MqttSettings.publish("battery/warning/highCurrentCharge", String(_warningHighCurrentCharge));
|
||||||
|
MqttSettings.publish("battery/warning/lowTemperature", String(_warningLowTemperature));
|
||||||
|
MqttSettings.publish("battery/warning/highTemperature", String(_warningHighTemperature));
|
||||||
|
MqttSettings.publish("battery/warning/lowVoltage", String(_warningLowVoltage));
|
||||||
|
MqttSettings.publish("battery/warning/highVoltage", String(_warningHighVoltage));
|
||||||
|
MqttSettings.publish("battery/warning/bmsInternal", String(_warningBmsInternal));
|
||||||
|
MqttSettings.publish("battery/charging/chargeEnabled", String(_chargeEnabled));
|
||||||
|
MqttSettings.publish("battery/charging/dischargeEnabled", String(_dischargeEnabled));
|
||||||
|
MqttSettings.publish("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
|
||||||
|
// NOTE that voltage is also published by the base class, however, we
|
||||||
|
// previously published it only from here using the respective topic.
|
||||||
|
// to avoid a breaking change, we publish the value again using the
|
||||||
|
// "old" topic.
|
||||||
|
};
|
||||||
|
|
||||||
|
// regularly publish all topics regardless of whether or not their value changed
|
||||||
|
bool neverFullyPublished = _lastFullMqttPublish == 0;
|
||||||
|
bool intervalElapsed = _lastFullMqttPublish + getMqttFullPublishIntervalMs() < millis();
|
||||||
|
bool fullPublish = neverFullyPublished || intervalElapsed;
|
||||||
|
|
||||||
|
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()) {
|
||||||
|
// the first twelve chars are expected to be the "User Private Data"
|
||||||
|
// setting (see smartphone app). the remainder is expected be the BMS
|
||||||
|
// name, which can be changed at will using the smartphone app. so
|
||||||
|
// there is not always a "JK" in this string. if there is, we still cut
|
||||||
|
// the string there to avoid possible regressions.
|
||||||
|
_manufacturer = oProductId->substr(12).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()) {
|
||||||
|
auto oSoCDataPoint = dp.getDataPointFor<Label::BatterySoCPercent>();
|
||||||
|
BatteryStats::setSoC(*oSoCValue, 0/*precision*/,
|
||||||
|
oSoCDataPoint->getTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto oVoltage = dp.get<Label::BatteryVoltageMilliVolt>();
|
||||||
|
if (oVoltage.has_value()) {
|
||||||
|
auto oVoltageDataPoint = dp.getDataPointFor<Label::BatteryVoltageMilliVolt>();
|
||||||
|
BatteryStats::setVoltage(static_cast<float>(*oVoltage) / 1000,
|
||||||
|
oVoltageDataPoint->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::data_t const& shuntData) {
|
||||||
|
BatteryStats::setVoltage(shuntData.batteryVoltage_V_mV / 1000.0, millis());
|
||||||
|
BatteryStats::setSoC(static_cast<float>(shuntData.SOC) / 10, 1/*precision*/, millis());
|
||||||
|
|
||||||
|
_current = static_cast<float>(shuntData.batteryCurrent_I_mA) / 1000;
|
||||||
|
_modelName = shuntData.getPidAsString().data();
|
||||||
|
_chargeCycles = shuntData.H4;
|
||||||
|
_timeToGo = shuntData.TTG / 60;
|
||||||
|
_chargedEnergy = static_cast<float>(shuntData.H18) / 100;
|
||||||
|
_dischargedEnergy = static_cast<float>(shuntData.H17) / 100;
|
||||||
|
_manufacturer = "Victron " + _modelName;
|
||||||
|
_temperature = shuntData.T;
|
||||||
|
_tempPresent = shuntData.tempPresent;
|
||||||
|
_instantaneousPower = shuntData.P;
|
||||||
|
_consumedAmpHours = static_cast<float>(shuntData.CE) / 1000;
|
||||||
|
_lastFullCharge = shuntData.H9 / 60;
|
||||||
|
// shuntData.AR is a bitfield, so we need to check each bit individually
|
||||||
|
_alarmLowVoltage = shuntData.alarmReason_AR & 1;
|
||||||
|
_alarmHighVoltage = shuntData.alarmReason_AR & 2;
|
||||||
|
_alarmLowSOC = shuntData.alarmReason_AR & 4;
|
||||||
|
_alarmLowTemperature = shuntData.alarmReason_AR & 32;
|
||||||
|
_alarmHighTemperature = shuntData.alarmReason_AR & 64;
|
||||||
|
|
||||||
|
_lastUpdate = VeDirectShunt.getLastUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VictronSmartShuntStats::getLiveViewData(JsonVariant& root) const {
|
||||||
|
BatteryStats::getLiveViewData(root);
|
||||||
|
|
||||||
|
// values go into the "Status" card of the web application
|
||||||
|
addLiveViewValue(root, "current", _current, "A", 1);
|
||||||
|
addLiveViewValue(root, "chargeCycles", _chargeCycles, "", 0);
|
||||||
|
addLiveViewValue(root, "chargedEnergy", _chargedEnergy, "kWh", 2);
|
||||||
|
addLiveViewValue(root, "dischargedEnergy", _dischargedEnergy, "kWh", 2);
|
||||||
|
addLiveViewValue(root, "instantaneousPower", _instantaneousPower, "W", 0);
|
||||||
|
addLiveViewValue(root, "consumedAmpHours", _consumedAmpHours, "Ah", 3);
|
||||||
|
addLiveViewValue(root, "lastFullCharge", _lastFullCharge, "min", 0);
|
||||||
|
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("battery/current", String(_current));
|
||||||
|
MqttSettings.publish("battery/chargeCycles", String(_chargeCycles));
|
||||||
|
MqttSettings.publish("battery/chargedEnergy", String(_chargedEnergy));
|
||||||
|
MqttSettings.publish("battery/dischargedEnergy", String(_dischargedEnergy));
|
||||||
|
MqttSettings.publish("battery/instantaneousPower", String(_instantaneousPower));
|
||||||
|
MqttSettings.publish("battery/consumedAmpHours", String(_consumedAmpHours));
|
||||||
|
MqttSettings.publish("battery/lastFullCharge", String(_lastFullCharge));
|
||||||
|
}
|
||||||
@ -56,6 +56,7 @@ bool ConfigurationClass::write()
|
|||||||
|
|
||||||
JsonObject mqtt = doc["mqtt"].to<JsonObject>();
|
JsonObject mqtt = doc["mqtt"].to<JsonObject>();
|
||||||
mqtt["enabled"] = config.Mqtt.Enabled;
|
mqtt["enabled"] = config.Mqtt.Enabled;
|
||||||
|
mqtt["verbose_logging"] = config.Mqtt.VerboseLogging;
|
||||||
mqtt["hostname"] = config.Mqtt.Hostname;
|
mqtt["hostname"] = config.Mqtt.Hostname;
|
||||||
mqtt["port"] = config.Mqtt.Port;
|
mqtt["port"] = config.Mqtt.Port;
|
||||||
mqtt["username"] = config.Mqtt.Username;
|
mqtt["username"] = config.Mqtt.Username;
|
||||||
@ -88,6 +89,7 @@ bool ConfigurationClass::write()
|
|||||||
JsonObject dtu = doc["dtu"].to<JsonObject>();
|
JsonObject dtu = doc["dtu"].to<JsonObject>();
|
||||||
dtu["serial"] = config.Dtu.Serial;
|
dtu["serial"] = config.Dtu.Serial;
|
||||||
dtu["poll_interval"] = config.Dtu.PollInterval;
|
dtu["poll_interval"] = config.Dtu.PollInterval;
|
||||||
|
dtu["verbose_logging"] = config.Dtu.VerboseLogging;
|
||||||
dtu["nrf_pa_level"] = config.Dtu.Nrf.PaLevel;
|
dtu["nrf_pa_level"] = config.Dtu.Nrf.PaLevel;
|
||||||
dtu["cmt_pa_level"] = config.Dtu.Cmt.PaLevel;
|
dtu["cmt_pa_level"] = config.Dtu.Cmt.PaLevel;
|
||||||
dtu["cmt_frequency"] = config.Dtu.Cmt.Frequency;
|
dtu["cmt_frequency"] = config.Dtu.Cmt.Frequency;
|
||||||
@ -139,6 +141,90 @@ bool ConfigurationClass::write()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JsonObject vedirect = doc["vedirect"].to<JsonObject>();
|
||||||
|
vedirect["enabled"] = config.Vedirect.Enabled;
|
||||||
|
vedirect["verbose_logging"] = config.Vedirect.VerboseLogging;
|
||||||
|
vedirect["updates_only"] = config.Vedirect.UpdatesOnly;
|
||||||
|
|
||||||
|
JsonObject powermeter = doc["powermeter"].to<JsonObject>();
|
||||||
|
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["http_phases"].to<JsonArray>();
|
||||||
|
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
|
||||||
|
JsonObject powermeter_phase = powermeter_http_phases.add<JsonObject>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
powermeter_phase["unit"] = config.PowerMeter.Http_Phase[i].PowerUnit;
|
||||||
|
powermeter_phase["sign_inverted"] = config.PowerMeter.Http_Phase[i].SignInverted;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject powerlimiter = doc["powerlimiter"].to<JsonObject>();
|
||||||
|
powerlimiter["enabled"] = config.PowerLimiter.Enabled;
|
||||||
|
powerlimiter["verbose_logging"] = config.PowerLimiter.VerboseLogging;
|
||||||
|
powerlimiter["solar_passtrough_enabled"] = config.PowerLimiter.SolarPassThroughEnabled;
|
||||||
|
powerlimiter["solar_passthrough_losses"] = config.PowerLimiter.SolarPassThroughLosses;
|
||||||
|
powerlimiter["battery_always_use_at_night"] = config.PowerLimiter.BatteryAlwaysUseAtNight;
|
||||||
|
powerlimiter["interval"] = config.PowerLimiter.Interval;
|
||||||
|
powerlimiter["is_inverter_behind_powermeter"] = config.PowerLimiter.IsInverterBehindPowerMeter;
|
||||||
|
powerlimiter["is_inverter_solar_powered"] = config.PowerLimiter.IsInverterSolarPowered;
|
||||||
|
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["base_load_limit"] = config.PowerLimiter.BaseLoadLimit;
|
||||||
|
powerlimiter["upper_power_limit"] = config.PowerLimiter.UpperPowerLimit;
|
||||||
|
powerlimiter["ignore_soc"] = config.PowerLimiter.IgnoreSoc;
|
||||||
|
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["battery"].to<JsonObject>();
|
||||||
|
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.MqttSocTopic;
|
||||||
|
battery["mqtt_voltage_topic"] = config.Battery.MqttVoltageTopic;
|
||||||
|
|
||||||
|
JsonObject huawei = doc["huawei"].to<JsonObject>();
|
||||||
|
huawei["enabled"] = config.Huawei.Enabled;
|
||||||
|
huawei["verbose_logging"] = config.Huawei.VerboseLogging;
|
||||||
|
huawei["can_controller_frequency"] = config.Huawei.CAN_Controller_Frequency;
|
||||||
|
huawei["auto_power_enabled"] = config.Huawei.Auto_Power_Enabled;
|
||||||
|
huawei["auto_power_batterysoc_limits_enabled"] = config.Huawei.Auto_Power_BatterySoC_Limits_Enabled;
|
||||||
|
huawei["emergency_charge_enabled"] = config.Huawei.Emergency_Charge_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;
|
||||||
|
huawei["stop_batterysoc_threshold"] = config.Huawei.Auto_Power_Stop_BatterySoC_Threshold;
|
||||||
|
huawei["target_power_consumption"] = config.Huawei.Auto_Power_Target_Power_Consumption;
|
||||||
|
|
||||||
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
|
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -229,6 +315,7 @@ bool ConfigurationClass::read()
|
|||||||
|
|
||||||
JsonObject mqtt = doc["mqtt"];
|
JsonObject mqtt = doc["mqtt"];
|
||||||
config.Mqtt.Enabled = mqtt["enabled"] | MQTT_ENABLED;
|
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));
|
strlcpy(config.Mqtt.Hostname, mqtt["hostname"] | MQTT_HOST, sizeof(config.Mqtt.Hostname));
|
||||||
config.Mqtt.Port = mqtt["port"] | MQTT_PORT;
|
config.Mqtt.Port = mqtt["port"] | MQTT_PORT;
|
||||||
strlcpy(config.Mqtt.Username, mqtt["username"] | MQTT_USER, sizeof(config.Mqtt.Username));
|
strlcpy(config.Mqtt.Username, mqtt["username"] | MQTT_USER, sizeof(config.Mqtt.Username));
|
||||||
@ -261,6 +348,7 @@ bool ConfigurationClass::read()
|
|||||||
JsonObject dtu = doc["dtu"];
|
JsonObject dtu = doc["dtu"];
|
||||||
config.Dtu.Serial = dtu["serial"] | DTU_SERIAL;
|
config.Dtu.Serial = dtu["serial"] | DTU_SERIAL;
|
||||||
config.Dtu.PollInterval = dtu["poll_interval"] | DTU_POLL_INTERVAL;
|
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.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.PaLevel = dtu["cmt_pa_level"] | DTU_CMT_PA_LEVEL;
|
||||||
config.Dtu.Cmt.Frequency = dtu["cmt_frequency"] | DTU_CMT_FREQUENCY;
|
config.Dtu.Cmt.Frequency = dtu["cmt_frequency"] | DTU_CMT_FREQUENCY;
|
||||||
@ -312,6 +400,91 @@ 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"] | PowerMeterHttpConfig::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));
|
||||||
|
config.PowerMeter.Http_Phase[i].PowerUnit = powermeter_phase["unit"] | PowerMeterHttpConfig::Unit::Watts;
|
||||||
|
config.PowerMeter.Http_Phase[i].SignInverted = powermeter_phase["sign_inverted"] | false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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_passtrough_losses"] | POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES; // solar_passthrough_losses was previously saved as solar_passtrough_losses. Be nice and also try mistyped key.
|
||||||
|
config.PowerLimiter.BatteryAlwaysUseAtNight = powerlimiter["battery_always_use_at_night"] | POWERLIMITER_BATTERY_ALWAYS_USE_AT_NIGHT;
|
||||||
|
if (powerlimiter["battery_drain_strategy"].as<uint8_t>() == 1) { config.PowerLimiter.BatteryAlwaysUseAtNight = true; } // convert legacy setting
|
||||||
|
config.PowerLimiter.Interval = powerlimiter["interval"] | POWERLIMITER_INTERVAL;
|
||||||
|
config.PowerLimiter.IsInverterBehindPowerMeter = powerlimiter["is_inverter_behind_powermeter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER;
|
||||||
|
config.PowerLimiter.IsInverterSolarPowered = powerlimiter["is_inverter_solar_powered"] | POWERLIMITER_IS_INVERTER_SOLAR_POWERED;
|
||||||
|
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.BaseLoadLimit = powerlimiter["base_load_limit"] | POWERLIMITER_BASE_LOAD_LIMIT;
|
||||||
|
config.PowerLimiter.UpperPowerLimit = powerlimiter["upper_power_limit"] | POWERLIMITER_UPPER_POWER_LIMIT;
|
||||||
|
config.PowerLimiter.IgnoreSoc = powerlimiter["ignore_soc"] | POWERLIMITER_IGNORE_SOC;
|
||||||
|
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.MqttSocTopic, battery["mqtt_topic"] | "", sizeof(config.Battery.MqttSocTopic));
|
||||||
|
strlcpy(config.Battery.MqttVoltageTopic, battery["mqtt_voltage_topic"] | "", sizeof(config.Battery.MqttVoltageTopic));
|
||||||
|
|
||||||
|
JsonObject huawei = doc["huawei"];
|
||||||
|
config.Huawei.Enabled = huawei["enabled"] | HUAWEI_ENABLED;
|
||||||
|
config.Huawei.VerboseLogging = huawei["verbose_logging"] | VERBOSE_LOGGING;
|
||||||
|
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_BatterySoC_Limits_Enabled = huawei["auto_power_batterysoc_limits_enabled"] | false;
|
||||||
|
config.Huawei.Emergency_Charge_Enabled = huawei["emergency_charge_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;
|
||||||
|
config.Huawei.Auto_Power_Stop_BatterySoC_Threshold = huawei["stop_batterysoc_threshold"] | HUAWEI_AUTO_POWER_STOP_BATTERYSOC_THRESHOLD;
|
||||||
|
config.Huawei.Auto_Power_Target_Power_Consumption = huawei["target_power_consumption"] | HUAWEI_AUTO_POWER_TARGET_POWER_CONSUMPTION;
|
||||||
|
|
||||||
f.close();
|
f.close();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
#include "Display_Graphic.h"
|
#include "Display_Graphic.h"
|
||||||
#include "Datastore.h"
|
#include "Datastore.h"
|
||||||
|
#include "PowerMeter.h"
|
||||||
|
#include "Configuration.h"
|
||||||
#include <NetworkSettings.h>
|
#include <NetworkSettings.h>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
@ -33,6 +35,9 @@ static const char* const i18n_offline[] = { "Offline", "Offline", "Offline" };
|
|||||||
static const char* const i18n_current_power_w[] = { "%.0f W", "%.0f W", "%.0f W" };
|
static const char* const i18n_current_power_w[] = { "%.0f W", "%.0f W", "%.0f W" };
|
||||||
static const char* const i18n_current_power_kw[] = { "%.1f kW", "%.1f kW", "%.1f kW" };
|
static const char* const i18n_current_power_kw[] = { "%.1f kW", "%.1f kW", "%.1f kW" };
|
||||||
|
|
||||||
|
static const char* const i18n_meter_power_w[] = { "grid: %.0f W", "Netz: %.0f W", "reseau: %.0f W" };
|
||||||
|
static const char* const i18n_meter_power_kw[] = { "grid: %.1f kW", "Netz: %.1f kW", "reseau: %.1f kW" };
|
||||||
|
|
||||||
static const char* const i18n_yield_today_wh[] = { "today: %4.0f Wh", "Heute: %4.0f Wh", "auj.: %4.0f Wh" };
|
static const char* const i18n_yield_today_wh[] = { "today: %4.0f Wh", "Heute: %4.0f Wh", "auj.: %4.0f Wh" };
|
||||||
static const char* const i18n_yield_today_kwh[] = { "today: %.1f kWh", "Heute: %.1f kWh", "auj.: %.1f kWh" };
|
static const char* const i18n_yield_today_kwh[] = { "today: %.1f kWh", "Heute: %.1f kWh", "auj.: %.1f kWh" };
|
||||||
|
|
||||||
@ -273,6 +278,32 @@ void DisplayGraphicClass::loop()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the IP and time info in the third line use three-second slots. the
|
||||||
|
// timing for the power meter is chosen such that every third of those
|
||||||
|
// three-second slots is used to NOT overwrite the total inverter energy.
|
||||||
|
bool timing = (_mExtra % 9) >= 3;
|
||||||
|
|
||||||
|
if (showText && Configuration.get().PowerMeter.Enabled && timing && !displayPowerSave) {
|
||||||
|
// erase the third line and print the power meter value instead.
|
||||||
|
// we do it this way to touch as least upstream code as possible
|
||||||
|
// to make maintenance easier.
|
||||||
|
setFont(2);
|
||||||
|
auto lineHeight = _display->getAscent() - _display->getDescent();
|
||||||
|
auto y = _lineOffsets[2] - _display->getAscent();
|
||||||
|
_display->setDrawColor(0);
|
||||||
|
_display->drawBox(0, y, _display->getDisplayWidth(), lineHeight);
|
||||||
|
_display->setDrawColor(1);
|
||||||
|
|
||||||
|
auto acPower = PowerMeter.getPowerTotal(false);
|
||||||
|
if (acPower > 999) {
|
||||||
|
snprintf(_fmtText, sizeof(_fmtText), i18n_meter_power_kw[_display_language], (acPower / 1000));
|
||||||
|
} else {
|
||||||
|
snprintf(_fmtText, sizeof(_fmtText), i18n_meter_power_w[_display_language], acPower);
|
||||||
|
}
|
||||||
|
|
||||||
|
printText(_fmtText, 2);
|
||||||
|
}
|
||||||
|
|
||||||
_display->sendBuffer();
|
_display->sendBuffer();
|
||||||
|
|
||||||
_mExtra++;
|
_mExtra++;
|
||||||
|
|||||||
357
src/HttpPowerMeter.cpp
Normal file
@ -0,0 +1,357 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#include "Configuration.h"
|
||||||
|
#include "HttpPowerMeter.h"
|
||||||
|
#include "MessageOutput.h"
|
||||||
|
#include <WiFiClientSecure.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include "mbedtls/sha256.h"
|
||||||
|
#include <base64.h>
|
||||||
|
#include <memory>
|
||||||
|
#include <ESPmDNS.h>
|
||||||
|
|
||||||
|
void HttpPowerMeterClass::init()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
float HttpPowerMeterClass::getPower(int8_t phase)
|
||||||
|
{
|
||||||
|
if (phase < 1 || phase > POWERMETER_MAX_PHASES) { return 0.0; }
|
||||||
|
|
||||||
|
return power[phase - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HttpPowerMeterClass::updateValues()
|
||||||
|
{
|
||||||
|
auto const& config = Configuration.get();
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
|
||||||
|
auto const& phaseConfig = config.PowerMeter.Http_Phase[i];
|
||||||
|
|
||||||
|
if (!phaseConfig.Enabled) {
|
||||||
|
power[i] = 0.0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i == 0 || config.PowerMeter.HttpIndividualRequests) {
|
||||||
|
if (!queryPhase(i, phaseConfig)) {
|
||||||
|
MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d failed.\r\n", i + 1);
|
||||||
|
MessageOutput.printf("%s\r\n", httpPowerMeterError);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!tryGetFloatValueForPhase(i, phaseConfig.JsonPath, phaseConfig.PowerUnit, phaseConfig.SignInverted)) {
|
||||||
|
MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d (from JSON fetched with Phase 1 config) failed.\r\n", i + 1);
|
||||||
|
MessageOutput.printf("%s\r\n", httpPowerMeterError);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HttpPowerMeterClass::queryPhase(int phase, PowerMeterHttpConfig const& config)
|
||||||
|
{
|
||||||
|
//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;
|
||||||
|
String base64Authorization;
|
||||||
|
uint16_t port;
|
||||||
|
extractUrlComponents(config.Url, protocol, host, uri, port, base64Authorization);
|
||||||
|
|
||||||
|
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(), port, uri, https, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HttpPowerMeterClass::httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config)
|
||||||
|
{
|
||||||
|
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(config.Timeout, config.HeaderKey, config.HeaderValue);
|
||||||
|
if (config.AuthType == Auth_t::Digest) {
|
||||||
|
const char *headers[1] = {"WWW-Authenticate"};
|
||||||
|
httpClient.collectHeaders(headers, 1);
|
||||||
|
} else if (config.AuthType == Auth_t::Basic) {
|
||||||
|
String authString = config.Username;
|
||||||
|
authString += ":";
|
||||||
|
authString += config.Password;
|
||||||
|
String auth = "Basic ";
|
||||||
|
auth.concat(base64::encode(authString));
|
||||||
|
httpClient.addHeader("Authorization", auth);
|
||||||
|
}
|
||||||
|
int httpCode = httpClient.GET();
|
||||||
|
|
||||||
|
if (httpCode == HTTP_CODE_UNAUTHORIZED && config.AuthType == Auth_t::Digest) {
|
||||||
|
// Handle authentication challenge
|
||||||
|
if (httpClient.hasHeader("WWW-Authenticate")) {
|
||||||
|
String authReq = httpClient.header("WWW-Authenticate");
|
||||||
|
String authorization = getDigestAuth(authReq, String(config.Username), String(config.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(config.Timeout, config.HeaderKey, config.HeaderValue);
|
||||||
|
httpClient.addHeader("Authorization", authorization);
|
||||||
|
httpCode = httpClient.GET();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (httpCode <= 0) {
|
||||||
|
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("HTTP Error %s"), httpClient.errorToString(httpCode).c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (httpCode != HTTP_CODE_OK) {
|
||||||
|
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Bad HTTP code: %d"), httpCode);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpResponse = httpClient.getString(); // very unfortunate that we cannot parse WifiClient stream directly
|
||||||
|
httpClient.end();
|
||||||
|
|
||||||
|
// TODO(schlimmchen): postpone calling tryGetFloatValueForPhase, as it
|
||||||
|
// will be called twice for each phase when doing separate requests.
|
||||||
|
return tryGetFloatValueForPhase(phase, config.JsonPath, config.PowerUnit, config.SignInverted);
|
||||||
|
}
|
||||||
|
|
||||||
|
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, String jsonPath, Unit_t unit, bool signInverted)
|
||||||
|
{
|
||||||
|
JsonDocument root;
|
||||||
|
const DeserializationError error = deserializeJson(root, httpResponse);
|
||||||
|
if (error) {
|
||||||
|
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError),
|
||||||
|
PSTR("[HttpPowerMeter] Unable to parse server response as JSON"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr char delimiter = '/';
|
||||||
|
int start = 0;
|
||||||
|
int end = jsonPath.indexOf(delimiter);
|
||||||
|
auto value = root.as<JsonVariantConst>();
|
||||||
|
|
||||||
|
// NOTE: "Because ArduinoJson implements the Null Object Pattern, it is
|
||||||
|
// always safe to read the object: if the key doesn't exist, it returns an
|
||||||
|
// empty value."
|
||||||
|
while (end != -1) {
|
||||||
|
String key = jsonPath.substring(start, end);
|
||||||
|
value = value[key];
|
||||||
|
start = end + 1;
|
||||||
|
end = jsonPath.indexOf(delimiter, start);
|
||||||
|
}
|
||||||
|
|
||||||
|
String lastKey = jsonPath.substring(start);
|
||||||
|
value = value[lastKey];
|
||||||
|
|
||||||
|
if (value.isNull()) {
|
||||||
|
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError),
|
||||||
|
PSTR("[HttpPowerMeter] Unable to find a value for phase %i with JSON path \"%s\""),
|
||||||
|
phase+1, jsonPath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this value is supposed to be in Watts and positive if energy is consumed.
|
||||||
|
power[phase] = value.as<float>();
|
||||||
|
|
||||||
|
switch (unit) {
|
||||||
|
case Unit_t::MilliWatts:
|
||||||
|
power[phase] /= 1000;
|
||||||
|
break;
|
||||||
|
case Unit_t::KiloWatts:
|
||||||
|
power[phase] *= 1000;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signInverted) { power[phase] *= -1; }
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//extract url component as done by httpClient::begin(String url, const char* expectedProtocol) https://github.com/espressif/arduino-esp32/blob/da6325dd7e8e152094b19fe63190907f38ef1ff0/libraries/HTTPClient/src/HTTPClient.cpp#L250
|
||||||
|
bool HttpPowerMeterClass::extractUrlComponents(String url, String& _protocol, String& _host, String& _uri, uint16_t& _port, String& _base64Authorization)
|
||||||
|
{
|
||||||
|
// check for : (http: or https:
|
||||||
|
int index = url.indexOf(':');
|
||||||
|
if(index < 0) {
|
||||||
|
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("failed to parse protocol"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_protocol = url.substring(0, index);
|
||||||
|
|
||||||
|
//initialize port to default values for http or https.
|
||||||
|
//port will be overwritten below in case port is explicitly defined
|
||||||
|
_port = (_protocol == "https" ? 443 : 80);
|
||||||
|
|
||||||
|
url.remove(0, (index + 3)); // remove http:// or https://
|
||||||
|
|
||||||
|
index = url.indexOf('/');
|
||||||
|
if (index == -1) {
|
||||||
|
index = url.length();
|
||||||
|
url += '/';
|
||||||
|
}
|
||||||
|
String host = url.substring(0, index);
|
||||||
|
url.remove(0, index); // remove host part
|
||||||
|
|
||||||
|
// get Authorization
|
||||||
|
index = host.indexOf('@');
|
||||||
|
if(index >= 0) {
|
||||||
|
// auth info
|
||||||
|
String auth = host.substring(0, index);
|
||||||
|
host.remove(0, index + 1); // remove auth part including @
|
||||||
|
_base64Authorization = base64::encode(auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get port
|
||||||
|
index = host.indexOf(':');
|
||||||
|
String the_host;
|
||||||
|
if(index >= 0) {
|
||||||
|
the_host = host.substring(0, index); // hostname
|
||||||
|
host.remove(0, (index + 1)); // remove hostname + :
|
||||||
|
_port = host.toInt(); // get port
|
||||||
|
} else {
|
||||||
|
the_host = host;
|
||||||
|
}
|
||||||
|
|
||||||
|
_host = the_host;
|
||||||
|
_uri = url;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String HttpPowerMeterClass::sha256(const String& data) {
|
||||||
|
uint8_t hash[32];
|
||||||
|
|
||||||
|
mbedtls_sha256_context ctx;
|
||||||
|
mbedtls_sha256_init(&ctx);
|
||||||
|
mbedtls_sha256_starts(&ctx, 0); // select SHA256
|
||||||
|
mbedtls_sha256_update(&ctx, reinterpret_cast<const unsigned char*>(data.c_str()), data.length());
|
||||||
|
mbedtls_sha256_finish(&ctx, hash);
|
||||||
|
mbedtls_sha256_free(&ctx);
|
||||||
|
|
||||||
|
char res[sizeof(hash) * 2 + 1];
|
||||||
|
for (int i = 0; i < sizeof(hash); i++) {
|
||||||
|
snprintf(res + (i*2), sizeof(res) - (i*2), "%02x", hash[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
524
src/Huawei_can.cpp
Normal file
@ -0,0 +1,524 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Malte Schmidt and others
|
||||||
|
*/
|
||||||
|
#include "Battery.h"
|
||||||
|
#include "Huawei_can.h"
|
||||||
|
#include "MessageOutput.h"
|
||||||
|
#include "PowerMeter.h"
|
||||||
|
#include "PowerLimiter.h"
|
||||||
|
#include "Configuration.h"
|
||||||
|
#include "Battery.h"
|
||||||
|
#include <SPI.h>
|
||||||
|
#include <mcp_can.h>
|
||||||
|
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool verboseLogging = config.Huawei.VerboseLogging;
|
||||||
|
|
||||||
|
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) && verboseLogging) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (_mode == HUAWEI_MODE_AUTO_INT || _batteryEmergencyCharging) {
|
||||||
|
|
||||||
|
// Set voltage limit in periodic intervals if we're in auto mode or if emergency battery charge is requested.
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ***********************
|
||||||
|
// Emergency charge
|
||||||
|
// ***********************
|
||||||
|
auto stats = Battery.getStats();
|
||||||
|
if (config.Huawei.Emergency_Charge_Enabled && stats->getImmediateChargingRequest()) {
|
||||||
|
_batteryEmergencyCharging = true;
|
||||||
|
|
||||||
|
// Set output current
|
||||||
|
float efficiency = (_rp.efficiency > 0.5 ? _rp.efficiency : 1.0);
|
||||||
|
float outputCurrent = efficiency * (config.Huawei.Auto_Power_Upper_Power_Limit / _rp.output_voltage);
|
||||||
|
MessageOutput.printf("[HuaweiCanClass::loop] Emergency Charge Output current %f \r\n", outputCurrent);
|
||||||
|
_setValue(outputCurrent, HUAWEI_ONLINE_CURRENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_batteryEmergencyCharging && !stats->getImmediateChargingRequest()) {
|
||||||
|
// Battery request has changed. Set current to 0, wait for PSU to respond and then clear state
|
||||||
|
_setValue(0, HUAWEI_ONLINE_CURRENT);
|
||||||
|
if (_rp.output_current < 1) {
|
||||||
|
_batteryEmergencyCharging = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ***********************
|
||||||
|
// Automatic power control
|
||||||
|
// ***********************
|
||||||
|
|
||||||
|
if (_mode == HUAWEI_MODE_AUTO_INT ) {
|
||||||
|
|
||||||
|
// 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.getInverterBySerial(config.PowerLimiter.InverterId);
|
||||||
|
|
||||||
|
if (inverter == nullptr && config.PowerLimiter.InverterId < INV_MAX_COUNT) {
|
||||||
|
// we previously had an index saved as InverterId. fall back to the
|
||||||
|
// respective positional lookup if InverterId is not a known serial.
|
||||||
|
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());
|
||||||
|
float efficiency = (_rp.efficiency > 0.5 ? _rp.efficiency : 1.0);
|
||||||
|
|
||||||
|
// Powerlimit is the requested output power + permissable Grid consumption factoring in the efficiency factor
|
||||||
|
newPowerLimit += _rp.output_power + config.Huawei.Auto_Power_Target_Power_Consumption / efficiency;
|
||||||
|
|
||||||
|
if (verboseLogging){
|
||||||
|
MessageOutput.printf("[HuaweiCanClass::loop] newPowerLimit: %f, output_power: %f \r\n", newPowerLimit, _rp.output_power);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.Battery.Enabled && config.Huawei.Auto_Power_BatterySoC_Limits_Enabled) {
|
||||||
|
uint8_t _batterySoC = Battery.getStats()->getSoC();
|
||||||
|
if (_batterySoC >= config.Huawei.Auto_Power_Stop_BatterySoC_Threshold) {
|
||||||
|
newPowerLimit = 0;
|
||||||
|
if (verboseLogging) {
|
||||||
|
MessageOutput.printf("[HuaweiCanClass::loop] Current battery SoC %i reached "
|
||||||
|
"stop threshold %i, set newPowerLimit to %f \r\n", _batterySoC,
|
||||||
|
config.Huawei.Auto_Power_Stop_BatterySoC_Threshold, newPowerLimit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate output current
|
||||||
|
float calculatedCurrent = efficiency * (newPowerLimit / _rp.output_voltage);
|
||||||
|
|
||||||
|
// Limit output current to value requested by BMS
|
||||||
|
float permissableCurrent = stats->getChargeCurrentLimitation() - (stats->getChargeCurrent() - _rp.output_current); // BMS current limit - current from other sources
|
||||||
|
float outputCurrent = std::min(calculatedCurrent, permissableCurrent);
|
||||||
|
outputCurrent= outputCurrent > 0 ? outputCurrent : 0;
|
||||||
|
|
||||||
|
if (verboseLogging) {
|
||||||
|
MessageOutput.printf("[HuaweiCanClass::loop] Setting output current to %.2fA. This is the lower value of calculated %.2fA and BMS permissable %.2fA currents\r\n", outputCurrent, calculatedCurrent, permissableCurrent);
|
||||||
|
}
|
||||||
|
_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);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -68,6 +68,9 @@ void InverterSettingsClass::init(Scheduler& scheduler)
|
|||||||
MessageOutput.println(" Setting poll interval... ");
|
MessageOutput.println(" Setting poll interval... ");
|
||||||
Hoymiles.setPollInterval(config.Dtu.PollInterval);
|
Hoymiles.setPollInterval(config.Dtu.PollInterval);
|
||||||
|
|
||||||
|
MessageOutput.println(" Setting verbosity... ");
|
||||||
|
Hoymiles.setVerboseLogging(config.Dtu.VerboseLogging);
|
||||||
|
|
||||||
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
|
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
|
||||||
if (config.Inverter[i].Serial > 0) {
|
if (config.Inverter[i].Serial > 0) {
|
||||||
MessageOutput.print(" Adding inverter: ");
|
MessageOutput.print(" Adding inverter: ");
|
||||||
|
|||||||
431
src/JkBmsController.cpp
Normal file
@ -0,0 +1,431 @@
|
|||||||
|
#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((ARDUINO_USB_CDC_ON_BOOT != 1)?2:0);
|
||||||
|
#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.end(); // make sure the UART will be re-initialized
|
||||||
|
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-2024 Thomas Basler and others
|
* Copyright (C) 2022-2024 Thomas Basler and others
|
||||||
*/
|
*/
|
||||||
|
#include <HardwareSerial.h>
|
||||||
#include "MessageOutput.h"
|
#include "MessageOutput.h"
|
||||||
|
|
||||||
#include <Arduino.h>
|
|
||||||
|
|
||||||
MessageOutputClass MessageOutput;
|
MessageOutputClass MessageOutput;
|
||||||
|
|
||||||
MessageOutputClass::MessageOutputClass()
|
MessageOutputClass::MessageOutputClass()
|
||||||
@ -21,46 +20,97 @@ void MessageOutputClass::init(Scheduler& scheduler)
|
|||||||
|
|
||||||
void MessageOutputClass::register_ws_output(AsyncWebSocket* output)
|
void MessageOutputClass::register_ws_output(AsyncWebSocket* output)
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_msgLock);
|
||||||
|
|
||||||
_ws = output;
|
_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)
|
size_t MessageOutputClass::write(uint8_t c)
|
||||||
{
|
{
|
||||||
if (_buff_pos < BUFFER_SIZE) {
|
std::lock_guard<std::mutex> lock(_msgLock);
|
||||||
std::lock_guard<std::mutex> lock(_msgLock);
|
|
||||||
_buffer[_buff_pos] = c;
|
auto res = _task_messages.emplace(xTaskGetCurrentTaskHandle(), message_t());
|
||||||
_buff_pos++;
|
auto iter = res.first;
|
||||||
} else {
|
auto& message = iter->second;
|
||||||
_forceSend = true;
|
|
||||||
|
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);
|
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()
|
void MessageOutputClass::loop()
|
||||||
{
|
{
|
||||||
// Send data via websocket if either time is over or buffer is full
|
std::lock_guard<std::mutex> lock(_msgLock);
|
||||||
if (_forceSend || (millis() - _lastSend > 1000)) {
|
|
||||||
std::lock_guard<std::mutex> lock(_msgLock);
|
// clean up (possibly filled) buffers of deleted tasks
|
||||||
if (_ws && _buff_pos > 0) {
|
auto map_iter = _task_messages.begin();
|
||||||
_ws->textAll(_buffer, _buff_pos);
|
while (map_iter != _task_messages.end()) {
|
||||||
_buff_pos = 0;
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
114
src/MqttBattery.cpp
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
#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.MqttSocTopic;
|
||||||
|
if (!_socTopic.isEmpty()) {
|
||||||
|
MqttSettings.subscribe(_socTopic, 0/*QoS*/,
|
||||||
|
std::bind(&MqttBattery::onMqttMessageSoC,
|
||||||
|
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' for SoC readings\r\n",
|
||||||
|
_socTopic.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_voltageTopic = config.Battery.MqttVoltageTopic;
|
||||||
|
if (!_voltageTopic.isEmpty()) {
|
||||||
|
MqttSettings.subscribe(_voltageTopic, 0/*QoS*/,
|
||||||
|
std::bind(&MqttBattery::onMqttMessageVoltage,
|
||||||
|
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' for voltage readings\r\n",
|
||||||
|
_voltageTopic.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttBattery::deinit()
|
||||||
|
{
|
||||||
|
if (!_voltageTopic.isEmpty()) {
|
||||||
|
MqttSettings.unsubscribe(_voltageTopic);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_socTopic.isEmpty()) {
|
||||||
|
MqttSettings.unsubscribe(_socTopic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<float> MqttBattery::getFloat(std::string const& src, char const* topic) {
|
||||||
|
float res = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
res = std::stof(src);
|
||||||
|
}
|
||||||
|
catch(std::invalid_argument const& e) {
|
||||||
|
MessageOutput.printf("MqttBattery: Cannot parse payload '%s' in topic '%s' as float\r\n",
|
||||||
|
src.c_str(), topic);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttBattery::onMqttMessageSoC(espMqttClientTypes::MessageProperties const& properties,
|
||||||
|
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total)
|
||||||
|
{
|
||||||
|
auto soc = getFloat(std::string(reinterpret_cast<const char*>(payload), len), topic);
|
||||||
|
if (!soc.has_value()) { return; }
|
||||||
|
|
||||||
|
if (*soc < 0 || *soc > 100) {
|
||||||
|
MessageOutput.printf("MqttBattery: Implausible SoC '%.2f' in topic '%s'\r\n",
|
||||||
|
*soc, topic);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_stats->setSoC(*soc, 0/*precision*/, millis());
|
||||||
|
|
||||||
|
if (_verboseLogging) {
|
||||||
|
MessageOutput.printf("MqttBattery: Updated SoC to %d from '%s'\r\n",
|
||||||
|
static_cast<uint8_t>(*soc), topic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttBattery::onMqttMessageVoltage(espMqttClientTypes::MessageProperties const& properties,
|
||||||
|
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total)
|
||||||
|
{
|
||||||
|
auto voltage = getFloat(std::string(reinterpret_cast<const char*>(payload), len), topic);
|
||||||
|
if (!voltage.has_value()) { return; }
|
||||||
|
|
||||||
|
// since this project is revolving around Hoymiles microinverters, which can
|
||||||
|
// only handle up to 65V of input voltage at best, it is safe to assume that
|
||||||
|
// an even higher voltage is implausible.
|
||||||
|
if (*voltage < 0 || *voltage > 65) {
|
||||||
|
MessageOutput.printf("MqttBattery: Implausible voltage '%.2f' in topic '%s'\r\n",
|
||||||
|
*voltage, topic);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_stats->setVoltage(*voltage, millis());
|
||||||
|
|
||||||
|
if (_verboseLogging) {
|
||||||
|
MessageOutput.printf("MqttBattery: Updated voltage to %.2f from '%s'\r\n",
|
||||||
|
*voltage, topic);
|
||||||
|
}
|
||||||
|
}
|
||||||
225
src/MqttHandlVedirectHass.cpp
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
// 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([this] { loop(); });
|
||||||
|
_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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// device info
|
||||||
|
for (int idx = 0; idx < VictronMppt.controllerAmount(); ++idx) {
|
||||||
|
auto optMpptData = VictronMppt.getData(idx);
|
||||||
|
if (!optMpptData.has_value()) { continue; }
|
||||||
|
|
||||||
|
publishBinarySensor("MPPT load output state", "mdi:export", "LOAD", "ON", "OFF", *optMpptData);
|
||||||
|
publishSensor("MPPT serial number", "mdi:counter", "SER", nullptr, nullptr, nullptr, *optMpptData);
|
||||||
|
publishSensor("MPPT firmware number", "mdi:counter", "FW", nullptr, nullptr, nullptr, *optMpptData);
|
||||||
|
publishSensor("MPPT state of operation", "mdi:wrench", "CS", nullptr, nullptr, nullptr, *optMpptData);
|
||||||
|
publishSensor("MPPT error code", "mdi:bell", "ERR", nullptr, nullptr, nullptr, *optMpptData);
|
||||||
|
publishSensor("MPPT off reason", "mdi:wrench", "OR", nullptr, nullptr, nullptr, *optMpptData);
|
||||||
|
publishSensor("MPPT tracker operation mode", "mdi:wrench", "MPPT", nullptr, nullptr, nullptr, *optMpptData);
|
||||||
|
publishSensor("MPPT Day sequence number (0...364)", "mdi:calendar-month-outline", "HSDS", NULL, "total", "d", *optMpptData);
|
||||||
|
|
||||||
|
// battery info
|
||||||
|
publishSensor("Battery voltage", NULL, "V", "voltage", "measurement", "V", *optMpptData);
|
||||||
|
publishSensor("Battery current", NULL, "I", "current", "measurement", "A", *optMpptData);
|
||||||
|
publishSensor("Battery power (calculated)", NULL, "P", "power", "measurement", "W", *optMpptData);
|
||||||
|
publishSensor("Battery efficiency (calculated)", NULL, "E", NULL, "measurement", "%", *optMpptData);
|
||||||
|
|
||||||
|
// panel info
|
||||||
|
publishSensor("Panel voltage", NULL, "VPV", "voltage", "measurement", "V", *optMpptData);
|
||||||
|
publishSensor("Panel current (calculated)", NULL, "IPV", "current", "measurement", "A", *optMpptData);
|
||||||
|
publishSensor("Panel power", NULL, "PPV", "power", "measurement", "W", *optMpptData);
|
||||||
|
publishSensor("Panel yield total", NULL, "H19", "energy", "total_increasing", "kWh", *optMpptData);
|
||||||
|
publishSensor("Panel yield today", NULL, "H20", "energy", "total", "kWh", *optMpptData);
|
||||||
|
publishSensor("Panel maximum power today", NULL, "H21", "power", "measurement", "W", *optMpptData);
|
||||||
|
publishSensor("Panel yield yesterday", NULL, "H22", "energy", "total", "kWh", *optMpptData);
|
||||||
|
publishSensor("Panel maximum power yesterday", NULL, "H23", "power", "measurement", "W", *optMpptData);
|
||||||
|
|
||||||
|
// optional info, provided only if TX is connected to charge controller
|
||||||
|
if (optMpptData->NetworkTotalDcInputPowerMilliWatts.first != 0) {
|
||||||
|
publishSensor("VE.Smart network total DC input power", "mdi:solar-power", "NetworkTotalDcInputPower", "power", "measurement", "W", *optMpptData);
|
||||||
|
}
|
||||||
|
if (optMpptData->MpptTemperatureMilliCelsius.first != 0) {
|
||||||
|
publishSensor("MPPT temperature", "mdi:temperature-celsius", "MpptTemperature", "temperature", "measurement", "W", *optMpptData);
|
||||||
|
}
|
||||||
|
if (optMpptData->SmartBatterySenseTemperatureMilliCelsius.first != 0) {
|
||||||
|
publishSensor("Smart Battery Sense temperature", "mdi:temperature-celsius", "SmartBatterySenseTemperature", "temperature", "measurement", "W", *optMpptData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttHandleVedirectHassClass::publishSensor(const char *caption, const char *icon, const char *subTopic,
|
||||||
|
const char *deviceClass, const char *stateClass,
|
||||||
|
const char *unitOfMeasurement,
|
||||||
|
const VeDirectMpptController::data_t &mpptData)
|
||||||
|
{
|
||||||
|
String serial = mpptData.serialNr_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);
|
||||||
|
|
||||||
|
JsonDocument root;
|
||||||
|
|
||||||
|
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["dev"].to<JsonObject>();
|
||||||
|
createDeviceInfo(deviceObj, mpptData);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
const VeDirectMpptController::data_t &mpptData)
|
||||||
|
{
|
||||||
|
String serial = mpptData.serialNr_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);
|
||||||
|
|
||||||
|
JsonDocument root;
|
||||||
|
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["dev"].to<JsonObject>();
|
||||||
|
createDeviceInfo(deviceObj, mpptData);
|
||||||
|
|
||||||
|
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char buffer[512];
|
||||||
|
serializeJson(root, buffer);
|
||||||
|
publish(configTopic, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject &object,
|
||||||
|
const VeDirectMpptController::data_t &mpptData)
|
||||||
|
{
|
||||||
|
String serial = mpptData.serialNr_SER;
|
||||||
|
object["name"] = "Victron(" + serial + ")";
|
||||||
|
object["ids"] = serial;
|
||||||
|
object["cu"] = String("http://") + NetworkSettings.localIP().toString();
|
||||||
|
object["mf"] = "OpenDTU";
|
||||||
|
object["mdl"] = mpptData.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);
|
||||||
|
}
|
||||||
248
src/MqttHandleBatteryHass.cpp
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "PylontechCanReceiver.h"
|
||||||
|
#include "Battery.h"
|
||||||
|
#include "MqttHandleBatteryHass.h"
|
||||||
|
#include "Configuration.h"
|
||||||
|
#include "MqttSettings.h"
|
||||||
|
#include "Utils.h"
|
||||||
|
|
||||||
|
MqttHandleBatteryHassClass MqttHandleBatteryHass;
|
||||||
|
|
||||||
|
void MqttHandleBatteryHassClass::init(Scheduler& scheduler)
|
||||||
|
{
|
||||||
|
scheduler.addTask(_loopTask);
|
||||||
|
_loopTask.setCallback(std::bind(&MqttHandleBatteryHassClass::loop, this));
|
||||||
|
_loopTask.setIterations(TASK_FOREVER);
|
||||||
|
_loopTask.enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttHandleBatteryHassClass::loop()
|
||||||
|
{
|
||||||
|
CONFIG_T& config = Configuration.get();
|
||||||
|
|
||||||
|
if (!config.Battery.Enabled) { return; }
|
||||||
|
|
||||||
|
if (!config.Mqtt.Hass.Enabled) { return; }
|
||||||
|
|
||||||
|
// TODO(schlimmchen): this cannot make sure that transient
|
||||||
|
// connection problems are actually always noticed.
|
||||||
|
if (!MqttSettings.getConnected()) {
|
||||||
|
_doPublish = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// only publish HA config once when (re-)connecting
|
||||||
|
// to the MQTT broker or on config changes.
|
||||||
|
if (!_doPublish) { return; }
|
||||||
|
|
||||||
|
// the MQTT battery provider does not re-publish the SoC under a different
|
||||||
|
// known topic. we don't know the manufacture either. HASS auto-discovery
|
||||||
|
// for that provider makes no sense.
|
||||||
|
if (config.Battery.Provider != 2) {
|
||||||
|
publishSensor("Manufacturer", "mdi:factory", "manufacturer");
|
||||||
|
publishSensor("Data Age", "mdi:timer-sand", "dataAge", "duration", "measurement", "s");
|
||||||
|
publishSensor("State of Charge (SoC)", "mdi:battery-medium", "stateOfCharge", "battery", "measurement", "%");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (config.Battery.Provider) {
|
||||||
|
case 0: // Pylontech Battery
|
||||||
|
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 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");
|
||||||
|
break;
|
||||||
|
case 1: // JK BMS
|
||||||
|
// caption icon topic dev. class state class unit
|
||||||
|
publishSensor("Voltage", "mdi:battery-charging", "BatteryVoltageMilliVolt", "voltage", "measurement", "mV");
|
||||||
|
publishSensor("Current", "mdi:current-dc", "BatteryCurrentMilliAmps", "current", "measurement", "mA");
|
||||||
|
publishSensor("BMS Temperature", "mdi:thermometer", "BmsTempCelsius", "temperature", "measurement", "°C");
|
||||||
|
publishSensor("Cell Voltage Diff", "mdi:battery-alert", "CellDiffMilliVolt", "voltage", "measurement", "mV");
|
||||||
|
publishSensor("Charge Cycles", "mdi:counter", "BatteryCycles");
|
||||||
|
publishSensor("Cycle Capacity", "mdi:battery-sync", "BatteryCycleCapacity");
|
||||||
|
|
||||||
|
publishBinarySensor("Charging Possible", "mdi:battery-arrow-up", "status/ChargingActive", "1", "0");
|
||||||
|
publishBinarySensor("Discharging Possible", "mdi:battery-arrow-down", "status/DischargingActive", "1", "0");
|
||||||
|
publishBinarySensor("Balancing Active", "mdi:scale-balance", "status/BalancingActive", "1", "0");
|
||||||
|
|
||||||
|
#define PBS(a, b, c) publishBinarySensor("Alarm: " a, "mdi:" b, "alarms/" c, "1", "0")
|
||||||
|
PBS("Low Capacity", "battery-alert-variant-outline", "LowCapacity");
|
||||||
|
PBS("BMS Overtemperature", "thermometer-alert", "BmsOvertemperature");
|
||||||
|
PBS("Charging Overvoltage", "fuse-alert", "ChargingOvervoltage");
|
||||||
|
PBS("Discharge Undervoltage", "fuse-alert", "DischargeUndervoltage");
|
||||||
|
PBS("Battery Overtemperature", "thermometer-alert", "BatteryOvertemperature");
|
||||||
|
PBS("Charging Overcurrent", "fuse-alert", "ChargingOvercurrent");
|
||||||
|
PBS("Discharging Overcurrent", "fuse-alert", "DischargeOvercurrent");
|
||||||
|
PBS("Cell Voltage Difference", "battery-alert", "CellVoltageDifference");
|
||||||
|
PBS("Battery Box Overtemperature", "thermometer-alert", "BatteryBoxOvertemperature");
|
||||||
|
PBS("Battery Undertemperature", "thermometer-alert", "BatteryUndertemperature");
|
||||||
|
PBS("Cell Overvoltage", "battery-alert", "CellOvervoltage");
|
||||||
|
PBS("Cell Undervoltage", "battery-alert", "CellUndervoltage");
|
||||||
|
#undef PBS
|
||||||
|
break;
|
||||||
|
case 2: // SoC from MQTT
|
||||||
|
break;
|
||||||
|
case 3: // Victron SmartShunt
|
||||||
|
publishSensor("Voltage", "mdi:battery-charging", "voltage", "voltage", "measurement", "V");
|
||||||
|
publishSensor("Current", "mdi:current-dc", "current", "current", "measurement", "A");
|
||||||
|
publishSensor("Instantaneous Power", NULL, "instantaneousPower", "power", "measurement", "W");
|
||||||
|
publishSensor("Charged Energy", NULL, "chargedEnergy", "energy", "total_increasing", "kWh");
|
||||||
|
publishSensor("Discharged Energy", NULL, "dischargedEnergy", "energy", "total_increasing", "kWh");
|
||||||
|
publishSensor("Charge Cycles", "mdi:counter", "chargeCycles");
|
||||||
|
publishSensor("Consumed Amp Hours", NULL, "consumedAmpHours", NULL, "measurement", "Ah");
|
||||||
|
publishSensor("Last Full Charge", "mdi:timelapse", "lastFullCharge", NULL, NULL, "min");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
_doPublish = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttHandleBatteryHassClass::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);
|
||||||
|
|
||||||
|
JsonDocument root;
|
||||||
|
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["dev"].to<JsonObject>();
|
||||||
|
createDeviceInfo(deviceObj);
|
||||||
|
|
||||||
|
if (Configuration.get().Mqtt.Hass.Expire) {
|
||||||
|
root["exp_aft"] = Battery.getStats()->getMqttFullPublishIntervalMs() / 1000 * 3;
|
||||||
|
}
|
||||||
|
if (deviceClass != NULL) {
|
||||||
|
root["dev_cla"] = deviceClass;
|
||||||
|
}
|
||||||
|
if (stateClass != NULL) {
|
||||||
|
root["stat_cla"] = stateClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char buffer[512];
|
||||||
|
serializeJson(root, buffer);
|
||||||
|
publish(configTopic, buffer);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttHandleBatteryHassClass::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.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);
|
||||||
|
|
||||||
|
JsonDocument root;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto deviceObj = root["dev"].to<JsonObject>();
|
||||||
|
createDeviceInfo(deviceObj);
|
||||||
|
|
||||||
|
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char buffer[512];
|
||||||
|
serializeJson(root, buffer);
|
||||||
|
publish(configTopic, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttHandleBatteryHassClass::createDeviceInfo(JsonObject& object)
|
||||||
|
{
|
||||||
|
object["name"] = "Battery(" + serial + ")";
|
||||||
|
|
||||||
|
auto& config = Configuration.get();
|
||||||
|
if (config.Battery.Provider == 1) {
|
||||||
|
object["name"] = "JK BMS (" + Battery.getStats()->getManufacturer() + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
object["ids"] = serial;
|
||||||
|
object["cu"] = String("http://") + NetworkSettings.localIP().toString();
|
||||||
|
object["mf"] = "OpenDTU";
|
||||||
|
object["mdl"] = Battery.getStats()->getManufacturer();
|
||||||
|
object["sw"] = AUTO_GIT_HASH;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttHandleBatteryHassClass::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);
|
||||||
|
}
|
||||||
@ -323,9 +323,7 @@ void MqttHandleHassClass::publishDtuSensor(const char* name, const char* device_
|
|||||||
|
|
||||||
createDtuInfo(root);
|
createDtuInfo(root);
|
||||||
|
|
||||||
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String buffer;
|
String buffer;
|
||||||
const String configTopic = "sensor/" + getDtuUniqueId() + "/" + id + "/config";
|
const String configTopic = "sensor/" + getDtuUniqueId() + "/" + id + "/config";
|
||||||
|
|||||||
162
src/MqttHandleHuawei.cpp
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
// 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));
|
||||||
|
MqttSettings.publish("huawei/mode", String(HuaweiCan.getMode()));
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
181
src/MqttHandlePowerLimiter.cpp
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
// 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 const& prefix = MqttSettings.getPrefix();
|
||||||
|
|
||||||
|
auto subscribe = [&prefix, this](char const* subTopic, MqttPowerLimiterCommand command) {
|
||||||
|
String fullTopic(prefix + "powerlimiter/cmd/" + subTopic);
|
||||||
|
MqttSettings.subscribe(fullTopic.c_str(), 0,
|
||||||
|
std::bind(&MqttHandlePowerLimiterClass::onMqttCmd, this, command,
|
||||||
|
std::placeholders::_1, std::placeholders::_2,
|
||||||
|
std::placeholders::_3, std::placeholders::_4,
|
||||||
|
std::placeholders::_5, std::placeholders::_6));
|
||||||
|
};
|
||||||
|
|
||||||
|
subscribe("threshold/soc/start", MqttPowerLimiterCommand::BatterySoCStartThreshold);
|
||||||
|
subscribe("threshold/soc/stop", MqttPowerLimiterCommand::BatterySoCStopThreshold);
|
||||||
|
subscribe("threshold/soc/full_solar_passthrough", MqttPowerLimiterCommand::FullSolarPassthroughSoC);
|
||||||
|
subscribe("threshold/voltage/start", MqttPowerLimiterCommand::VoltageStartThreshold);
|
||||||
|
subscribe("threshold/voltage/stop", MqttPowerLimiterCommand::VoltageStopThreshold);
|
||||||
|
subscribe("threshold/voltage/full_solar_passthrough_start", MqttPowerLimiterCommand::FullSolarPassThroughStartVoltage);
|
||||||
|
subscribe("threshold/voltage/full_solar_passthrough_stop", MqttPowerLimiterCommand::FullSolarPassThroughStopVoltage);
|
||||||
|
subscribe("mode", MqttPowerLimiterCommand::Mode);
|
||||||
|
|
||||||
|
_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)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastPublish = millis();
|
||||||
|
|
||||||
|
auto val = static_cast<unsigned>(PowerLimiter.getMode());
|
||||||
|
MqttSettings.publish("powerlimiter/status/mode", String(val));
|
||||||
|
|
||||||
|
MqttSettings.publish("powerlimiter/status/inverter_update_timeouts", String(PowerLimiter.getInverterUpdateTimeouts()));
|
||||||
|
|
||||||
|
// no thresholds are relevant for setups without a battery
|
||||||
|
if (config.PowerLimiter.IsInverterSolarPowered) { return; }
|
||||||
|
|
||||||
|
MqttSettings.publish("powerlimiter/status/threshold/voltage/start", String(config.PowerLimiter.VoltageStartThreshold));
|
||||||
|
MqttSettings.publish("powerlimiter/status/threshold/voltage/stop", String(config.PowerLimiter.VoltageStopThreshold));
|
||||||
|
|
||||||
|
if (config.Vedirect.Enabled) {
|
||||||
|
MqttSettings.publish("powerlimiter/status/threshold/voltage/full_solar_passthrough_start", String(config.PowerLimiter.FullSolarPassThroughStartVoltage));
|
||||||
|
MqttSettings.publish("powerlimiter/status/threshold/voltage/full_solar_passthrough_stop", String(config.PowerLimiter.FullSolarPassThroughStopVoltage));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.Battery.Enabled || config.PowerLimiter.IgnoreSoc) { return; }
|
||||||
|
|
||||||
|
MqttSettings.publish("powerlimiter/status/threshold/soc/start", String(config.PowerLimiter.BatterySocStartThreshold));
|
||||||
|
MqttSettings.publish("powerlimiter/status/threshold/soc/stop", String(config.PowerLimiter.BatterySocStopThreshold));
|
||||||
|
|
||||||
|
if (config.Vedirect.Enabled) {
|
||||||
|
MqttSettings.publish("powerlimiter/status/threshold/soc/full_solar_passthrough", String(config.PowerLimiter.FullSolarPassThroughSoc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttHandlePowerLimiterClass::onMqttCmd(MqttPowerLimiterCommand command, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
|
||||||
|
{
|
||||||
|
CONFIG_T& config = Configuration.get();
|
||||||
|
|
||||||
|
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("PowerLimiter MQTT handler: cannot parse payload of topic '%s' as float: %s\r\n",
|
||||||
|
topic, strValue.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int intValue = static_cast<int>(payload_val);
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> mqttLock(_mqttMutex);
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case MqttPowerLimiterCommand::Mode:
|
||||||
|
{
|
||||||
|
using Mode = PowerLimiterClass::Mode;
|
||||||
|
Mode mode = static_cast<Mode>(intValue);
|
||||||
|
if (mode == Mode::UnconditionalFullSolarPassthrough) {
|
||||||
|
MessageOutput.println("Power limiter unconditional full solar PT");
|
||||||
|
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
|
||||||
|
&PowerLimiter, Mode::UnconditionalFullSolarPassthrough));
|
||||||
|
} else if (mode == Mode::Disabled) {
|
||||||
|
MessageOutput.println("Power limiter disabled (override)");
|
||||||
|
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
|
||||||
|
&PowerLimiter, Mode::Disabled));
|
||||||
|
} else if (mode == Mode::Normal) {
|
||||||
|
MessageOutput.println("Power limiter normal operation");
|
||||||
|
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
|
||||||
|
&PowerLimiter, Mode::Normal));
|
||||||
|
} else {
|
||||||
|
MessageOutput.printf("PowerLimiter - unknown mode %d\r\n", intValue);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case MqttPowerLimiterCommand::BatterySoCStartThreshold:
|
||||||
|
if (config.PowerLimiter.BatterySocStartThreshold == intValue) { return; }
|
||||||
|
MessageOutput.printf("Setting battery SoC start threshold to: %d %%\r\n", intValue);
|
||||||
|
config.PowerLimiter.BatterySocStartThreshold = intValue;
|
||||||
|
break;
|
||||||
|
case MqttPowerLimiterCommand::BatterySoCStopThreshold:
|
||||||
|
if (config.PowerLimiter.BatterySocStopThreshold == intValue) { return; }
|
||||||
|
MessageOutput.printf("Setting battery SoC stop threshold to: %d %%\r\n", intValue);
|
||||||
|
config.PowerLimiter.BatterySocStopThreshold = intValue;
|
||||||
|
break;
|
||||||
|
case MqttPowerLimiterCommand::FullSolarPassthroughSoC:
|
||||||
|
if (config.PowerLimiter.FullSolarPassThroughSoc == intValue) { return; }
|
||||||
|
MessageOutput.printf("Setting full solar passthrough SoC to: %d %%\r\n", intValue);
|
||||||
|
config.PowerLimiter.FullSolarPassThroughSoc = intValue;
|
||||||
|
break;
|
||||||
|
case MqttPowerLimiterCommand::VoltageStartThreshold:
|
||||||
|
if (config.PowerLimiter.VoltageStartThreshold == payload_val) { return; }
|
||||||
|
MessageOutput.printf("Setting voltage start threshold to: %.2f V\r\n", payload_val);
|
||||||
|
config.PowerLimiter.VoltageStartThreshold = payload_val;
|
||||||
|
break;
|
||||||
|
case MqttPowerLimiterCommand::VoltageStopThreshold:
|
||||||
|
if (config.PowerLimiter.VoltageStopThreshold == payload_val) { return; }
|
||||||
|
MessageOutput.printf("Setting voltage stop threshold to: %.2f V\r\n", payload_val);
|
||||||
|
config.PowerLimiter.VoltageStopThreshold = payload_val;
|
||||||
|
break;
|
||||||
|
case MqttPowerLimiterCommand::FullSolarPassThroughStartVoltage:
|
||||||
|
if (config.PowerLimiter.FullSolarPassThroughStartVoltage == payload_val) { return; }
|
||||||
|
MessageOutput.printf("Setting full solar passthrough start voltage to: %.2f V\r\n", payload_val);
|
||||||
|
config.PowerLimiter.FullSolarPassThroughStartVoltage = payload_val;
|
||||||
|
break;
|
||||||
|
case MqttPowerLimiterCommand::FullSolarPassThroughStopVoltage:
|
||||||
|
if (config.PowerLimiter.FullSolarPassThroughStopVoltage == payload_val) { return; }
|
||||||
|
MessageOutput.printf("Setting full solar passthrough stop voltage to: %.2f V\r\n", payload_val);
|
||||||
|
config.PowerLimiter.FullSolarPassThroughStopVoltage = payload_val;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// not reached if the value did not change
|
||||||
|
Configuration.write();
|
||||||
|
}
|
||||||
204
src/MqttHandlePowerLimiterHass.cpp
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Thomas Basler and others
|
||||||
|
*/
|
||||||
|
#include "MqttHandlePowerLimiterHass.h"
|
||||||
|
#include "Configuration.h"
|
||||||
|
#include "MqttSettings.h"
|
||||||
|
#include "NetworkSettings.h"
|
||||||
|
#include "MessageOutput.h"
|
||||||
|
#include "Utils.h"
|
||||||
|
|
||||||
|
MqttHandlePowerLimiterHassClass MqttHandlePowerLimiterHass;
|
||||||
|
|
||||||
|
void MqttHandlePowerLimiterHassClass::init(Scheduler& scheduler)
|
||||||
|
{
|
||||||
|
scheduler.addTask(_loopTask);
|
||||||
|
_loopTask.setCallback(std::bind(&MqttHandlePowerLimiterHassClass::loop, this));
|
||||||
|
_loopTask.setIterations(TASK_FOREVER);
|
||||||
|
_loopTask.enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttHandlePowerLimiterHassClass::loop()
|
||||||
|
{
|
||||||
|
if (!Configuration.get().PowerLimiter.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 MqttHandlePowerLimiterHassClass::forceUpdate()
|
||||||
|
{
|
||||||
|
_updateForced = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttHandlePowerLimiterHassClass::publishConfig()
|
||||||
|
{
|
||||||
|
auto const& config = Configuration.get();
|
||||||
|
|
||||||
|
if (!config.Mqtt.Hass.Enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MqttSettings.getConnected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.PowerLimiter.Enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
publishSelect("DPL Mode", "mdi:gauge", "config", "mode", "mode");
|
||||||
|
|
||||||
|
if (config.PowerLimiter.IsInverterSolarPowered) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// as this project revolves around Hoymiles inverters, 16 - 60 V is a reasonable voltage range
|
||||||
|
publishNumber("DPL battery voltage start threshold", "mdi:battery-charging",
|
||||||
|
"config", "threshold/voltage/start", "threshold/voltage/start", "V", 16, 60);
|
||||||
|
publishNumber("DPL battery voltage stop threshold", "mdi:battery-charging",
|
||||||
|
"config", "threshold/voltage/stop", "threshold/voltage/stop", "V", 16, 60);
|
||||||
|
|
||||||
|
if (config.Vedirect.Enabled) {
|
||||||
|
publishNumber("DPL full solar passthrough start voltage",
|
||||||
|
"mdi:transmission-tower-import", "config",
|
||||||
|
"threshold/voltage/full_solar_passthrough_start",
|
||||||
|
"threshold/voltage/full_solar_passthrough_start", "V", 16, 60);
|
||||||
|
publishNumber("DPL full solar passthrough stop voltage",
|
||||||
|
"mdi:transmission-tower-import", "config",
|
||||||
|
"threshold/voltage/full_solar_passthrough_stop",
|
||||||
|
"threshold/voltage/full_solar_passthrough_stop", "V", 16, 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.Battery.Enabled && !config.PowerLimiter.IgnoreSoc) {
|
||||||
|
publishNumber("DPL battery SoC start threshold", "mdi:battery-charging",
|
||||||
|
"config", "threshold/soc/start", "threshold/soc/start", "%", 0, 100);
|
||||||
|
publishNumber("DPL battery SoC stop threshold", "mdi:battery-charging",
|
||||||
|
"config", "threshold/soc/stop", "threshold/soc/stop", "%", 0, 100);
|
||||||
|
|
||||||
|
if (config.Vedirect.Enabled) {
|
||||||
|
publishNumber("DPL full solar passthrough SoC",
|
||||||
|
"mdi:transmission-tower-import", "config",
|
||||||
|
"threshold/soc/full_solar_passthrough",
|
||||||
|
"threshold/soc/full_solar_passthrough", "%", 0, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttHandlePowerLimiterHassClass::publishSelect(
|
||||||
|
const char* caption, const char* icon, const char* category,
|
||||||
|
const char* commandTopic, const char* stateTopic)
|
||||||
|
{
|
||||||
|
|
||||||
|
String selectId = caption;
|
||||||
|
selectId.replace(" ", "_");
|
||||||
|
selectId.toLowerCase();
|
||||||
|
|
||||||
|
const String configTopic = "select/powerlimiter/" + selectId + "/config";
|
||||||
|
|
||||||
|
const String cmdTopic = MqttSettings.getPrefix() + "powerlimiter/cmd/" + commandTopic;
|
||||||
|
const String statTopic = MqttSettings.getPrefix() + "powerlimiter/status/" + stateTopic;
|
||||||
|
|
||||||
|
JsonDocument root;
|
||||||
|
|
||||||
|
root["name"] = caption;
|
||||||
|
root["uniq_id"] = selectId;
|
||||||
|
if (strcmp(icon, "")) {
|
||||||
|
root["ic"] = icon;
|
||||||
|
}
|
||||||
|
root["ent_cat"] = category;
|
||||||
|
root["cmd_t"] = cmdTopic;
|
||||||
|
root["stat_t"] = statTopic;
|
||||||
|
JsonArray options = root["options"].to<JsonArray>();
|
||||||
|
options.add("0");
|
||||||
|
options.add("1");
|
||||||
|
options.add("2");
|
||||||
|
|
||||||
|
JsonObject deviceObj = root["dev"].to<JsonObject>();
|
||||||
|
createDeviceInfo(deviceObj);
|
||||||
|
|
||||||
|
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String buffer;
|
||||||
|
serializeJson(root, buffer);
|
||||||
|
publish(configTopic, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttHandlePowerLimiterHassClass::publishNumber(
|
||||||
|
const char* caption, const char* icon, const char* category,
|
||||||
|
const char* commandTopic, const char* stateTopic, const char* unitOfMeasure,
|
||||||
|
const int16_t min, const int16_t max)
|
||||||
|
{
|
||||||
|
|
||||||
|
String numberId = caption;
|
||||||
|
numberId.replace(" ", "_");
|
||||||
|
numberId.toLowerCase();
|
||||||
|
|
||||||
|
const String configTopic = "number/powerlimiter/" + numberId + "/config";
|
||||||
|
|
||||||
|
const String cmdTopic = MqttSettings.getPrefix() + "powerlimiter/cmd/" + commandTopic;
|
||||||
|
const String statTopic = MqttSettings.getPrefix() + "powerlimiter/status/" + stateTopic;
|
||||||
|
|
||||||
|
JsonDocument root;
|
||||||
|
|
||||||
|
root["name"] = caption;
|
||||||
|
root["uniq_id"] = numberId;
|
||||||
|
if (strcmp(icon, "")) {
|
||||||
|
root["ic"] = icon;
|
||||||
|
}
|
||||||
|
root["ent_cat"] = category;
|
||||||
|
root["cmd_t"] = cmdTopic;
|
||||||
|
root["stat_t"] = statTopic;
|
||||||
|
root["unit_of_meas"] = unitOfMeasure;
|
||||||
|
root["min"] = min;
|
||||||
|
root["max"] = max;
|
||||||
|
root["mode"] = "box";
|
||||||
|
|
||||||
|
auto const& config = Configuration.get();
|
||||||
|
if (config.Mqtt.Hass.Expire) {
|
||||||
|
root["exp_aft"] = config.Mqtt.PublishInterval * 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject deviceObj = root["dev"].to<JsonObject>();
|
||||||
|
createDeviceInfo(deviceObj);
|
||||||
|
|
||||||
|
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String buffer;
|
||||||
|
serializeJson(root, buffer);
|
||||||
|
publish(configTopic, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttHandlePowerLimiterHassClass::createDeviceInfo(JsonObject& object)
|
||||||
|
{
|
||||||
|
object["name"] = "Dynamic Power Limiter";
|
||||||
|
object["ids"] = "0002";
|
||||||
|
object["cu"] = String("http://") + NetworkSettings.localIP().toString();
|
||||||
|
object["mf"] = "OpenDTU";
|
||||||
|
object["mdl"] = "Dynamic Power Limiter";
|
||||||
|
object["sw"] = AUTO_GIT_HASH;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttHandlePowerLimiterHassClass::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);
|
||||||
|
}
|
||||||
143
src/MqttHandleVedirect.cpp
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
// 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([this] { loop(); });
|
||||||
|
_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 ((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
|
||||||
|
|
||||||
|
for (int idx = 0; idx < VictronMppt.controllerAmount(); ++idx) {
|
||||||
|
std::optional<VeDirectMpptController::data_t> optMpptData = VictronMppt.getData(idx);
|
||||||
|
if (!optMpptData.has_value()) { continue; }
|
||||||
|
|
||||||
|
auto const& kvFrame = _kvFrames[optMpptData->serialNr_SER];
|
||||||
|
publish_mppt_data(*optMpptData, kvFrame);
|
||||||
|
if (!_PublishFull) {
|
||||||
|
_kvFrames[optMpptData->serialNr_SER] = *optMpptData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::data_t ¤tData,
|
||||||
|
const VeDirectMpptController::data_t &previousData) const {
|
||||||
|
String value;
|
||||||
|
String topic = "victron/";
|
||||||
|
topic.concat(currentData.serialNr_SER);
|
||||||
|
topic.concat("/");
|
||||||
|
|
||||||
|
#define PUBLISH(sm, t, val) \
|
||||||
|
if (_PublishFull || currentData.sm != previousData.sm) { \
|
||||||
|
MqttSettings.publish(topic + t, String(val)); \
|
||||||
|
}
|
||||||
|
|
||||||
|
PUBLISH(productID_PID, "PID", currentData.getPidAsString().data());
|
||||||
|
PUBLISH(serialNr_SER, "SER", currentData.serialNr_SER);
|
||||||
|
PUBLISH(firmwareNr_FW, "FW", currentData.firmwareNr_FW);
|
||||||
|
PUBLISH(loadOutputState_LOAD, "LOAD", (currentData.loadOutputState_LOAD ? "ON" : "OFF"));
|
||||||
|
PUBLISH(currentState_CS, "CS", currentData.getCsAsString().data());
|
||||||
|
PUBLISH(errorCode_ERR, "ERR", currentData.getErrAsString().data());
|
||||||
|
PUBLISH(offReason_OR, "OR", currentData.getOrAsString().data());
|
||||||
|
PUBLISH(stateOfTracker_MPPT, "MPPT", currentData.getMpptAsString().data());
|
||||||
|
PUBLISH(daySequenceNr_HSDS, "HSDS", currentData.daySequenceNr_HSDS);
|
||||||
|
PUBLISH(batteryVoltage_V_mV, "V", currentData.batteryVoltage_V_mV / 1000.0);
|
||||||
|
PUBLISH(batteryCurrent_I_mA, "I", currentData.batteryCurrent_I_mA / 1000.0);
|
||||||
|
PUBLISH(batteryOutputPower_W, "P", currentData.batteryOutputPower_W);
|
||||||
|
PUBLISH(panelVoltage_VPV_mV, "VPV", currentData.panelVoltage_VPV_mV / 1000.0);
|
||||||
|
PUBLISH(panelCurrent_mA, "IPV", currentData.panelCurrent_mA / 1000.0);
|
||||||
|
PUBLISH(panelPower_PPV_W, "PPV", currentData.panelPower_PPV_W);
|
||||||
|
PUBLISH(mpptEfficiency_Percent, "E", currentData.mpptEfficiency_Percent);
|
||||||
|
PUBLISH(yieldTotal_H19_Wh, "H19", currentData.yieldTotal_H19_Wh / 1000.0);
|
||||||
|
PUBLISH(yieldToday_H20_Wh, "H20", currentData.yieldToday_H20_Wh / 1000.0);
|
||||||
|
PUBLISH(maxPowerToday_H21_W, "H21", currentData.maxPowerToday_H21_W);
|
||||||
|
PUBLISH(yieldYesterday_H22_Wh, "H22", currentData.yieldYesterday_H22_Wh / 1000.0);
|
||||||
|
PUBLISH(maxPowerYesterday_H23_W, "H23", currentData.maxPowerYesterday_H23_W);
|
||||||
|
#undef PUBLILSH
|
||||||
|
|
||||||
|
#define PUBLISH_OPT(sm, t, val) \
|
||||||
|
if (currentData.sm.first != 0 && (_PublishFull || currentData.sm.second != previousData.sm.second)) { \
|
||||||
|
MqttSettings.publish(topic + t, String(val)); \
|
||||||
|
}
|
||||||
|
|
||||||
|
PUBLISH_OPT(NetworkTotalDcInputPowerMilliWatts, "NetworkTotalDcInputPower", currentData.NetworkTotalDcInputPowerMilliWatts.second / 1000.0);
|
||||||
|
PUBLISH_OPT(MpptTemperatureMilliCelsius, "MpptTemperature", currentData.MpptTemperatureMilliCelsius.second / 1000.0);
|
||||||
|
PUBLISH_OPT(SmartBatterySenseTemperatureMilliCelsius, "SmartBatterySenseTemperature", currentData.SmartBatterySenseTemperatureMilliCelsius.second / 1000.0);
|
||||||
|
#undef PUBLILSH_OPT
|
||||||
|
}
|
||||||
@ -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)
|
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: ");
|
if (_verboseLogging) {
|
||||||
MessageOutput.println(topic);
|
MessageOutput.print("Received MQTT message on topic: ");
|
||||||
|
MessageOutput.println(topic);
|
||||||
|
}
|
||||||
|
|
||||||
_mqttSubscribeParser.handle_message(properties, topic, payload, len, index, total);
|
_mqttSubscribeParser.handle_message(properties, topic, payload, len, index, total);
|
||||||
}
|
}
|
||||||
@ -114,6 +116,7 @@ void MqttSettingsClass::performConnect()
|
|||||||
|
|
||||||
MessageOutput.println("Connecting to MQTT...");
|
MessageOutput.println("Connecting to MQTT...");
|
||||||
const CONFIG_T& config = Configuration.get();
|
const CONFIG_T& config = Configuration.get();
|
||||||
|
_verboseLogging = config.Mqtt.VerboseLogging;
|
||||||
const String willTopic = getPrefix() + config.Mqtt.Lwt.Topic;
|
const String willTopic = getPrefix() + config.Mqtt.Lwt.Topic;
|
||||||
const String clientId = NetworkSettings.getApName();
|
const String clientId = NetworkSettings.getApName();
|
||||||
if (config.Mqtt.Tls.Enabled) {
|
if (config.Mqtt.Tls.Enabled) {
|
||||||
|
|||||||
@ -84,6 +84,84 @@
|
|||||||
#define CMT_SDIO -1
|
#define CMT_SDIO -1
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef VICTRON_PIN_TX
|
||||||
|
#define VICTRON_PIN_TX -1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef VICTRON_PIN_RX
|
||||||
|
#define VICTRON_PIN_RX -1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef VICTRON_PIN_TX2
|
||||||
|
#define VICTRON_PIN_TX2 -1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef VICTRON_PIN_RX2
|
||||||
|
#define VICTRON_PIN_RX2 -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
|
||||||
|
|
||||||
|
#ifndef POWERMETER_PIN_RX
|
||||||
|
#define POWERMETER_PIN_RX -1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef POWERMETER_PIN_TX
|
||||||
|
#define POWERMETER_PIN_TX -1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef POWERMETER_PIN_DERE
|
||||||
|
#define POWERMETER_PIN_DERE -1
|
||||||
|
#endif
|
||||||
|
|
||||||
PinMappingClass PinMapping;
|
PinMappingClass PinMapping;
|
||||||
|
|
||||||
PinMappingClass::PinMappingClass()
|
PinMappingClass::PinMappingClass()
|
||||||
@ -124,6 +202,29 @@ PinMappingClass::PinMappingClass()
|
|||||||
|
|
||||||
_pinMapping.led[0] = LED0;
|
_pinMapping.led[0] = LED0;
|
||||||
_pinMapping.led[1] = LED1;
|
_pinMapping.led[1] = LED1;
|
||||||
|
|
||||||
|
// OpenDTU-OnBattery-specific pins below
|
||||||
|
_pinMapping.victron_rx = VICTRON_PIN_RX;
|
||||||
|
_pinMapping.victron_tx = VICTRON_PIN_TX;
|
||||||
|
|
||||||
|
_pinMapping.victron_rx2 = VICTRON_PIN_RX;
|
||||||
|
_pinMapping.victron_tx2 = VICTRON_PIN_TX;
|
||||||
|
|
||||||
|
_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.powermeter_rx = POWERMETER_PIN_RX;
|
||||||
|
_pinMapping.powermeter_tx = POWERMETER_PIN_TX;
|
||||||
|
_pinMapping.powermeter_dere = POWERMETER_PIN_DERE;
|
||||||
}
|
}
|
||||||
|
|
||||||
PinMapping_t& PinMappingClass::get()
|
PinMapping_t& PinMappingClass::get()
|
||||||
@ -186,6 +287,28 @@ bool PinMappingClass::init(const String& deviceMapping)
|
|||||||
_pinMapping.led[0] = doc[i]["led"]["led0"] | LED0;
|
_pinMapping.led[0] = doc[i]["led"]["led0"] | LED0;
|
||||||
_pinMapping.led[1] = doc[i]["led"]["led1"] | LED1;
|
_pinMapping.led[1] = doc[i]["led"]["led1"] | LED1;
|
||||||
|
|
||||||
|
// OpenDTU-OnBattery-specific pins below
|
||||||
|
_pinMapping.victron_rx = doc[i]["victron"]["rx"] | VICTRON_PIN_RX;
|
||||||
|
_pinMapping.victron_tx = doc[i]["victron"]["tx"] | VICTRON_PIN_TX;
|
||||||
|
_pinMapping.victron_rx2 = doc[i]["victron"]["rx2"] | VICTRON_PIN_RX2;
|
||||||
|
_pinMapping.victron_tx2 = doc[i]["victron"]["tx2"] | VICTRON_PIN_TX2;
|
||||||
|
|
||||||
|
_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.powermeter_rx = doc[i]["powermeter"]["rx"] | POWERMETER_PIN_RX;
|
||||||
|
_pinMapping.powermeter_tx = doc[i]["powermeter"]["tx"] | POWERMETER_PIN_TX;
|
||||||
|
_pinMapping.powermeter_dere = doc[i]["powermeter"]["dere"] | POWERMETER_PIN_DERE;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -215,3 +338,13 @@ bool PinMappingClass::isValidEthConfig() const
|
|||||||
{
|
{
|
||||||
return _pinMapping.eth_enabled;
|
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;
|
||||||
|
}
|
||||||
|
|||||||