Modbus TCP registers uitlezen Solaredge
ik zit een beetje vast.
Zijn er mensen die ervaring hebben met het uitlezen met modbus-tcp?
Ik wil mijn data uitlezen van mijn Solaredge inverter.
Ik weet dat er kant en klare oplossingen zijn op internet maar ik wil het zelf schrijven.
Om een of andere manier krijg ik geen output.
Modbus over tcp heb ik ingeschakeld op mijn inverter, heb ik getest met een werkend python scriptje en die geeft output.
(opm.: mochten er mensen zijn die willen weten hoe je modbus-tcp enabled opSolaredge wil ik dit met harte wel uitleggen)
Aan de code is te zien dat ik veel opties heb uitgeprobeerd....
in het sunspec pdf staat ook in de appendix dat er 2 methodes zijn (met en zonder offset 40001 wat erg verwarrend is)
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
<?php
$id = 1; //1
$fc = 3;
$offset = 0; //70, ...107
$length = 122; //39, ..3
//Appendix A – Supported MODBUS Request Methods
//SolarEdge has implemented two methods of the MODBUS request procedure:
//MODBUS request without explicit addressing – supported by communication board CPU version 2.478 and above. For example:
//$offset_h = floor($offset/256);
//$offset_l = $offset-$offset_h*256;
//MODBUS request with explicit register addressing - supported by all communication board CPU versions. For example:
$offset_h = floor(($offset+40000)/256);
$offset_l = ($offset+40000)-$offset_h*256;
$length_h = floor($length/256);
$length_l = $length-$length_h*256;
//$crc = crc16(chr(0).chr($id).chr(0).chr($fc).chr($offset_h).chr($offset_l).chr($length_h).chr($length_l));
$crc = crc16(chr($id).chr($fc).chr($offset_h).chr($offset_l).chr($length_h).chr($length_l));
$crc_h = floor($crc/256);
$crc_l = $crc-$crc_h*256;
if ($id<10) {$id0 = "0";} else {$id0 = "";}
if ($fc<10) {$fc0 = "0";} else {$fc0 = "";}
if ($offset_h<10) {$offset_h0 = "0";} else {$offset_h0 = "";}
if ($offset_l<10) {$offset_l0 = "0";} else {$offset_l0 = "";}
if ($length_h<10) {$length_h0 = "0";} else {$length_h0 = "";}
if ($length_l<10) {$length_l0 = "0";} else {$length_l0 = "";}
if ($crc_h<10) {$crc_h0 = "0";} else {$crc_h0 = "";}
if ($crc_l<10) {$crc_l0 = "0";} else {$crc_l0 = "";}
//$senddata = chr(0).chr($id).chr(0).chr($fc).chr($offset_h).chr($offset_l).chr($length_h).chr($length_l).chr($crc_l).chr($crc_h);
$senddata = chr($id).chr($fc).chr($offset_h).chr($offset_l).chr($length_h).chr($length_l).chr($crc_l).chr($crc_h);
//without crc ...
//$senddata = chr($id).chr($fc).chr($offset_h).chr($offset_l).chr($length_h).chr($length_l);
$senddata2 = $id0.dechex($id)." ".$fc0.dechex($fc)." ".$offset_h0.dechex($offset_h).$offset_l0.dechex($offset_l)." ".$length_h0.dechex($length_h).$length_l0.dechex($length_l)." ".$crc_l0.dechex($crc_l).$crc_h0.dechex($crc_h);
//$senddata = "11 03 006B 0003 7687";
//$senddata = "1103006B00037687";
//print $senddata."<br />\n";
print $senddata2."<br />\n";
//print dechex($id)." ".dechex($fc)." ".dechex($offset_h).dechex($offset_l)." ".dechex($length_h).dechex($length_l)." ";
//print dechex($crc_l).dechex($crc_h);
//set_time_limit(0);
$fp = fsockopen("192.168.0.192", 502, $errno, $errstr, 3);
if(!$fp)
{
print "can\'t connect modbus tcp device<br />\n";
die();
}
else{
print "Connected to modbus<br />\n";
//11 03 0046 0027 xxxx //request 40070 +27 xxxx is crc check
//stream_set_timeout($socketHandler, 2);
write($fp, $senddata);
//write($fp, $senddata,strlen($senddata));
//set_socket_blocking($fp,true);
//fputs($fp, $senddata);
$response = '';
while (!feof($fp)) {
//echo fgets($fp, 128);
//stream_set_timeout($socketHandler, 2);
//$line[] = fgets($fp, 128);
$line[] = fgets($fp);
//echo fgets($fp);
//echo fread($fp,255);
//$response = stream_get_contents($fp);
//$line[] = fread($fp);
//$response .= fgets($fp, 128); // If you expect an answer
}
print $response;
foreach ($line as $key => $value) {
print $key+$offset.":".intval($value)."<br />\n";
}
}
fclose($fp);
function crc16($data) {
$crc16 = 0xFFFF;
for ($i = 0; $i < strlen($data); $i++)
{
$crc16 ^=ord($data[$i]);
for ($j = 8; $j !=0; $j--)
{
if (($crc16 & 0x0001) !=0)
{
$crc16 >>= 1;
$crc16 ^= 0xA001;
}
else
$crc16 >>= 1;
}
}
return $crc16;
}
?>
$id = 1; //1
$fc = 3;
$offset = 0; //70, ...107
$length = 122; //39, ..3
//Appendix A – Supported MODBUS Request Methods
//SolarEdge has implemented two methods of the MODBUS request procedure:
//MODBUS request without explicit addressing – supported by communication board CPU version 2.478 and above. For example:
//$offset_h = floor($offset/256);
//$offset_l = $offset-$offset_h*256;
//MODBUS request with explicit register addressing - supported by all communication board CPU versions. For example:
$offset_h = floor(($offset+40000)/256);
$offset_l = ($offset+40000)-$offset_h*256;
$length_h = floor($length/256);
$length_l = $length-$length_h*256;
//$crc = crc16(chr(0).chr($id).chr(0).chr($fc).chr($offset_h).chr($offset_l).chr($length_h).chr($length_l));
$crc = crc16(chr($id).chr($fc).chr($offset_h).chr($offset_l).chr($length_h).chr($length_l));
$crc_h = floor($crc/256);
$crc_l = $crc-$crc_h*256;
if ($id<10) {$id0 = "0";} else {$id0 = "";}
if ($fc<10) {$fc0 = "0";} else {$fc0 = "";}
if ($offset_h<10) {$offset_h0 = "0";} else {$offset_h0 = "";}
if ($offset_l<10) {$offset_l0 = "0";} else {$offset_l0 = "";}
if ($length_h<10) {$length_h0 = "0";} else {$length_h0 = "";}
if ($length_l<10) {$length_l0 = "0";} else {$length_l0 = "";}
if ($crc_h<10) {$crc_h0 = "0";} else {$crc_h0 = "";}
if ($crc_l<10) {$crc_l0 = "0";} else {$crc_l0 = "";}
//$senddata = chr(0).chr($id).chr(0).chr($fc).chr($offset_h).chr($offset_l).chr($length_h).chr($length_l).chr($crc_l).chr($crc_h);
$senddata = chr($id).chr($fc).chr($offset_h).chr($offset_l).chr($length_h).chr($length_l).chr($crc_l).chr($crc_h);
//without crc ...
//$senddata = chr($id).chr($fc).chr($offset_h).chr($offset_l).chr($length_h).chr($length_l);
$senddata2 = $id0.dechex($id)." ".$fc0.dechex($fc)." ".$offset_h0.dechex($offset_h).$offset_l0.dechex($offset_l)." ".$length_h0.dechex($length_h).$length_l0.dechex($length_l)." ".$crc_l0.dechex($crc_l).$crc_h0.dechex($crc_h);
//$senddata = "11 03 006B 0003 7687";
//$senddata = "1103006B00037687";
//print $senddata."<br />\n";
print $senddata2."<br />\n";
//print dechex($id)." ".dechex($fc)." ".dechex($offset_h).dechex($offset_l)." ".dechex($length_h).dechex($length_l)." ";
//print dechex($crc_l).dechex($crc_h);
//set_time_limit(0);
$fp = fsockopen("192.168.0.192", 502, $errno, $errstr, 3);
if(!$fp)
{
print "can\'t connect modbus tcp device<br />\n";
die();
}
else{
print "Connected to modbus<br />\n";
//11 03 0046 0027 xxxx //request 40070 +27 xxxx is crc check
//stream_set_timeout($socketHandler, 2);
write($fp, $senddata);
//write($fp, $senddata,strlen($senddata));
//set_socket_blocking($fp,true);
//fputs($fp, $senddata);
$response = '';
while (!feof($fp)) {
//echo fgets($fp, 128);
//stream_set_timeout($socketHandler, 2);
//$line[] = fgets($fp, 128);
$line[] = fgets($fp);
//echo fgets($fp);
//echo fread($fp,255);
//$response = stream_get_contents($fp);
//$line[] = fread($fp);
//$response .= fgets($fp, 128); // If you expect an answer
}
print $response;
foreach ($line as $key => $value) {
print $key+$offset.":".intval($value)."<br />\n";
}
}
fclose($fp);
function crc16($data) {
$crc16 = 0xFFFF;
for ($i = 0; $i < strlen($data); $i++)
{
$crc16 ^=ord($data[$i]);
for ($j = 8; $j !=0; $j--)
{
if (($crc16 & 0x0001) !=0)
{
$crc16 >>= 1;
$crc16 ^= 0xA001;
}
else
$crc16 >>= 1;
}
}
return $crc16;
}
?>
$length = 122
Maar wat mij opvalt is dat $length_h = floor($length/256) is dus 0 (nul).
En $length_l = $length-$length_h*256 is dus 122 is gewoon $length ($length = 122).
En dat gebeurt bij alle $length tussen 0 en 256.
01 03 9c40 007a ebad
en dat klopt dus.
eerst is id, 2e is functiecode, 3e is de offset (weet niet of het bij 0 of 40000 moet beginnen) en 4e is de lengte (aantal registers 007a) em het laatste is de crc16 check
toegevoegd: ff nagerekend maar dat is dus de bedoeling ook ... Length_l is de lo-byte dus restgetal van het geheel minus de high-byte
Gewijzigd op 26/11/2020 11:50:49 door Rene Wennekes
Als je aanwijzingen hebt hoe ik die weer open krijg: graag.
Ik wijd het tot nu toe aan een update van de fabrikant.
Maar heb je zo een met een blauw 20x4 lcd display en 4 knoppen met hele kleine rode lampjes?
voor de modvus-tcp aan te zetten moest je wel minimaal firmware 3.x hebben en een bepaald cpu hebben.
Overigens heeft mijn inverter nooit op 80 of 443 gewerkt ......
en modbus protocol heeft normaal poort 502 (of 1502)
In ieder geval (op eigen risico) hierond de methode om modbus-tcp aan te zetten in het 'geheime' menu.
Houd de OK-knop minstens 5 seconden ingedrukt. De volgende melding verschijnt:
Druk de knoppen Omhoog (=1), Omlaag (=2) en OK (=3)in om het wachtwoord voor hetconfiguratiemenuin te voeren: ??OK ??OK ??(1 2 3 1 2 3 1 2).
Select Communication ?LAN Conf ? Modbus TCP (the default port is 502).
2 To modify the TCP port, select Modbus TCP ? TCP Port, set the port number and long-press <Enter>.
Toevoeging op 26/11/2020 16:11:51:
opm.:?? is dus omhoog en omlaag ...^v
Gewijzigd op 26/11/2020 17:01:19 door Rene Wennekes
ik heb kennelijk een ander type, want een display is er bij mij niet aanwezig....
mijne werd geinstalleerd in 2017.
Er moeten toch meer mensen zijn die een nieuwer model hebben en realtime willen kijken zonder tussenkomst via de cloud-app van Solaredge....
Overigens merk ik dat de cloud-app erg onsecuur is en nog véél trager .....
Gewijzigd op 27/11/2020 07:48:05 door Rene Wennekes
Begrijp ik wel: die data zal op gezette tijden naar de centrale server gestuurd worden en pas daarna kan de app die raadplegen.
En volgens mij ook in een wat grovere tijdsintervallen.
Is jammer dat de poorten later gesloten zijn. Ik heb eerder wel een topic op Tweakers gelezen waarin iemand het dataverkeer snoopte om een sleutel te achterhalen om zo de data met een Raspi te op te slaan.
Dat ging me te ver. Maar nu kan ik helemaal niet meer met het ding verbinden.
Maar ik moet zeggen dat na een jaar de app en de app van de energieleverancier ook wel genoeg is.
In het begin zit je er boven op, maar nu kijk ik nog een paar keer per maand.
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$host = "192.168.0.192";
$port = 502;
// No Timeout
set_time_limit(0);
$socket = socket_create(AF_INET, SOCK_STREAM, 0) or die("Could not create socket\n");
socket_connect($socket, $host, $port) or die("Could not connect to server\n");
socket_write($socket, $senddata) or die("Could not send data to server\n");
$result = socket_recv($socket, $buf , 2000 , 0 );
echo "Reply From Server :".strlen($result)." / ".strlen($result)." / ".strlen($buf);
socket_close($socket);
print "\n\n<br />\ntest1<br />\n";
$port = 502;
// No Timeout
set_time_limit(0);
$socket = socket_create(AF_INET, SOCK_STREAM, 0) or die("Could not create socket\n");
socket_connect($socket, $host, $port) or die("Could not connect to server\n");
socket_write($socket, $senddata) or die("Could not send data to server\n");
$result = socket_recv($socket, $buf , 2000 , 0 );
echo "Reply From Server :".strlen($result)." / ".strlen($result)." / ".strlen($buf);
socket_close($socket);
print "\n\n<br />\ntest1<br />\n";
output:
11 03 138c6 0027 ea1d
Reply From Server :1 / 1 / 0
test1
Gewijzigd op 28/11/2020 09:08:46 door Rene Wennekes
<= Onderwerp gesloten =>
Toevoeging op 03/12/2020 10:10:09:
heeft me wat hersen-kraken gekost maar ik heb het kunnen oplossen.
<= Onderwerp gesloten =>
Wat was de oplossing?
In de SolarEdge documentatie "Technical Note – SunSpec Logging in SolarEdge Inverters, Appendix A" staat dat je een request als volgt stuurt (hexadecimaal):
Tx: 01 03 9C 40 00 7A EB AD – Read 122 registers starting at address 40001
Maar dan krijg je dus niks terug, zoals hierboven al staat.
De oplossing stond in een python script van de SunSpec organisatie: https://github.com/sunspec/pysunspec/blob/master/sunspec/core/modbus/client.py
Daaruit blijkt dat je nog iets vóór die serie bytes moet plaatsen, namelijk twee keer 0 en dan de lengte van het request. Dan krijg je:
Tx: 00 00 00 00 00 06 01 03 9C 40 00 7A EB AD
N.B.: De Sunspec standaard gaat uit van words (2 bytes); daarom is de lengte 6 en niet 12.
En dan krijg je dus wel antwoord van de inverter!
Nog een heel gedoe om te decoderen, maar uiteindelijk heb ik er een service van gemaakt die elke 10 seconden de data van mijn inverters ophaalt. En dat werkt uitstekend.
Mocht je interesse hebben in mijn script, laat het even weten.
Groet van Ton
Toevoeging op 17/09/2023 00:57:14:
Ik plak het script hier gelijk maar onder.
Noot: ik heb niet veel ervaring met php, dus het zal hier en daar makkelijker of beter kunnen. Suggesties zijn welkom!
Verder staan er specifieke onderdelen in voor mijn omgeving; daar moet je maar overheen lezen.
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
<?php
// SolarEdge Inverter ModBus/TCP client script / SunSpec protocol
// ing. Ton Rutgers 24-8-2023
// Purpose: Monitor SolarEdge inverters through ModBus/TCP
// Starten op XigmaNAS in een Putty venster:
// /usr/local/bin/php-cgi /mnt/Fileserver/Web/modbusTCP/SolarEdge_modbus_tcp.php
// OR
// php-cgi /mnt/Fileserver/Web/modbusTCP/SolarEdge_modbus_tcp.php
// Stoppen met CTRL-C
// Updates:
// 2023-09-16: using rrdCmd($command) now for rrdtool commands (instead of exec($command)
// rrdtool errors are now logged to rrdtool.log
// corrected socket error message texts
// removed "die()" if a socket could not be created; instead skip this interval
/* References:
https://github.com/sunspec/pysunspec/blob/master/sunspec/core/modbus/client.py -> here the mystery of the requeststring is revealed
https://www.phphulp.nl/php/forum/topic/modbus-tcp-registers-uitlezen-solaredge/103803/last/ -> this was not really helpful
*/
include '/mnt/Fileserver/Web/phpbase/dbConnect.php';
include '/mnt/Fileserver/Web/phpbase/phpFunctions.php';
$debug = 0; // will echo the output also to the terminal
$ipSE12K = "192.168.178.75"; // IP address of SE12.5K inverter
$ipSE10K = "192.168.178.76"; // IP address of SE10K inverter
$serverPort = 1502; // Modbus/TCP port of SolarEdge inverter
$TSformat = "Y-m-d H:i:s"; // timestamp format
error_reporting(E_ALL & ~E_NOTICE); // report all PHP messages, except "Notice"
// open logfile and append
$logfile = fopen("/mnt/Fileserver/Web/modbusTCP/modbusTCP.log", "a") or die("Unable to open file!");
$txt = date($TSformat) . " INFO modbusTCP client started at port $serverPort\n";
fwrite($logfile, $txt);
// get properties of inverters from database, devicetypeID = 3
$conn = OpenCon();
if ($conn->connect_error) {
$txt = date($TSformat) . " ERROR Could not connect to mySQL. Exiting \n";
fwrite($logfile, $txt);
die();
}
$sql = "select deviceID, locatie from device where devicetypeID = 3";
$result = $conn->query($sql);
if ($result->num_rows < 1) {
$txt = date($TSformat) . " ERROR No inverters in database. Exiting \n";
fwrite($logfile, $txt);
die();
}
$i = 0;
while ( $row = $result->fetch_assoc() ) {
$deviceID[$i] = $row["deviceID"];
$IPinverter[$i] = $row["locatie"];
$i++;
}
$nInverter = $i;
CloseCon($conn); // close database connection
for ($i = 0; $i < $nInverter; $i++) {
$txt = date($TSformat) . " INFO logging inverter with deviceID $deviceID[$i] ip $IPinverter[$i] \n";
fwrite($logfile, $txt);
}
// request related parameters
$TCP_HEADER = 0; // no idea what this is for, but it part of the header
$TCP_REQ_LEN = 6; // request length
$id = 1; // inverter device id
$fc = 3; // ?
$regaddr = 40000; // register base address
$length = 122-8; // length of SunSpec Common model plus three-phase inverter model. IN WORDS!
// Note: there are 17 bytes at the end who contain no info, so exclude them
$requestString = pack ("nnnCCnn", $TCP_HEADER, $TCP_HEADER, $TCP_REQ_LEN, $id, $fc, $regaddr, $length);
if ($debug) {
$lenbin = strlen($requestString);
echo "Len of binary string: $lenbin bytes \n";
// decode binary requeststring back into array for verification
$a = unpack ("n1h1/n1h2/n1len/C1id/C1fc/n1address/n1length", $requestString); // but unpack in hi-lo order, so the representaion is as expected
$a1 = implode(",", $a);
print_r ($a);
}
$startTime = round(microtime(true), -1); // seconds since 1-1-1970, rounded to multiples of 10 seconds to align the time to the interval
$interval = 10; // process every 10 seconds;
while (true) { // loop forever
usleep(500000); // go to sleep for 500ms
if (microtime(true) - $startTime >= $interval) {
$startTime += $interval; // renew $starttime
$conn = OpenCon();
if ($conn->connect_error) {
$txt = date($TSformat) . " ERROR Could not connec to mySQL. Skip this interval \n";
fwrite($logfile, $txt);
}
else {
$timestamp = date($TSformat); // use same timestamp for all inverters
for ($i = 0; $i < $nInverter; $i++) {
getLog($deviceID[$i], $IPinverter[$i], $serverPort, $requestString, $length, $conn, $logfile, $timestamp, $TSformat, $debug);
}
}
CloseCon($conn); // close database connection
}
}
fclose($logfile); // this will probably never happen, because of the endless loop above
die("Terminated");
// END OF MAIN LOOP
function getLog($deviceID, $serverIP, $serverPort, $requestString, $length, $conn, $logfile, $timestamp, $TSformat, $debug) {
$RRDNAME = "InvLogReadings$deviceID.rrd"; // include deviceID in rrd log archive
$STORAGE_PATH = "/mnt/Fileserver/Web/www/dbLogging/rrd";
$RRDFILE = "$STORAGE_PATH/archives/$RRDNAME";
if ($debug) {
$t0 = microtime(true); // for time measurement
echo "Get data from deviceID $deviceID ip $serverIP \n";
}
if (!$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) {
$errorcode = socket_last_error();
$errormsg = socket_strerror($errorcode);
$txt = date($TSformat) . " ip=$serverIP socket_create errmsg: $errormsg\n";
fwrite($logfile, $txt);
return;
}
if (!socket_connect($socket, $serverIP, $serverPort)) {
$errorcode = socket_last_error();
$errormsg = socket_strerror($errorcode);
$txt = date($TSformat) . " ip=$serverIP socket_connect errmsg: $errormsg\n";
fwrite($logfile, $txt);
socket_close($socket);
return;
}
if (!socket_write($socket, $requestString)) {
$errorcode = socket_last_error();
$errormsg = socket_strerror($errorcode);
$txt = date($TSformat) . " ip=$serverIP socket_write errmsg: $errormsg\n";
fwrite($logfile, $txt);
socket_close($socket);
return;
}
$buf = "";
// we are expecting exactly $length number of words = 2 * $length bytes
if (!socket_recv($socket, $buf , 2 * $length, MSG_WAITALL )) {
$errorcode = socket_last_error();
$errormsg = socket_strerror($errorcode);
$txt = date($TSformat) . " ip=$serverIP socket_recv errmsg: $errormsg\n";
fwrite($logfile, $txt);
socket_close($socket);
return;
}
socket_close($socket);
// DECODE the buffer
// Note: the first 0 ... 8 (9 bytes) are not part of the model; what are they?
// the first 5 bytes are 0x00
// byte 6 = 247 (0xE7)
// byte 7 = 0x01 -> device ID?
// byte 8 = 0x03 -> model number?
// byte 9 = 244 (0xE4)
// Between the Common model and the inverter model there are bytes who do not belong to a model; they are skipped with a 'filler item'
// At the end of the message there are multiple irrelevant bytes; leave them undecoded
// I did not discover a crc at the end of the message, so we are not going to use the CRC16 function
$buf = substr($buf, 9); // get rid of the first 9 bytes
$data = unpack('a4C_SunSpec_ID/n1C_SunSpec_DID1/n1C_sunSpec_length1/a32C_Manufacturer/a32C_model/a16C_Version/a32C_SerialNumber/n1C_DeviceAddress/a16filler/n1C_SunSpec_DID2/n1C_sunSpec_length2/n1I_AC_Current/n1I_AC_CurrentA/n1I_AC_CurrentB/n1I_AC_CurrentC/n1I_AC_Current_SF/n1I_AC_VoltageAB/n1I_AC_VoltageBC/n1I_AC_VoltageCA/n1I_AC_VoltageAN/n1I_AC_VoltageBN/n1I_AC_VoltageCN/n1I_AC_Voltage_SF/n1I_AC_Power/n1I_AC_Power_SF/n1I_AC_Frequency/n1I_AC_Frequency_SF/n1I_AC_VA/n1I_AC_VA_SF/n1I_AC_VAR/n1I_AC_VAR_SF/n1I_AC_PF/n1I_AC_PF_SF/N1I_AC_Energy_WH/n1I_AC_Energy_WH_SF/n1I_DC_Current/n1I_DC_Current_SF/n1I_DC_Voltage/n1I_DC_Voltage_SF/n1I_DC_Power/n1I_DC_Power_SF/n1I_Temp_Cab/n1I_Temp_Sink/n1I_Temp_Transf/n1I_Temp_Other/n1I_Temp_SF/n1I_Status/n1I_Status_Vendor', $buf);
// correct int's who are unpacked as uint16, because php unpack does not provide unpack of signed short in big endian format
$data["I_AC_Current_SF"] = correctInt($data["I_AC_Current_SF"]);
$data["I_AC_Voltage_SF"] = correctInt($data["I_AC_Voltage_SF"]);
$data["I_AC_Power"] = correctInt($data["I_AC_Power"]);
$data["I_AC_Power_SF"] = correctInt($data["I_AC_Power_SF"]);
$data["I_AC_Frequency_SF"] = correctInt($data["I_AC_Frequency_SF"]);
$data["I_AC_VA"] = correctInt($data["I_AC_VA"]);
$data["I_AC_VA_SF"] = correctInt($data["I_AC_VA_SF"]);
$data["I_AC_VAR"] = correctInt($data["I_AC_VAR"]);
$data["I_AC_VAR_SF"] = correctInt($data["I_AC_VAR_SF"]);
$data["I_AC_PF"] = correctInt($data["I_AC_PF"]);
$data["I_AC_PF_SF"] = correctInt($data["I_AC_PF_SF"]);
$data["I_AC_Energy_WH_SF"] = correctInt($data["I_AC_Energy_WH_SF"]);
$data["I_DC_Current_SF"] = correctInt($data["I_DC_Current_SF"]);
$data["I_DC_Voltage_SF"] = correctInt($data["I_DC_Voltage_SF"]);
$data["I_DC_Power"] = correctInt($data["I_DC_Power"]);
$data["I_DC_Power_SF"] = correctInt($data["I_DC_Power_SF"]);
$data["I_Temp_Cab"] = correctInt($data["I_Temp_Cab"]);
$data["I_Temp_Sink"] = correctInt($data["I_Temp_Sink"]);
$data["I_Temp_Transf"] = correctInt($data["I_Temp_Transf"]);
$data["I_Temp_Other"] = correctInt($data["I_Temp_Other"]);
$data["I_Temp_SF"] = correctInt($data["I_Temp_SF"]);
// append to array $data calculated scaled float values
$data["F_AC_Current"] = scale($data["I_AC_Current"], $data["I_AC_Current_SF"]);
$data["F_AC_CurrentA"] = scale($data["I_AC_CurrentA"], $data["I_AC_Current_SF"]);
$data["F_AC_CurrentB"] = scale($data["I_AC_CurrentB"], $data["I_AC_Current_SF"]);
$data["F_AC_CurrentC"] = scale($data["I_AC_CurrentC"], $data["I_AC_Current_SF"]);
$data["F_AC_CurrentC"] = scale($data["I_AC_CurrentC"], $data["I_AC_Current_SF"]);
$data["F_AC_VoltageAB"] = scale($data["I_AC_VoltageAB"], $data["I_AC_Voltage_SF"]);
$data["F_AC_VoltageBC"] = scale($data["I_AC_VoltageBC"], $data["I_AC_Voltage_SF"]);
$data["F_AC_VoltageCA"] = scale($data["I_AC_VoltageCA"], $data["I_AC_Voltage_SF"]);
$data["F_AC_VoltageAN"] = scale($data["I_AC_VoltageAN"], $data["I_AC_Voltage_SF"]);
$data["F_AC_VoltageBN"] = scale($data["I_AC_VoltageBN"], $data["I_AC_Voltage_SF"]);
$data["F_AC_VoltageCN"] = scale($data["I_AC_VoltageCN"], $data["I_AC_Voltage_SF"]);
$data["F_AC_Power"] = scale($data["I_AC_Power"], $data["I_AC_Power_SF"]);
$data["F_AC_Frequency"] = scale($data["I_AC_Frequency"], $data["I_AC_Frequency_SF"]);
$data["F_AC_VA"] = scale($data["I_AC_VA"], $data["I_AC_VA_SF"]);
$data["F_AC_VAR"] = scale($data["I_AC_VAR"], $data["I_AC_VAR_SF"]);
$data["F_AC_PF"] = scale($data["I_AC_PF"], $data["I_AC_PF_SF"]);
$data["F_AC_Energy_WH"] = scale($data["I_AC_Energy_WH"], $data["I_AC_Energy_WH_SF"]);
$data["F_DC_Current"] = scale($data["I_DC_Current"], $data["I_DC_Current_SF"]);
$data["F_DC_Voltage"] = scale($data["I_DC_Voltage"], $data["I_DC_Voltage_SF"]);
$data["F_DC_Power"] = scale($data["I_DC_Power"], $data["I_DC_Power_SF"]);
$data["F_Temp_Sink"] = scale($data["I_Temp_Sink"], $data["I_Temp_SF"]);
// append to array calculated values for power per phase. The inverter model does not supply these values, but I want them to compare with the smart meter values.
$data["F_AC_PowerA"] = $data["F_AC_CurrentA"] * $data["F_AC_VoltageAN"] * abs($data["F_AC_PF"]/100); // power (W) for phase A, assuming the general power factor is valid for this phase
$data["F_AC_PowerB"] = $data["F_AC_CurrentB"] * $data["F_AC_VoltageBN"] * abs($data["F_AC_PF"]/100); // power (W) for phase B, assuming the general power factor is valid for this phase
$data["F_AC_PowerC"] = $data["F_AC_CurrentC"] * $data["F_AC_VoltageCN"] * abs($data["F_AC_PF"]/100); // power (W) for phase C, assuming the general power factor is valid for this phase
if ($debug) print_r($data);
// Values to be logged in the database, per device (= inverter)
// F_AC_Energy_WH this is a counter, constantly increasing
// F_AC_Power
// F_AC_VA
// F_AC_VAR
// F_AC_PF
// F_DC_Current
// F_DC_Voltage
// F_DC_Power
// F_Temp_Sink
// I_Status
// insert values into database table. Note: timestamp must be enquoted for mysql
$values = "'$timestamp'" . ',' . $deviceID . ',' . $data['F_AC_Energy_WH'] . ',' . $data['F_AC_Power'] . ',' . $data['F_AC_PowerA'] . ',' . $data['F_AC_PowerB'] . ',' . $data['F_AC_PowerC'] . ',' . $data['F_AC_VA'] . ',' . $data['F_AC_VAR'] . ',' . $data['F_AC_PF'] . ',' . $data['F_DC_Current'] . ',' . $data['F_DC_Voltage'] . ',' . $data['F_DC_Power'] . ',' . $data['F_Temp_Sink'] . ',' . $data['I_Status'];
$sql = "INSERT INTO logInverter(timestamp, deviceID, AC_Energy_WH, AC_Power, AC_PowerA, AC_PowerB, AC_PowerC, AC_VA, AC_VAR, AC_PF, DC_Current, DC_Voltage, DC_Power, Temp_Sink, I_Status) VALUES ($values)";
$result = $conn->query($sql);
if ($result != 1) {
$txt = date($TSformat) . " ERROR Insert failure for deviceID $deviceID \n";
fwrite($logfile, $txt);
}
// Store the data also in the RRD archive; create RRD-file if necessary
// Values to be logged in rrd-archive, each device in it's own rrd:
// F_AC_CurrentA
// F_AC_CurrentB
// F_AC_CurrentC
// F_AC_VoltageAN
// F_AC_VoltageBN
// F_AC_VoltageCN
// F_AC_Power
// F_AC_VA
// F_AC_VAR
// F_AC_PF
// F_DC_Current
// F_DC_Voltage
// F_DC_Power
// F_Temp_Sink
if( !file_exists($RRDFILE) ){
// create the RRD database
// RRA 1: 2 day
// RRA 2: 2 week
// RRA 3: 2 month (31 days)
// RRA 4: 2 year (366 days)
$command = "/usr/local/bin/rrdtool \
create $RRDFILE \
--step 10 \
DS:AC_CurrentA:GAUGE:20:0:U \
DS:AC_CurrentB:GAUGE:20:0:U \
DS:AC_CurrentC:GAUGE:20:0:U \
DS:AC_VoltageAN:GAUGE:20:0:U \
DS:AC_VoltageBN:GAUGE:20:0:U \
DS:AC_VoltageCN:GAUGE:20:0:U \
DS:AC_Power:GAUGE:20:U:U \
DS:AC_VA:GAUGE:20:U:U \
DS:AC_VAR:GAUGE:20:U:U \
DS:AC_PF:GAUGE:20:U:U \
DS:DC_Current:GAUGE:20:0:U \
DS:DC_Voltage:GAUGE:20:0:U \
DS:DC_Power:GAUGE:20:U:U \
DS:Temp_Sink:GAUGE:20:U:U \
RRA:AVERAGE:0.5:1:17280 \
RRA:AVERAGE:0.5:7:17280 \
RRA:AVERAGE:0.5:31:17280 \
RRA:AVERAGE:0.5:366:17280";
if (!rrdCmd($command)) {
$txt = date($TSformat) . " ERROR Could not create rrd archive for deviceID $deviceID Check rrdtool.log \n";
fwrite($logfile, $txt);
}
}
// update archive
$values = $data["F_AC_CurrentA"] . ':' . $data["F_AC_CurrentB"] . ':' . $data["F_AC_CurrentC"] . ':' . $data["F_AC_VoltageAN"] . ':' . $data["F_AC_VoltageBN"] . ':' . $data["F_AC_VoltageCN"] . ':' . $data["F_AC_Power"] . ':' . $data["F_AC_VA"] . ':' . $data["F_AC_VAR"] . ':' . $data["F_AC_PF"] . ':' . $data["F_DC_Current"] . ':' . $data["F_DC_Voltage"] . ':' . $data["F_DC_Power"] . ':' . $data["F_Temp_Sink"];
if (!rrdCmd ( "/usr/local/bin/rrdtool update $RRDFILE N:$values" )) {
$txt = date($TSformat) . " ERROR could not update rrd archive for deviceID $deviceID Check rrdtool.log \n";
fwrite($logfile, $txt);
}
if ($debug) {
$timeUsed = microtime(true) - $t0;
echo date($TSformat) . " -> done for $serverIP in $timeUsed seconds\n";
}
}
function scale($inVal, $scalef) {
// purpose: convert integer input value to float, given the scale factor
return (float)($inVal * pow(10, $scalef));
}
function correctInt($uint16) {
// unint16 format, to be converted to an int format
$int = $uint16;
if ($uint16 >= 32768) $int = $uint16 - 65536;
return $int;
}
function crc16($data) {
$crc16 = 0xFFFF;
for ($i = 0; $i < strlen($data); $i++)
{
$crc16 ^=ord($data[$i]);
for ($j = 8; $j !=0; $j--)
{
if (($crc16 & 0x0001) !=0)
{
$crc16 >>= 1;
$crc16 ^= 0xA001;
}
else
$crc16 >>= 1;
}
}
return $crc16;
}
?>
// SolarEdge Inverter ModBus/TCP client script / SunSpec protocol
// ing. Ton Rutgers 24-8-2023
// Purpose: Monitor SolarEdge inverters through ModBus/TCP
// Starten op XigmaNAS in een Putty venster:
// /usr/local/bin/php-cgi /mnt/Fileserver/Web/modbusTCP/SolarEdge_modbus_tcp.php
// OR
// php-cgi /mnt/Fileserver/Web/modbusTCP/SolarEdge_modbus_tcp.php
// Stoppen met CTRL-C
// Updates:
// 2023-09-16: using rrdCmd($command) now for rrdtool commands (instead of exec($command)
// rrdtool errors are now logged to rrdtool.log
// corrected socket error message texts
// removed "die()" if a socket could not be created; instead skip this interval
/* References:
https://github.com/sunspec/pysunspec/blob/master/sunspec/core/modbus/client.py -> here the mystery of the requeststring is revealed
https://www.phphulp.nl/php/forum/topic/modbus-tcp-registers-uitlezen-solaredge/103803/last/ -> this was not really helpful
*/
include '/mnt/Fileserver/Web/phpbase/dbConnect.php';
include '/mnt/Fileserver/Web/phpbase/phpFunctions.php';
$debug = 0; // will echo the output also to the terminal
$ipSE12K = "192.168.178.75"; // IP address of SE12.5K inverter
$ipSE10K = "192.168.178.76"; // IP address of SE10K inverter
$serverPort = 1502; // Modbus/TCP port of SolarEdge inverter
$TSformat = "Y-m-d H:i:s"; // timestamp format
error_reporting(E_ALL & ~E_NOTICE); // report all PHP messages, except "Notice"
// open logfile and append
$logfile = fopen("/mnt/Fileserver/Web/modbusTCP/modbusTCP.log", "a") or die("Unable to open file!");
$txt = date($TSformat) . " INFO modbusTCP client started at port $serverPort\n";
fwrite($logfile, $txt);
// get properties of inverters from database, devicetypeID = 3
$conn = OpenCon();
if ($conn->connect_error) {
$txt = date($TSformat) . " ERROR Could not connect to mySQL. Exiting \n";
fwrite($logfile, $txt);
die();
}
$sql = "select deviceID, locatie from device where devicetypeID = 3";
$result = $conn->query($sql);
if ($result->num_rows < 1) {
$txt = date($TSformat) . " ERROR No inverters in database. Exiting \n";
fwrite($logfile, $txt);
die();
}
$i = 0;
while ( $row = $result->fetch_assoc() ) {
$deviceID[$i] = $row["deviceID"];
$IPinverter[$i] = $row["locatie"];
$i++;
}
$nInverter = $i;
CloseCon($conn); // close database connection
for ($i = 0; $i < $nInverter; $i++) {
$txt = date($TSformat) . " INFO logging inverter with deviceID $deviceID[$i] ip $IPinverter[$i] \n";
fwrite($logfile, $txt);
}
// request related parameters
$TCP_HEADER = 0; // no idea what this is for, but it part of the header
$TCP_REQ_LEN = 6; // request length
$id = 1; // inverter device id
$fc = 3; // ?
$regaddr = 40000; // register base address
$length = 122-8; // length of SunSpec Common model plus three-phase inverter model. IN WORDS!
// Note: there are 17 bytes at the end who contain no info, so exclude them
$requestString = pack ("nnnCCnn", $TCP_HEADER, $TCP_HEADER, $TCP_REQ_LEN, $id, $fc, $regaddr, $length);
if ($debug) {
$lenbin = strlen($requestString);
echo "Len of binary string: $lenbin bytes \n";
// decode binary requeststring back into array for verification
$a = unpack ("n1h1/n1h2/n1len/C1id/C1fc/n1address/n1length", $requestString); // but unpack in hi-lo order, so the representaion is as expected
$a1 = implode(",", $a);
print_r ($a);
}
$startTime = round(microtime(true), -1); // seconds since 1-1-1970, rounded to multiples of 10 seconds to align the time to the interval
$interval = 10; // process every 10 seconds;
while (true) { // loop forever
usleep(500000); // go to sleep for 500ms
if (microtime(true) - $startTime >= $interval) {
$startTime += $interval; // renew $starttime
$conn = OpenCon();
if ($conn->connect_error) {
$txt = date($TSformat) . " ERROR Could not connec to mySQL. Skip this interval \n";
fwrite($logfile, $txt);
}
else {
$timestamp = date($TSformat); // use same timestamp for all inverters
for ($i = 0; $i < $nInverter; $i++) {
getLog($deviceID[$i], $IPinverter[$i], $serverPort, $requestString, $length, $conn, $logfile, $timestamp, $TSformat, $debug);
}
}
CloseCon($conn); // close database connection
}
}
fclose($logfile); // this will probably never happen, because of the endless loop above
die("Terminated");
// END OF MAIN LOOP
function getLog($deviceID, $serverIP, $serverPort, $requestString, $length, $conn, $logfile, $timestamp, $TSformat, $debug) {
$RRDNAME = "InvLogReadings$deviceID.rrd"; // include deviceID in rrd log archive
$STORAGE_PATH = "/mnt/Fileserver/Web/www/dbLogging/rrd";
$RRDFILE = "$STORAGE_PATH/archives/$RRDNAME";
if ($debug) {
$t0 = microtime(true); // for time measurement
echo "Get data from deviceID $deviceID ip $serverIP \n";
}
if (!$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) {
$errorcode = socket_last_error();
$errormsg = socket_strerror($errorcode);
$txt = date($TSformat) . " ip=$serverIP socket_create errmsg: $errormsg\n";
fwrite($logfile, $txt);
return;
}
if (!socket_connect($socket, $serverIP, $serverPort)) {
$errorcode = socket_last_error();
$errormsg = socket_strerror($errorcode);
$txt = date($TSformat) . " ip=$serverIP socket_connect errmsg: $errormsg\n";
fwrite($logfile, $txt);
socket_close($socket);
return;
}
if (!socket_write($socket, $requestString)) {
$errorcode = socket_last_error();
$errormsg = socket_strerror($errorcode);
$txt = date($TSformat) . " ip=$serverIP socket_write errmsg: $errormsg\n";
fwrite($logfile, $txt);
socket_close($socket);
return;
}
$buf = "";
// we are expecting exactly $length number of words = 2 * $length bytes
if (!socket_recv($socket, $buf , 2 * $length, MSG_WAITALL )) {
$errorcode = socket_last_error();
$errormsg = socket_strerror($errorcode);
$txt = date($TSformat) . " ip=$serverIP socket_recv errmsg: $errormsg\n";
fwrite($logfile, $txt);
socket_close($socket);
return;
}
socket_close($socket);
// DECODE the buffer
// Note: the first 0 ... 8 (9 bytes) are not part of the model; what are they?
// the first 5 bytes are 0x00
// byte 6 = 247 (0xE7)
// byte 7 = 0x01 -> device ID?
// byte 8 = 0x03 -> model number?
// byte 9 = 244 (0xE4)
// Between the Common model and the inverter model there are bytes who do not belong to a model; they are skipped with a 'filler item'
// At the end of the message there are multiple irrelevant bytes; leave them undecoded
// I did not discover a crc at the end of the message, so we are not going to use the CRC16 function
$buf = substr($buf, 9); // get rid of the first 9 bytes
$data = unpack('a4C_SunSpec_ID/n1C_SunSpec_DID1/n1C_sunSpec_length1/a32C_Manufacturer/a32C_model/a16C_Version/a32C_SerialNumber/n1C_DeviceAddress/a16filler/n1C_SunSpec_DID2/n1C_sunSpec_length2/n1I_AC_Current/n1I_AC_CurrentA/n1I_AC_CurrentB/n1I_AC_CurrentC/n1I_AC_Current_SF/n1I_AC_VoltageAB/n1I_AC_VoltageBC/n1I_AC_VoltageCA/n1I_AC_VoltageAN/n1I_AC_VoltageBN/n1I_AC_VoltageCN/n1I_AC_Voltage_SF/n1I_AC_Power/n1I_AC_Power_SF/n1I_AC_Frequency/n1I_AC_Frequency_SF/n1I_AC_VA/n1I_AC_VA_SF/n1I_AC_VAR/n1I_AC_VAR_SF/n1I_AC_PF/n1I_AC_PF_SF/N1I_AC_Energy_WH/n1I_AC_Energy_WH_SF/n1I_DC_Current/n1I_DC_Current_SF/n1I_DC_Voltage/n1I_DC_Voltage_SF/n1I_DC_Power/n1I_DC_Power_SF/n1I_Temp_Cab/n1I_Temp_Sink/n1I_Temp_Transf/n1I_Temp_Other/n1I_Temp_SF/n1I_Status/n1I_Status_Vendor', $buf);
// correct int's who are unpacked as uint16, because php unpack does not provide unpack of signed short in big endian format
$data["I_AC_Current_SF"] = correctInt($data["I_AC_Current_SF"]);
$data["I_AC_Voltage_SF"] = correctInt($data["I_AC_Voltage_SF"]);
$data["I_AC_Power"] = correctInt($data["I_AC_Power"]);
$data["I_AC_Power_SF"] = correctInt($data["I_AC_Power_SF"]);
$data["I_AC_Frequency_SF"] = correctInt($data["I_AC_Frequency_SF"]);
$data["I_AC_VA"] = correctInt($data["I_AC_VA"]);
$data["I_AC_VA_SF"] = correctInt($data["I_AC_VA_SF"]);
$data["I_AC_VAR"] = correctInt($data["I_AC_VAR"]);
$data["I_AC_VAR_SF"] = correctInt($data["I_AC_VAR_SF"]);
$data["I_AC_PF"] = correctInt($data["I_AC_PF"]);
$data["I_AC_PF_SF"] = correctInt($data["I_AC_PF_SF"]);
$data["I_AC_Energy_WH_SF"] = correctInt($data["I_AC_Energy_WH_SF"]);
$data["I_DC_Current_SF"] = correctInt($data["I_DC_Current_SF"]);
$data["I_DC_Voltage_SF"] = correctInt($data["I_DC_Voltage_SF"]);
$data["I_DC_Power"] = correctInt($data["I_DC_Power"]);
$data["I_DC_Power_SF"] = correctInt($data["I_DC_Power_SF"]);
$data["I_Temp_Cab"] = correctInt($data["I_Temp_Cab"]);
$data["I_Temp_Sink"] = correctInt($data["I_Temp_Sink"]);
$data["I_Temp_Transf"] = correctInt($data["I_Temp_Transf"]);
$data["I_Temp_Other"] = correctInt($data["I_Temp_Other"]);
$data["I_Temp_SF"] = correctInt($data["I_Temp_SF"]);
// append to array $data calculated scaled float values
$data["F_AC_Current"] = scale($data["I_AC_Current"], $data["I_AC_Current_SF"]);
$data["F_AC_CurrentA"] = scale($data["I_AC_CurrentA"], $data["I_AC_Current_SF"]);
$data["F_AC_CurrentB"] = scale($data["I_AC_CurrentB"], $data["I_AC_Current_SF"]);
$data["F_AC_CurrentC"] = scale($data["I_AC_CurrentC"], $data["I_AC_Current_SF"]);
$data["F_AC_CurrentC"] = scale($data["I_AC_CurrentC"], $data["I_AC_Current_SF"]);
$data["F_AC_VoltageAB"] = scale($data["I_AC_VoltageAB"], $data["I_AC_Voltage_SF"]);
$data["F_AC_VoltageBC"] = scale($data["I_AC_VoltageBC"], $data["I_AC_Voltage_SF"]);
$data["F_AC_VoltageCA"] = scale($data["I_AC_VoltageCA"], $data["I_AC_Voltage_SF"]);
$data["F_AC_VoltageAN"] = scale($data["I_AC_VoltageAN"], $data["I_AC_Voltage_SF"]);
$data["F_AC_VoltageBN"] = scale($data["I_AC_VoltageBN"], $data["I_AC_Voltage_SF"]);
$data["F_AC_VoltageCN"] = scale($data["I_AC_VoltageCN"], $data["I_AC_Voltage_SF"]);
$data["F_AC_Power"] = scale($data["I_AC_Power"], $data["I_AC_Power_SF"]);
$data["F_AC_Frequency"] = scale($data["I_AC_Frequency"], $data["I_AC_Frequency_SF"]);
$data["F_AC_VA"] = scale($data["I_AC_VA"], $data["I_AC_VA_SF"]);
$data["F_AC_VAR"] = scale($data["I_AC_VAR"], $data["I_AC_VAR_SF"]);
$data["F_AC_PF"] = scale($data["I_AC_PF"], $data["I_AC_PF_SF"]);
$data["F_AC_Energy_WH"] = scale($data["I_AC_Energy_WH"], $data["I_AC_Energy_WH_SF"]);
$data["F_DC_Current"] = scale($data["I_DC_Current"], $data["I_DC_Current_SF"]);
$data["F_DC_Voltage"] = scale($data["I_DC_Voltage"], $data["I_DC_Voltage_SF"]);
$data["F_DC_Power"] = scale($data["I_DC_Power"], $data["I_DC_Power_SF"]);
$data["F_Temp_Sink"] = scale($data["I_Temp_Sink"], $data["I_Temp_SF"]);
// append to array calculated values for power per phase. The inverter model does not supply these values, but I want them to compare with the smart meter values.
$data["F_AC_PowerA"] = $data["F_AC_CurrentA"] * $data["F_AC_VoltageAN"] * abs($data["F_AC_PF"]/100); // power (W) for phase A, assuming the general power factor is valid for this phase
$data["F_AC_PowerB"] = $data["F_AC_CurrentB"] * $data["F_AC_VoltageBN"] * abs($data["F_AC_PF"]/100); // power (W) for phase B, assuming the general power factor is valid for this phase
$data["F_AC_PowerC"] = $data["F_AC_CurrentC"] * $data["F_AC_VoltageCN"] * abs($data["F_AC_PF"]/100); // power (W) for phase C, assuming the general power factor is valid for this phase
if ($debug) print_r($data);
// Values to be logged in the database, per device (= inverter)
// F_AC_Energy_WH this is a counter, constantly increasing
// F_AC_Power
// F_AC_VA
// F_AC_VAR
// F_AC_PF
// F_DC_Current
// F_DC_Voltage
// F_DC_Power
// F_Temp_Sink
// I_Status
// insert values into database table. Note: timestamp must be enquoted for mysql
$values = "'$timestamp'" . ',' . $deviceID . ',' . $data['F_AC_Energy_WH'] . ',' . $data['F_AC_Power'] . ',' . $data['F_AC_PowerA'] . ',' . $data['F_AC_PowerB'] . ',' . $data['F_AC_PowerC'] . ',' . $data['F_AC_VA'] . ',' . $data['F_AC_VAR'] . ',' . $data['F_AC_PF'] . ',' . $data['F_DC_Current'] . ',' . $data['F_DC_Voltage'] . ',' . $data['F_DC_Power'] . ',' . $data['F_Temp_Sink'] . ',' . $data['I_Status'];
$sql = "INSERT INTO logInverter(timestamp, deviceID, AC_Energy_WH, AC_Power, AC_PowerA, AC_PowerB, AC_PowerC, AC_VA, AC_VAR, AC_PF, DC_Current, DC_Voltage, DC_Power, Temp_Sink, I_Status) VALUES ($values)";
$result = $conn->query($sql);
if ($result != 1) {
$txt = date($TSformat) . " ERROR Insert failure for deviceID $deviceID \n";
fwrite($logfile, $txt);
}
// Store the data also in the RRD archive; create RRD-file if necessary
// Values to be logged in rrd-archive, each device in it's own rrd:
// F_AC_CurrentA
// F_AC_CurrentB
// F_AC_CurrentC
// F_AC_VoltageAN
// F_AC_VoltageBN
// F_AC_VoltageCN
// F_AC_Power
// F_AC_VA
// F_AC_VAR
// F_AC_PF
// F_DC_Current
// F_DC_Voltage
// F_DC_Power
// F_Temp_Sink
if( !file_exists($RRDFILE) ){
// create the RRD database
// RRA 1: 2 day
// RRA 2: 2 week
// RRA 3: 2 month (31 days)
// RRA 4: 2 year (366 days)
$command = "/usr/local/bin/rrdtool \
create $RRDFILE \
--step 10 \
DS:AC_CurrentA:GAUGE:20:0:U \
DS:AC_CurrentB:GAUGE:20:0:U \
DS:AC_CurrentC:GAUGE:20:0:U \
DS:AC_VoltageAN:GAUGE:20:0:U \
DS:AC_VoltageBN:GAUGE:20:0:U \
DS:AC_VoltageCN:GAUGE:20:0:U \
DS:AC_Power:GAUGE:20:U:U \
DS:AC_VA:GAUGE:20:U:U \
DS:AC_VAR:GAUGE:20:U:U \
DS:AC_PF:GAUGE:20:U:U \
DS:DC_Current:GAUGE:20:0:U \
DS:DC_Voltage:GAUGE:20:0:U \
DS:DC_Power:GAUGE:20:U:U \
DS:Temp_Sink:GAUGE:20:U:U \
RRA:AVERAGE:0.5:1:17280 \
RRA:AVERAGE:0.5:7:17280 \
RRA:AVERAGE:0.5:31:17280 \
RRA:AVERAGE:0.5:366:17280";
if (!rrdCmd($command)) {
$txt = date($TSformat) . " ERROR Could not create rrd archive for deviceID $deviceID Check rrdtool.log \n";
fwrite($logfile, $txt);
}
}
// update archive
$values = $data["F_AC_CurrentA"] . ':' . $data["F_AC_CurrentB"] . ':' . $data["F_AC_CurrentC"] . ':' . $data["F_AC_VoltageAN"] . ':' . $data["F_AC_VoltageBN"] . ':' . $data["F_AC_VoltageCN"] . ':' . $data["F_AC_Power"] . ':' . $data["F_AC_VA"] . ':' . $data["F_AC_VAR"] . ':' . $data["F_AC_PF"] . ':' . $data["F_DC_Current"] . ':' . $data["F_DC_Voltage"] . ':' . $data["F_DC_Power"] . ':' . $data["F_Temp_Sink"];
if (!rrdCmd ( "/usr/local/bin/rrdtool update $RRDFILE N:$values" )) {
$txt = date($TSformat) . " ERROR could not update rrd archive for deviceID $deviceID Check rrdtool.log \n";
fwrite($logfile, $txt);
}
if ($debug) {
$timeUsed = microtime(true) - $t0;
echo date($TSformat) . " -> done for $serverIP in $timeUsed seconds\n";
}
}
function scale($inVal, $scalef) {
// purpose: convert integer input value to float, given the scale factor
return (float)($inVal * pow(10, $scalef));
}
function correctInt($uint16) {
// unint16 format, to be converted to an int format
$int = $uint16;
if ($uint16 >= 32768) $int = $uint16 - 65536;
return $int;
}
function crc16($data) {
$crc16 = 0xFFFF;
for ($i = 0; $i < strlen($data); $i++)
{
$crc16 ^=ord($data[$i]);
for ($j = 8; $j !=0; $j--)
{
if (($crc16 & 0x0001) !=0)
{
$crc16 >>= 1;
$crc16 ^= 0xA001;
}
else
$crc16 >>= 1;
}
}
return $crc16;
}
?>
Groet van Ton
Ik ga dit binnenkort eens proberen aan de gang te krijgen.
1. Je moet de inverter enablen voor communicatie via modBus, want die staat standaard uit. Dat kan via het SetApp menu van de inverter. En die kun je benaderen zonder installateursaccount via het ingebouwde wifi access point:
a. Enable wifi direct on the inverter by switching the red toggle switch on the inverter to "P" position for less than 5 seconds.
b. Connect to the inverter access point like you would for a normal wifi network. The wifi password is published at the right side of the inverter.
c. Open up a browser and go to http://172.16.0.1 > Site Communication. From this webpage you can enable modbus TCP without setApp or installer account.
d. Here you also see the default modBus TCP port number and you can change it. Advice: leave the default.
2. Op regel 23 van mijn script staat een hulpscript, waarmee ik inlog op mijn mySQL database. Het bevat twee functies:
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
function OpenCon()
{
$dbhost = "127.0.0.1";
$dbuser = "your_database_name_user";
$dbpass = "your_password";
$db = "your_database_name";
$conn = new mysqli($dbhost, $dbuser, $dbpass,$db) or die("Connect failed: %s\n". $conn -> error);
return $conn;
}
function CloseCon($conn)
{
$conn -> close();
}
?>
function OpenCon()
{
$dbhost = "127.0.0.1";
$dbuser = "your_database_name_user";
$dbpass = "your_password";
$db = "your_database_name";
$conn = new mysqli($dbhost, $dbuser, $dbpass,$db) or die("Connect failed: %s\n". $conn -> error);
return $conn;
}
function CloseCon($conn)
{
$conn -> close();
}
?>
3. Regel 24: nog een setje hulpscripts, waaronder “rrdCmd()” die ik in de regels 253 t/m 307 gebruik. Dat gebruik ik om met “rrdtool” (https://oss.oetiker.ch/rrdtool/doc/index.en.html) de data van de inverters op te slaan en vervolgens grafieken van te maken. Dit is niet noodzakelijk voor de werking, dus kun je er uit slopen.
4. Op regel 26 staat $debug = 0. Als je die op 1 zet, wordt er op een aantal plekken debug info op de console getoond.
5. Op regel 27 en 28 staan de IP adressen van mijn inverters (ik heb er twee). Die regels doen echter niks, omdat ik de invertergegevens uit mijn database ophaal (regels 40 t/m 61). Je zou die regels kunnen vervangen door hard te coderen:
Code (php)
1
2
3
2
3
$deviceID[0] = 0; // wordt op een aantal plekken gebruikt, maar kun je er uit slopen
$IPinverter[0] = 192.168.178.76"; // hier het IP adres van jouw inverter
$nInverter = 1; // het aantal inverters
$IPinverter[0] = 192.168.178.76"; // hier het IP adres van jouw inverter
$nInverter = 1; // het aantal inverters
6. Regel 29 is de TCP/modbus poort (zie hierboven bij het enablen van modBus op de inverter).
7. Regel 78 maakt de binaire TCP request string voor de inverter m.b.v. de php pack functie. N.B. ik heb 3-fase inverters, en daar hoort SunSpec datamodel ‘0103’ bij ->ingesteld op regel 72 en 73. Als je een 1-fase inverter hebt, dan hoort daar datamodel ‘0101’ bij. Ik zie in de Solaredge en in de Sunspec documentatie echter geen layout voor het model ‘0101’, en vermoed dat de modellen ‘0101’ en ‘0103’ hetzelfde zijn, behalve dat dan de data items voor fase 2 en 3 leeg zijn. Maar dat heb ik niet uitgezocht. Solaredge noemt fase 1, 2 en 3 trouwens A, B en C.
8. Regel 89 bepaalt de interval in seconden waarmee de inverter(s) wordt/worden bevraagd. Ik heb gekozen voor 10 seconden, omdat ik in datzelfde tempo mijn slimme meter uitvraag.
9. Regel 91 t/m 110: eindeloze loop waarmee de inverters worden uitgevraagd.
10. Regel 93: om niet constant de CPU te belasten gaat dit script steeds 500ms in slaap
11. De functie getLog() op regel 117 vraagt de inverter om data te sturen, wacht op ontvangst, decodeert de binaire data en logt ze in (in mijn geval) een mySQL database en een rrdtool archief.
12. Regels 127 t/m 164: verbinding opzetten, request sturen en ontvangen data
13. Regel 177 decodeert de binaire data in array $data
14. Regel 179 t/m 200: gegoochel om ‘unsigned int16’ om te zetten naar ‘signed int16’. Ik kon in de php documentatie namelijk geen unpack functie vinden voor ‘signed int16’. Daarom maar gedecodeerd als ‘unsigned int’ en vervolgens omgezet in ‘signed int’.
15. Regel 202 t/m 223: alle numerieke Solaredge data items worden geleverd als integer, met een aparte integer als scale factor. Dat is niet handig, dus dit gedeelte berekent die getallen als float, en plakt ze achteraan in de array $data.
16. Regel 225 t/m 228: ik wilde ook weten wat per fase het geleverde vermogen van mijn inverter is, maar die waarden staan niet in het datamodel. Je kunt ze echter wel berekenen uit de AC_stroom, het AC_voltage en de power factor. Dat gebeurt hier.
17. Regel 244 t/m 251: de data opslaan in mijn mySQL database
18. Regel 253 t/m 307: data opslaan in het rrdtool archief
19. En helemaal onderaan drie hulpfuncties waarvan crc16() niet wordt gebruikt.
20. Tot slot: dit draait bij mij op een XigmaNAS server die24x7 aan is. En die is gebaseerd op FreeBSD. Om te testen draaide ik dit script in een terminal window:
De oneindige loop stop ik met CTRL-C.
Voor de uiteindelijke implementatie heb ik hier een service van gemaakt. Maar dat is platform-specifiek voor FreeBSD, dus ddat laat ik hier even weg.
Succes!
Groet van Ton
Wordt nog wel even een dingetje om fysiek bij de inverter te komen. Dat was al een lastige klimpartij, maar na de verbouwing is er ook nog een wandje toegevoegd. Weliswaar met luik op 3 of 4 meter hoogte, maar daar ben ik nog nooit doorheen gegaan....
Maar ik zal laten weten hoe het afloopt.