Python SolarEdge modbus script via lan
Posted: Sunday 23 February 2020 19:53
Hi,
Last year our member Micha123 posted a link to a Python library for the SolarEdge inverter family.
With some support of ardexa I managed to get the Domoticz Json calls in the script and now got the first version working.
For reading the modbus from your local network you need to enable the SolarEdge inverter Modbus over lan.
Please find below the initial description on how to install. I am still building so if something is missing/not working/unclear
Please let me know and I will update this post so we make life easier for others.
2020-08-11 THanks Strebrah !
Strebrah wrote:
Sunday 09 August 2020 17:20
Hi,
I had some difficulties getting the proposed script working.
So, over the weekend, I created my own Solaredge Modbus TCP to Domoticz python code.
I uploaded it to github; https://github.com/strebrah/Solaredge_Domoticz_Modbus
Maybe someone will find this useful
2020-2-25 Thanks for that Jos !
Updated the script with the suggestion of Jos.
<Jos>
I have been playing with your modified script and changed it a little to combine the W and WH values into one Domoticz Instant&Counter device instead of having them in separate devices.
https://www.domoticz.com/wiki/Domoticz_ ... counter.29
</jos>
2020-02-27 Thanks Telewy !
If the script does not connect to the inverter with the message : Cannot find the address: 1
Then please in the file /usr/local/lib/python2.7/dist-packages/sunspec/core/client.py
at line 333 insert the sleep command :
time.sleep(0.1)
2020-02-27
Updated with Crontab info
2020-03-05 THanks Gehoel !
Updated with Virtual sensor
**********************************************
How to install
**********************************************
Goto this webpage and read the project.
https://pypi.org/project/sunspec-ardexa/
Ready to go? Exec the below commands on your Raspberry Pi or linux system.
git clone --recursive https://github.com/sunspec/pysunspec.git
cd pysunspec
sudo python setup.py install
sudo pip install sunspec_ardexa
cd /home/pi/domoticz/scripts/
nano domo.ini
past the below lines in domo.ini
First change the ipaddress to your own Domoticz.
If you changed the default port, please change as well.
When you see :
Amps:0 ; Amps (A) Total Amps
change the 0 into the idx of the Domoticz sensor.
If you leave the 0 in , the value will not be updated in Domoticz.
You can do this for all sensor data available.
Perhaps start with a single sensor to see if you get the total working?
Most sensors are of the 'TEXT' type but others are 'Usage' Type where you will have a day counter and total counter.
You have to create the sensors yourself !!!!! Not done by the script.
See the comment in the domo.in for a clue what the sensor is reporting.
In order to get an IDX to fill out in the Domo.ini we do need to create a virtual sensor in Domoticz first.
The IDX you fill out in the domo.ini has to match the virtual sensor.
Now change directory to
cd /usr/local/lib/python2.7/dist-packages
and locate the:
sunspec_ardexa.py
file.
copy the file as a backup.
sudo cp sunspec_ardexa.py sunspec_ardexa.py.orig
Open the sunspec_ardexa.py file in your favorite editor and past the below code over it.
So below code completely replaces the orignal content of sunspec_ardexa.py.
Done everything? Lets do a first test :
From the commandline exe :
>> sunspec_ardexa discover 192.168.1.140 1
where the ipaddress is your Solaredge inverter and the 1 is your inverter.
If all went well the idx you adressed is updated with the realtime value.
if nothing happens ??
exec the follwing command line and see the screen for output.
>> >> sunspec_ardexa -vv discover 192.168.1.140 1
-----------------------------------
If you like the script running from a crontab please use the complete path :
crontab -e
*/1 * * * * /usr/local/bin/sunspec_ardexa discover 192.168.1.140 1 > /dev/null 2>&1
----------------------------------
If the script does not connect to the inverter with the message : Cannot find the address: 1
Then please in the file /usr/local/lib/python2.7/dist-packages/sunspec/core/client.py
at line 333 insert the sleep command :
time.sleep(0.1)
---------------------------------
Again, first run, please let me know any issues or questions so I can update this post.
Enjoy
Frank
Last year our member Micha123 posted a link to a Python library for the SolarEdge inverter family.
With some support of ardexa I managed to get the Domoticz Json calls in the script and now got the first version working.
For reading the modbus from your local network you need to enable the SolarEdge inverter Modbus over lan.
Please find below the initial description on how to install. I am still building so if something is missing/not working/unclear
Please let me know and I will update this post so we make life easier for others.
2020-08-11 THanks Strebrah !
Strebrah wrote:
Sunday 09 August 2020 17:20
Hi,
I had some difficulties getting the proposed script working.
So, over the weekend, I created my own Solaredge Modbus TCP to Domoticz python code.
I uploaded it to github; https://github.com/strebrah/Solaredge_Domoticz_Modbus
Maybe someone will find this useful
2020-2-25 Thanks for that Jos !
Updated the script with the suggestion of Jos.
<Jos>
I have been playing with your modified script and changed it a little to combine the W and WH values into one Domoticz Instant&Counter device instead of having them in separate devices.
https://www.domoticz.com/wiki/Domoticz_ ... counter.29
</jos>
2020-02-27 Thanks Telewy !
If the script does not connect to the inverter with the message : Cannot find the address: 1
Then please in the file /usr/local/lib/python2.7/dist-packages/sunspec/core/client.py
at line 333 insert the sleep command :
time.sleep(0.1)
2020-02-27
Updated with Crontab info
2020-03-05 THanks Gehoel !
Updated with Virtual sensor
**********************************************
How to install
**********************************************
Goto this webpage and read the project.
https://pypi.org/project/sunspec-ardexa/
Ready to go? Exec the below commands on your Raspberry Pi or linux system.
git clone --recursive https://github.com/sunspec/pysunspec.git
cd pysunspec
sudo python setup.py install
sudo pip install sunspec_ardexa
cd /home/pi/domoticz/scripts/
nano domo.ini
past the below lines in domo.ini
Code: Select all
[domohost]
;ip address and portnumer of Domoticz
domoip:192.168.1.155 ;Domoticz ip address
domoport:8080 ;Domoticz Port
;The idx numbers of the sensors made in Domoticz
;if idx is set to 0 (zero) it will be ignored
[domoidx]
Amps:0 ; Amps (A) Total Amps
AphA:0 ; Amps PhaseA inverter
AphB:0 ; Amps PhaseB inverter
AphC:0 ; Amps PhaseC inverter
PPVphAB:0 ; Phase Voltage AB inverter
PPVphBC:0 ; Phase Voltage BC inverter
PPVphCA:0 ; Phase Voltage CA inverter
PhVphA:0 ; Phase Voltage AN grid
PhVphB:0 ; Phase Voltage BN grid
PhVphC:0 ; Phase Voltage CN grid
Watts:0 ; Watts inverter
Hz:0 ; Hz Grid
VA:0 ; VA
VAr:0 ; VAr
PF:0 ; Percent
WH:0 ; WattHours (WH)
DCA:0 ; DC Amps
DCV:0 ; DC Voltage
TmpSnk:0 ; Heat Sink Temperature
St:0 ; Operating State
If you changed the default port, please change as well.
When you see :
Amps:0 ; Amps (A) Total Amps
change the 0 into the idx of the Domoticz sensor.
If you leave the 0 in , the value will not be updated in Domoticz.
You can do this for all sensor data available.
Perhaps start with a single sensor to see if you get the total working?
Most sensors are of the 'TEXT' type but others are 'Usage' Type where you will have a day counter and total counter.
You have to create the sensors yourself !!!!! Not done by the script.
See the comment in the domo.in for a clue what the sensor is reporting.
In order to get an IDX to fill out in the Domo.ini we do need to create a virtual sensor in Domoticz first.
The IDX you fill out in the domo.ini has to match the virtual sensor.
Now change directory to
cd /usr/local/lib/python2.7/dist-packages
and locate the:
sunspec_ardexa.py
file.
copy the file as a backup.
sudo cp sunspec_ardexa.py sunspec_ardexa.py.orig
Open the sunspec_ardexa.py file in your favorite editor and past the below code over it.
So below code completely replaces the orignal content of sunspec_ardexa.py.
Code: Select all
"""
To discover devices: sunspec_ardexa discover IP_address/Device_Node Bus_Addresses
Example 1: sunspec_ardexa discover 192.168.1.3 1-5
Example 2: sunspec_ardexa discover 192.168.1.3 1,3-5 --port=502
Example 3: sunspec_ardexa discover /dev/ttyUSB0 1,3,5 --baud 115200
Example 4: sunspec_ardexa discover /dev/ttyUSB0 1
To send production data to a file on disk: sunspec_ardexa log IP_address/Device_Node Bus_Addresses Output_directory
Example 1: sunspec_ardexa log 192.168.1.3 1-5 /opt/ardexa
Example 2: sunspec_ardexa log 192.168.1.3 1,3-5 /opt/ardexa --port=502
Example 3: sunspec_ardexa log /dev/ttyUSB0 1,3,5 /opt/ardexa --baud 115200
Example 4: sunspec_ardexa log /dev/ttyUSB0 1 /opt/ardexa
"""
# Copyright (c) 2018 Ardexa Pty Ltd
#
# This code is licensed under the MIT License (MIT).
#
# 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.
#
from __future__ import print_function
import sys
import time
import os
import socket
import click
import ardexaplugin as ap
import sunspec.core.client as sp_client
PY3K = sys.version_info >= (3, 0)
PIDFILE = '/tmp/sunspec-ardexa-'
SINGLE_PHASE_INVERTER_101 = 101
THREE_PHASE_INVERTER_103 = 103
THREE_PHASE_INVERTER_113 = 113
INVERTER_STRINGS = 160
STRING_COMBINER = 403
STORAGE = 124
TIMEOUT_VAL = 3.0
ATTEMPTS = 1
#Domoticz version 1.1 2020 02 25
SAVE_W = 0
#Dictonary to convert name into IDX for usage with Domoticz
DOMO_DEF ={'A':0,'APHA':0,'APHB':0,'APHC':0,'PPVPHAB':0,'PPVPHBC':0,'PPVPHCA':0,'PHVPHA':0,'PHVPHB':0,'PHVPHC':0,'W':0,
'HZ':0,'VA':0,'VAR':0,'PF':0,'WH':0,'DCA':0,'DCV':0,'TMPSNK':0,'ST':0}
#Modification made for usage Domoticz
import requests
import ConfigParser
#Reading config parameters for usage Domoticz
config = ConfigParser.ConfigParser()
config.readfp(open(r'/home/pi/domoticz/scripts/domo.ini'))
domoip = config.get('domohost', 'domoip')
domoport = config.get('domohost', 'domoport')
DOMO_DEF['A'] = config.get('domoidx', 'Amps')
DOMO_DEF['APHA'] = config.get('domoidx', 'AphA')
DOMO_DEF['APHB']= config.get('domoidx', 'AphB')
DOMO_DEF['APHC'] = config.get('domoidx', 'AphC')
DOMO_DEF['PPVPHAB'] = config.get('domoidx', 'PPVphAB')
DOMO_DEF['PPVPHBC'] = config.get('domoidx', 'PPVphBC')
DOMO_DEF['PPVPHCA'] = config.get('domoidx', 'PPVphCA')
DOMO_DEF['PHVPHA'] = config.get('domoidx', 'PhVphA')
DOMO_DEF['PHVPHB'] = config.get('domoidx', 'PhVphB')
DOMO_DEF['PHVPHC'] = config.get('domoidx', 'PhVphC')
DOMO_DEF['W'] = config.get('domoidx', 'Watts')
DOMO_DEF['HZ'] = config.get('domoidx', 'Hz')
DOMO_DEF['VA'] = config.get('domoidx', 'VA')
DOMO_DEF['VAR'] = config.get('domoidx', 'VAr')
DOMO_DEF['PF'] = config.get('domoidx', 'PF')
DOMO_DEF['WH'] = config.get('domoidx', 'WH')
DOMO_DEF['DCA'] = config.get('domoidx', 'DCA')
DOMO_DEF['DCV'] = config.get('domoidx', 'DCV')
DOMO_DEF['TMPSNK'] = config.get('domoidx', 'TmpSnk')
DOMO_DEF['ST'] = config.get('domoidx', 'St')
# This is the dictionary and list for a Single and Three Phase Inverter (101, 103 AND 113)
DICT_INVERTER = {'A' : 'AC Current (A)', 'APHA' : 'AC Current 1 (A)', 'APHB' : 'AC Current 2 (A)', 'APHC' : 'AC Current 3 (A)',
'PPVPHAB' : 'AC Voltage 12 (A)', 'PPVPHBC' : 'AC Voltage 23 (A)', 'PPVPHCA' : 'AC Voltage 31 (A)',
'PHVPHA' : 'AC Voltage 1 (A)', 'PHVPHB' : 'AC Voltage 2 (A)', 'PHVPHC' : 'AC Voltage 3 (A)',
'W' : 'AC Power (W)', 'HZ' : 'Grid Freq (Hz)', 'PF' : 'Cos Phi', 'WH' : 'Total Energy (Wh)', 'DCA' : 'DC Current 1 (A)',
'DCV' : 'DC Voltage 1 (V)', 'DCW' : 'DC Power (W)', 'TMPCAB' : 'Cabinet Temperature (C)', 'TMPSNK' : 'Heat Sink Temperature (C)',
'TMPTRNS' : 'Transformer Temperature (C)', 'TMPOT' : 'Other Temperature (C)', 'ST' : 'Operating State',
'STVND' : 'Vendor Operating State', 'EVT1' : 'Event1'}
LIST_INVERTER = ['A', 'APHA', 'APHB', 'APHC', 'PPVPHAB', 'PPVPHBC', 'PPVPHCA', 'PHVPHA', 'PHVPHB', 'PHVPHC', 'W', 'HZ', 'PF', 'WH',
'DCA', 'DCV', 'DCW', 'TMPCAB', 'TMPSNK', 'TMPTRNS', 'TMPOT', 'ST', 'STVND', 'EVT1']
# This is the dictionary and list for a Storage (124)
DICT_STORAGE = {'WCHAMAX' : 'SetPt Max Charge (W)', 'STORCTL_MOD' : 'Storage Mode', 'CHASTATE' : 'State of Charge (%)',
'INBATV' : 'Battery Voltage (V)', 'CHAST' : 'Charge Status', 'OUTWRTE' : 'Discharge Rate (%)', 'INWRTE' : 'Charge Rate (%)',
'STROAVAL' : 'Available Energy (AH)'}
LIST_STORAGE = ['WCHAMAX', 'STORCTL_MOD', 'CHASTATE', 'INBATV', 'CHAST', 'OUTWRTE', 'INWRTE', 'STROAVAL']
# This is the dictionary and list for a MPPT Inverter Extension (160)
DICT_STRINGS = {'Evt' : 'Global Events', 'N' : 'Number of Modules'}
LIST_STRINGS = ['Evt', 'N']
DICT_STRINGS_REPEATING = {'ID' : 'Module ID', 'DCA' : 'DC Current (A)', 'DCV' : 'DC Voltage (V)', 'DCW' : 'DC Power (W)',
'TMP' : 'Temperature', 'DCST' : 'Operating State', 'DCEVT' : 'Module Events'}
# This is the dictionary and list for a String Combiner (403)
DICT_COMBINER = {'Evt' : 'Global Events', 'N' : 'Number of Modules', 'DCA' : 'DC Current (A)', 'TMP' : 'Temperature'}
LIST_COMBINER = ['Evt', 'N', 'DCA', 'TMP']
DICT_COMBINER_REPEATING = {'INID' : 'Module ID', 'INDCA' : 'DC Current (A)', 'INEVT' : 'Module Events'}
###~~~~~~~~~~~~~~~~~~~
DICT_EVT1 = {0 : 'Ground fault', 1 : 'DC over voltage', 2 : 'AC disconnect open', 3 : 'DC disconnect open', 4 : 'Grid disconnect',
5 : 'Cabinet open', 6 : 'Manual shutdown', 7 : 'Over temperature', 8 : 'Frequency above limit', 9 : 'Frequency under limit',
10 : 'AC Voltage above limit', 11 : 'AC Voltage under limit', 12 : 'Blown String fuse on input', 13 : 'Under temperature',
14 : 'Generic Memory or Communication error (internal)', 15 : 'Hardware test failure'}
DICT_STORCTL_MOD = {0 : 'Charge', 1 : 'Discharge'}
DICT_ST = {1 : 'Off', 2 : 'Sleeping', 3 : 'Starting', 4 : 'MPPT', 5 : 'Throttled', 6 : 'Shutting down', 7 : 'Fault', 8 : 'Standby'}
DICT_CHAST = {1 : 'Off', 2 : 'Empty', 3 : 'Discharging', 4 : 'Charging', 5 : 'Full', 6 : 'Holding', 7 : 'Testing'}
DICT_EVT = {0 : 'Ground Fault', 1 : 'Input Over Voltage', 19 : 'Reserved', 3 : 'DC Disconnect', 5 : 'Cabinet Open', 6 : 'Manual Shutdown', \
7 : 'Over Temperature', 12 : 'Blown Fuse', 13 : 'Under Temperature', 14 : 'Memory Loss', 15 : 'Arc Detection', 20 : 'Test Failed', \
21 : 'Under Voltage', 22 : 'Over Current'}
DICT_DCST = {1 : 'Off', 2 : 'Sleeping', 3 : 'Starting', 4 : 'MPPT', 5 : 'Throttled', 6 : 'Shutting down', 7 : 'Fault', 8 : 'Standby',
9 : 'Test', 19 : 'Reserved'}
DICT_DCEVT = {0 : 'Ground Fault', 1 : 'Input Over Voltage', 19 : 'Reserved', 3 : 'DC Disconnect', 5 : 'Cabinet Open', 6 : 'Manual Shutdown',\
7 : 'Over Temperature', 12 : 'Blown Fuse', 13 : 'Under Temperature', 14 : 'Memory Loss', 15 : 'Arc Detection', 20 : 'Test Failed',\
21 : 'Under Voltage', 22 : 'Over Current'}
def write_line(line, log_directory, header_line, debug):
"""This will write a line to the base_directory
Assume header and lines are already \n terminated"""
# Write the log entry, as a date entry in the log directory
date_str = (time.strftime("%Y-%b-%d"))
log_filename = date_str + ".csv"
ap.write_log(log_directory, log_filename, header_line, line, debug, True, log_directory, "latest.csv")
return True
#call url and write message
def domo(domoname,domovalue):
global SAVE_W
if domoname in DOMO_DEF:
domoidx = DOMO_DEF[domoname]
if domoidx != '0':
if domoname == "W":
SAVE_W = domovalue #Save Wattage
elif domoname == "WH":
urlmessage = 'http://'+domoip+':'+domoport+'/json.htm?type=command¶m=udevice&idx='+str(DOMO_DEF["WH"])+'&svalue='+str(SAVE_W)+';'+str(domovalue)
r=requests.get(urlmessage)
else:
urlmessage = 'http://'+domoip+':'+domoport+'/json.htm?type=command¶m=udevice&idx='+str(domoidx)+'&svalue='+str(domovalue)
r=requests.get(urlmessage)
else:
return True
return True
def discover_devices(device_node, modbus_address, conn_type, baud, port, debug):
"""This function will discover all the Sunspec devices"""
try:
if conn_type == 'tcp':
port_int = int(port)
sunspec_client = sp_client.SunSpecClientDevice(sp_client.TCP, modbus_address, ipaddr=device_node, ipport=port_int, timeout=TIMEOUT_VAL)
elif conn_type == 'rtu':
sunspec_client = sp_client.SunSpecClientDevice(sp_client.RTU, modbus_address, name=device_node, baudrate=baud, timeout=TIMEOUT_VAL)
# read all models in the device
sunspec_client.read()
except:
if debug > 0:
print("Cannot find the address: ", modbus_address)
return False
if debug > 0:
print("Found a device at address: ", modbus_address)
for model in sunspec_client.device.models_list:
# Name may not exist
try:
if model.model_type.name:
if debug > 0:
print("\nName: ", model.model_type.name, "\tSunspec Id: ", model.model_type.id, "\tLabel: ", model.model_type.label)
except:
pass
for block in model.blocks:
for point in block.points_list:
if point.value is not None:
label = ""
suns_id = (point.point_type.id).strip().upper()
if point.point_type.label:
label = point.point_type.label
name = label + " (" + point.point_type.id + ")"
units = point.point_type.units
if not units:
units = ""
value = point.value
# Replace numbered status/events with description
value = convert_value(suns_id, value)
if debug > 0:
print('\t%-40s %20s %-10s' % (name, value, str(units)))
domo(suns_id,value)
return True
def extract_160_data(model, list_dev, dict_dev, debug):
"""This function will extract the data from a type 160 device
It is different in that this type has a repeating block"""
data_list = [""] * len(list_dev)
header_list = list_dev[:]
for idx, val in enumerate(header_list):
if val in dict_dev:
header_list[idx] = dict_dev[val]
else:
header_list[idx] = None
if debug > 0:
print("\nName: ", model.model_type.name, "\tSunspec Id: ", model.model_type.id, "\tLabel: ", model.model_type.label)
for block in model.blocks:
for point in block.points_list:
suns_id = (point.point_type.id).strip().upper()
# Check the value is present in the list we require
if suns_id in dict_dev:
value = ""
if point.value is not None:
value = point.value
# Replace numbered status/events with description
value = convert_value(suns_id, value)
# Put the data in the data_list
index = list_dev.index(suns_id)
data_list[index] = str(value)
if debug > 0:
print('\t%-20s %20s' % (suns_id, value))
# Else, if its in the repeating block, create a header AND data item
elif suns_id in DICT_STRINGS_REPEATING:
value = ""
if point.value is not None:
value = point.value
# Replace numbered status/events with description
value = convert_value(suns_id, value)
# ***Append*** to the data_list **AND** the header_list
header_item = DICT_STRINGS_REPEATING[suns_id]
header_list.append(header_item)
data_list.append(str(value))
if debug > 0:
print('\t%-20s %20s' % (suns_id, value))
# Add a datetime and Log the line
dt = ap.get_datetime_str()
data_list.insert(0, dt)
header_list.insert(0, 'Datetime')
# Formulate the line
line = ", ".join(data_list) + "\n"
# And the header line
header = "# " + ", ".join(header_list) + "\n"
return header, line
def extract_403_data(model, list_dev, dict_dev, debug):
"""This function will extract the data from a type 403 device
It is different in that this type has a repeating block"""
data_list = [""] * len(list_dev)
header_list = list_dev[:]
for idx, val in enumerate(header_list):
if val in dict_dev:
header_list[idx] = dict_dev[val]
else:
header_list[idx] = None
if debug > 0:
print("\nName: ", model.model_type.name, "\tSunspec Id: ", model.model_type.id, "\tLabel: ", model.model_type.label)
for block in model.blocks:
for point in block.points_list:
suns_id = (point.point_type.id).strip().upper()
# Check the value is present in the list we require
if suns_id in dict_dev:
value = ""
if point.value is not None:
value = point.value
# Replace numbered status/events with description
value = convert_value(suns_id, value)
# Put the data in the data_list
index = list_dev.index(suns_id)
data_list[index] = str(value)
if debug > 0:
print('\t%-20s %20s' % (suns_id, value))
# Else, if its in the repeating block, create a header AND data item
elif suns_id in DICT_COMBINER_REPEATING:
value = ""
if point.value is not None:
value = point.value
# Replace numbered status/events with description
value = convert_value(suns_id, value)
# ***Append*** to the data_list **AND** the header_list
header_item = DICT_COMBINER_REPEATING[suns_id]
header_list.append(header_item)
data_list.append(str(value))
if debug > 0:
print('\t%-20s %20s' % (suns_id, value))
# Add a datetime and Log the line
dt = ap.get_datetime_str()
data_list.insert(0, dt)
header_list.insert(0, 'Datetime')
# Formulate the line
line = ", ".join(data_list) + "\n"
# And the header line
header = "# " + ", ".join(header_list) + "\n"
return header, line
def extract_data(model, list_dev, dict_dev, debug):
"""This function will extract the data from a device"""
data_list = [""] * len(list_dev)
header_list = list_dev[:]
for idx, val in enumerate(header_list):
if val in dict_dev:
header_list[idx] = dict_dev[val]
else:
header_list[idx] = None
if debug > 0:
print("\nName: ", model.model_type.name, "\tSunspec Id: ", model.model_type.id, "\tLabel: ", model.model_type.label)
for block in model.blocks:
for point in block.points_list:
suns_id = (point.point_type.id).strip().upper()
# Check the value is present in the list we require
if suns_id in dict_dev:
value = ""
if point.value is not None:
value = point.value
# Replace numbered status/events with description
value = convert_value(suns_id, value)
# Put the data in the data_list
index = list_dev.index(suns_id)
data_list[index] = str(value)
if debug > 0:
print('\t%-20s %20s' % (suns_id, value))
# Add a datetime and Log the line
dt = ap.get_datetime_str()
data_list.insert(0, dt)
header_list.insert(0, 'Datetime')
# Formulate the line
line = ", ".join(data_list) + "\n"
# And the header line
header = "# " + ", ".join(header_list) + "\n"
return header, line
def convert_value(name, value):
"""This will look up and replace numbers with descriptions"""
if name == 'EVT1' and value in DICT_EVT1:
value = DICT_EVT1[value]
elif name == 'ST' and value in DICT_ST:
value = DICT_ST[value]
elif name == 'STORCTL_MOD' and value in DICT_STORCTL_MOD:
value = DICT_STORCTL_MOD[value]
elif name == 'CHAST' and value in DICT_CHAST:
value = DICT_CHAST[value]
elif name == 'EVT' and value in DICT_EVT:
value = DICT_EVT[value]
elif name == 'DCST' and value in DICT_DCST:
value = DICT_DCST[value]
elif name == 'DCEVT' and value in DICT_DCEVT:
value = DICT_DCEVT[value]
elif name == 'W':
value = abs(value)
return value
def log_devices(device_node, modbus_address, conn_type, baud, port, log_directory, debug):
"""This function writes a line of data based on Inverter 101, 103 or Storage 124"""
try:
if conn_type == 'tcp':
port_int = int(port)
sunspec_client = sp_client.SunSpecClientDevice(sp_client.TCP, modbus_address, ipaddr=device_node, ipport=port_int, timeout=TIMEOUT_VAL)
elif conn_type == 'rtu':
sunspec_client = sp_client.SunSpecClientDevice(sp_client.RTU, modbus_address, name=device_node, baudrate=baud, timeout=TIMEOUT_VAL)
# read all models in the device
sunspec_client.read()
except:
if debug > 0:
print("Cannot find the address: ", modbus_address)
return False
for model in sunspec_client.device.models_list:
full_log_directory = log_directory
line = header = ""
write = False
# "id" may not exist
try:
# Log the data for an 101 (Single Phase) Inverter
if model.model_type.id == SINGLE_PHASE_INVERTER_101:
header, line = extract_data(model, LIST_INVERTER, DICT_INVERTER, debug)
# Added "sunspec_101", device_node and modbus_address to the directory suffix
full_log_directory = os.path.join(full_log_directory, "sunspec_101")
write = True
# Log the data for an 103 (Three Phase) Inverter
elif model.model_type.id == THREE_PHASE_INVERTER_103:
header, line = extract_data(model, LIST_INVERTER, DICT_INVERTER, debug)
# Added "sunspec_103", device_node and modbus_address to the directory suffix
full_log_directory = os.path.join(full_log_directory, "sunspec_103")
write = True
# Log the data for an 113 (Three Phase) Inverter
elif model.model_type.id == THREE_PHASE_INVERTER_113:
header, line = extract_data(model, LIST_INVERTER, DICT_INVERTER, debug)
# Added "sunspec_113", device_node and modbus_address to the directory suffix
full_log_directory = os.path.join(full_log_directory, "sunspec_113")
write = True
# Log the data for a device 124 (Storage)
elif model.model_type.id == STORAGE:
header, line = extract_data(model, LIST_STORAGE, DICT_STORAGE, debug)
# Added "sunspec_124", device_node and modbus_address to the directory suffix
full_log_directory = os.path.join(full_log_directory, "sunspec_124")
write = True
# Log the data for a device 160 (Strings)
elif model.model_type.id == INVERTER_STRINGS:
header, line = extract_160_data(model, LIST_STRINGS, DICT_STRINGS, debug)
# Added "sunspec_160", device_node and modbus_address to the directory suffix
full_log_directory = os.path.join(full_log_directory, "sunspec_160")
write = True
# Log the data for a device 403 (String Combiner)
elif model.model_type.id == STRING_COMBINER:
header, line = extract_403_data(model, LIST_COMBINER, DICT_COMBINER, debug)
# Added "sunspec_403", device_node and modbus_address to the directory suffix
full_log_directory = os.path.join(full_log_directory, "sunspec_403")
write = True
if write:
# replace forward slashes
devnode = device_node.replace("/", "_")
full_log_directory = os.path.join(full_log_directory, str(devnode))
full_log_directory = os.path.join(full_log_directory, str(modbus_address))
if debug > 0:
print("Data line: ", line.strip())
print("Header line: ", header.strip())
write_line(line, full_log_directory, header, debug)
except:
pass
return True
class Config(object):
"""Config object for click"""
def __init__(self):
self.verbosity = 0
CONFIG = click.make_pass_decorator(Config, ensure=True)
@click.group()
@click.option('-v', '--verbose', count=True)
@CONFIG
def cli(config, verbose):
"""Command line entry point"""
config.verbosity = verbose
@cli.command()
@click.argument('device')
@click.argument('modbus_addresses')
@click.option('--baud', default="115200")
@click.option('--port', default="502")
@CONFIG
def discover(config, device, modbus_addresses, baud, port):
"""Connect to the target IP address and run a scan of all Sunspec devices"""
# Check that no other scripts are running
# The pidfile is based on the device, since there are multiple scripts running
pidfile = PIDFILE + device.replace('/', '-') + '.pid'
if ap.check_pidfile(pidfile, config.verbosity):
print("This script is already running")
sys.exit(4)
conn_type = 'tcp'
try:
socket.inet_aton(device)
except socket.error:
conn_type = 'rtu'
start_time = time.time()
# This will check each address
for address in ap.parse_address_list(modbus_addresses):
count = 0
while count < ATTEMPTS:
retval = discover_devices(device, address, conn_type, baud, port, config.verbosity)
if retval:
break
count += 1
#time.sleep(1)
elapsed_time = time.time() - start_time
if config.verbosity > 0:
print("This request took: ", elapsed_time, " seconds.")
# Remove the PID file
if os.path.isfile(pidfile):
os.unlink(pidfile)
@cli.command()
@click.argument('device')
@click.argument('modbus_addresses')
@click.argument('output_directory')
@click.option('--baud', default="115200")
@click.option('--port', default="502")
@CONFIG
def log(config, device, modbus_addresses, output_directory, baud, port):
"""Connect to the target IP address and log the inverter output for the given bus addresses"""
# If the logging directory doesn't exist, create it
if not os.path.exists(output_directory):
os.makedirs(output_directory)
# Check that no other scripts are running
# The pidfile is based on the device, since there are multiple scripts running
pidfile = PIDFILE + device.replace('/', '-') + '.pid'
if ap.check_pidfile(pidfile, config.verbosity):
print("This script is already running")
sys.exit(4)
conn_type = 'tcp'
try:
socket.inet_aton(device)
except socket.error:
conn_type = 'rtu'
start_time = time.time()
# This will check each address
for address in ap.parse_address_list(modbus_addresses):
count = 0
while count < ATTEMPTS:
retval = log_devices(device, address, conn_type, baud, port, output_directory, config.verbosity)
if retval:
break
count += 1
time.sleep(1)
elapsed_time = time.time() - start_time
if config.verbosity > 0:
print("This request took: ", elapsed_time, " seconds.")
# Remove the PID file
if os.path.isfile(pidfile):
os.unlink(pidfile)
From the commandline exe :
>> sunspec_ardexa discover 192.168.1.140 1
where the ipaddress is your Solaredge inverter and the 1 is your inverter.
If all went well the idx you adressed is updated with the realtime value.
if nothing happens ??
exec the follwing command line and see the screen for output.
>> >> sunspec_ardexa -vv discover 192.168.1.140 1
-----------------------------------
If you like the script running from a crontab please use the complete path :
crontab -e
*/1 * * * * /usr/local/bin/sunspec_ardexa discover 192.168.1.140 1 > /dev/null 2>&1
----------------------------------
If the script does not connect to the inverter with the message : Cannot find the address: 1
Then please in the file /usr/local/lib/python2.7/dist-packages/sunspec/core/client.py
at line 333 insert the sleep command :
time.sleep(0.1)
---------------------------------
Again, first run, please let me know any issues or questions so I can update this post.
Enjoy
Frank