samil-power-inverter-data-downloader
Samil Power Inverter Data Downloader
A flow to download model- and operating-data from Solarlake Samil Power Inverters.
How does it work?
- A TCP listener is started on port 60001. The TCP listener waits for connections from Samil Power devices.
- A UDP advertisement package is sent to the broadcast address of the network.
- Samil Power device makes a TCP-connection to port 60001.
- "TCP Listener status" reports a new connection and we can make the data_request.
- "TCP Listener" receives the 1. response, we then parse the data-response and initiate the model_request.
- "TCP Listener" receives the 2. response and we parse the model-response.
How to get started
- Before running the flow for the first time you have to set the broadcast-address to that of your network.
Tested on
Since I only have access to a Samil Power 10000TL-PM that's the only device I could test it with.
References
The functionality of this flow is based on the previous work by semonet. Particularly the file samil.py.
https://github.com/semonet/solarhttps://github.com/semonet/solar
[{"id":"34062c9b.ab3c44","type":"tab","label":"samil-power-inverter","disabled":false,"info":""},{"id":"c551d72e.57d17","type":"tcp in","z":"34062c9b.ab3c44","name":"TCP listener","server":"server","host":"","port":"60001","datamode":"stream","datatype":"buffer","newline":"","topic":"","base64":false,"x":130,"y":280,"wires":[["8a0835d2.335a48"]]},{"id":"a19633a.247865","type":"debug","z":"34062c9b.ab3c44","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":330,"y":80,"wires":[]},{"id":"8488bafb.789af","type":"catch","z":"34062c9b.ab3c44","name":"","scope":null,"uncaught":false,"x":120,"y":80,"wires":[["a19633a.247865"]]},{"id":"b427f914.680258","type":"udp out","z":"34062c9b.ab3c44","name":"","addr":"192.168.0.255","iface":"","port":"60000","ipv":"udp4","outport":"","base64":false,"multicast":"broad","x":610,"y":180,"wires":[]},{"id":"8a0835d2.335a48","type":"function","z":"34062c9b.ab3c44","name":"Set request_type","func":"\nfunction bufFromBuffer (buf, start, end) {\n var rv = Buffer.alloc(end-start);\n buf.copy( rv, 0, start, end );\n return rv;\n}\n\nfunction intFromBuffer (buf, start, end) {\n return bufFromBuffer(buf, start, end).readUIntBE(0,end-start);\n}\n\nvar request_type = intFromBuffer(msg.payload, 3, 4);\n//node.warn(request_type);\nif (request_type === 139) {\n // model_request \n msg.request_type = 'model_request';\n return [ msg, null ];\n} else if (request_type === 91) {\n // data_request\n msg.request_type = 'data_request';\n return [ null, msg ];\n} else {\n node.warn(\"Unknown request: \" + msg.payload);\n return [ null, null ];\n}\n","outputs":2,"noerr":0,"initialize":"","finalize":"","x":370,"y":280,"wires":[["9f265c62.e8ad98"],["17fd9890.cee337"]],"outputLabels":["model_request","data_request"]},{"id":"cbc7a8d5.1339a8","type":"inject","z":"34062c9b.ab3c44","name":"Send advertisement","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"reset","payloadType":"str","x":150,"y":180,"wires":[["ef6eb70.c2edf48"]]},{"id":"ef6eb70.c2edf48","type":"function","z":"34062c9b.ab3c44","name":"advertisement","func":"msg.payload = Buffer.from([85,170,0,12,1,0,0,0,0,7,0,0,0,0,1,19]);\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":360,"y":180,"wires":[["b427f914.680258"]]},{"id":"7b3f455.9f817bc","type":"status","z":"34062c9b.ab3c44","name":"TCP listener status","scope":["c551d72e.57d17"],"x":150,"y":440,"wires":[["2eda91da.f9fa8e","be1199ed.420838"]]},{"id":"2eda91da.f9fa8e","type":"switch","z":"34062c9b.ab3c44","name":"status: connect","property":"status.event","propertyType":"msg","rules":[{"t":"eq","v":"connect","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":360,"y":480,"wires":[["cd140e90.39a128"]]},{"id":"654b5c26.e9ddf4","type":"function","z":"34062c9b.ab3c44","name":"model_request","func":"msg.payload = Buffer.from([85,170,0,17,1,0,0,0,0,0,0,0,2,128,1,0,0,0,122,2,14]);\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":840,"y":400,"wires":[["714ce327.f1deec"]]},{"id":"714ce327.f1deec","type":"tcp out","z":"34062c9b.ab3c44","host":"","port":"","beserver":"reply","base64":false,"end":false,"name":"TCP send request","x":1070,"y":480,"wires":[]},{"id":"17fd9890.cee337","type":"function","z":"34062c9b.ab3c44","name":"parse data_request","func":"\nfunction bufFromBuffer (buf, start, end) {\n var rv = Buffer.alloc(end-start);\n buf.copy( rv, 0, start, end );\n return rv;\n}\n\nfunction intFromBuffer (buf, start, end) {\n return bufFromBuffer(buf, start, end).readUIntBE(0,end-start);\n}\n\nfunction stringFromBuffer (buf, start, end) {\n return bufFromBuffer(buf, start, end).toString('utf-8');\n}\n\nif (Buffer.isBuffer(msg.payload)) {\n var data = msg.payload;\n //var response_header = Buffer.alloc(15);\n //data.copy(response_header,0,2,17);\n //msg.response_header = bufFromBuffer(data, 2, 17);\n //var response_payload_size = intFromBuffer(data, 18, 19);\n var response_payload = bufFromBuffer(data, 19, data.length-2);\n var op_modes = {0: 'wait', 1: 'normal', 5: 'pv_power_off'};\n var op_mode;\n try {\n op_mode = op_modes[intFromBuffer(response_payload, 48, 50)];\n } catch (e) {\n op_mode = stringFromBuffer(response_payload, 48, 50);\n }\n msg.result = {\n 'ambiant_temp': intFromBuffer(response_payload, 0, 2) / 10.0,\n 'inverter_temp': intFromBuffer(response_payload, 14, 16) / 10.0,\n 'pv1_voltage': intFromBuffer(response_payload, 2, 4) / 10.0,\n 'pv2_voltage': intFromBuffer(response_payload, 4, 6) / 10.0, \n 'pv1_current': intFromBuffer(response_payload, 6, 8) / 10.0,\n 'pv2_current': intFromBuffer(response_payload, 8, 10) / 10.0, \n 'total_operation_hours': intFromBuffer(response_payload, 38, 42), \n 'operating_mode': op_mode,\n 'energy_today': intFromBuffer(response_payload, 42, 44) / 100.0,\n 'grid_current': intFromBuffer(response_payload, 22, 24) / 10.0,\n 'grid_voltage': intFromBuffer(response_payload, 18, 20) / 10.0,\n 'grid_frequency': intFromBuffer(response_payload, 20, 22) / 100.0,\n 'output_power': intFromBuffer(response_payload, 44, 48),\n 'energy_total': intFromBuffer(response_payload, 34, 38) / 10.0,\n }\n msg.payload = [msg.result];\n}\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":590,"y":320,"wires":[["654b5c26.e9ddf4","ec2882d3.382388"]]},{"id":"cd140e90.39a128","type":"function","z":"34062c9b.ab3c44","name":"data_request","func":"msg.payload = Buffer.from([85,170,0,17,1,0,0,0,0,0,0,3,2,128,1,3,232,0,74,2,204]);\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":830,"y":480,"wires":[["714ce327.f1deec"]]},{"id":"be1199ed.420838","type":"switch","z":"34062c9b.ab3c44","name":"status: disconnect","property":"status.event","propertyType":"msg","rules":[{"t":"eq","v":"disconnect","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":370,"y":400,"wires":[["88c4283f.820488"]]},{"id":"88c4283f.820488","type":"debug","z":"34062c9b.ab3c44","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"status","targetType":"msg","statusVal":"","statusType":"auto","x":570,"y":400,"wires":[]},{"id":"9f265c62.e8ad98","type":"function","z":"34062c9b.ab3c44","name":"parse model_request","func":"\nfunction bufFromBuffer (buf, start, end) {\n var rv = Buffer.alloc(end-start);\n buf.copy( rv, 0, start, end );\n return rv;\n}\n\nfunction stringFromBuffer (buf, start, end) {\n return bufFromBuffer(buf, start, end).toString('utf-8');\n}\n\n/*\nresult = {\n 'model_number': str(payload[12:22], 'utf-8'),\n 'manufacturer': str(payload[40:50], 'utf-8'),\n 'serial_number': str(payload[72:82], 'utf-8')\n}\n*/\n\nif (Buffer.isBuffer(msg.payload)) {\n var data = msg.payload;\n //var response_header = Buffer.alloc(15);\n //data.copy(response_header,0,2,17);\n //msg.response_header = bufFromBuffer(data, 2, 17);\n //var response_payload_size = intFromBuffer(data, 18, 19);\n var response_payload = bufFromBuffer(data, 19, data.length-2);\n msg.result = {\n 'model_number': stringFromBuffer(response_payload, 12, 22),\n 'manufacturer': stringFromBuffer(response_payload, 40, 50),\n 'serial_number': stringFromBuffer(response_payload, 72, 82)\n }\n} else {\n msg = null;\n}\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":600,"y":240,"wires":[["ec2882d3.382388"]]},{"id":"ec2882d3.382388","type":"debug","z":"34062c9b.ab3c44","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"result","targetType":"msg","statusVal":"","statusType":"auto","x":830,"y":280,"wires":[]},{"id":"b0af667d.823ee8","type":"comment","z":"34062c9b.ab3c44","name":"README","info":"# Samil Power Inverter Data Downloader\n\nA flow to download model- and operating-data from Solarlake Samil Power Inverters.\n\n## How does it work?\n\n1. A TCP listener is started on port 60001. The TCP listener waits for connections from Samil Power devices.\n2. A UDP advertisement package is sent to the broadcast address of the network.\n3. Samil Power device makes a TCP-connection to port 60001.\n4. \"TCP Listener status\" reports a new connection and we can make the data_request.\n5. \"TCP Listener\" receives the 1. response, we then parse the data-response and initiate the model_request.\n6. \"TCP Listener\" receives the 2. response and we parse the model-response.\n\n## How to get started\n\n- Before running the flow for the first time you have to set the broadcast-address to that of your network. \n\n## Tested on \n\nSince I only have access to a Samil Power 10000TL-PM that's the only device I could test it with. \n\n## References\n\nThe functionality of this flow is based on the previous work by semonet. Particularly the file samil.py.\n\n[https://github.com/semonet/solar](https://github.com/semonet/solar)https://github.com/semonet/solar","x":120,"y":40,"wires":[]}]