Python SolarEdge modbus script via lan

Python and python framework

Moderator: leecollings

freijn
Posts: 536
Joined: Friday 23 December 2016 16:40
Target OS: Raspberry Pi / ODroid
Domoticz version: Stable
Location: Netherlands Purmerend
Contact:

Python SolarEdge modbus script via lan

Post by freijn »

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

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
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.

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&param=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&param=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)

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 edited by freijn on Tuesday 11 August 2020 12:56, edited 9 times in total.
Piacco
Posts: 69
Joined: Friday 14 November 2014 9:33
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: Python SolarEdge modbus script via lan

Post by Piacco »

Hi Frank,

Thanks for the script, it works like a charm :D

Is there any diffrence in which data you get between the sunspec-ardexa script and the sunspec-monitor script?

Greetings,
Piacco
freijn
Posts: 536
Joined: Friday 23 December 2016 16:40
Target OS: Raspberry Pi / ODroid
Domoticz version: Stable
Location: Netherlands Purmerend
Contact:

Re: Python SolarEdge modbus script via lan

Post by freijn »

Hi Piacco

Thanks for sharing !

Any updates or comments for the "install guide" ?

I believe you now have more sensors as I had not seen Amp's on DC and Amp's per Phase listed.
and....
You are now running Python instead of Perl. excecution time is much faster on Python.

But very black and white the answer is NO Difference as the info comes from the same Inverter.
User avatar
jvdz
Posts: 2189
Joined: Tuesday 30 December 2014 19:25
Target OS: Raspberry Pi / ODroid
Domoticz version: 4.107
Location: Netherlands
Contact:

Re: Python SolarEdge modbus script via lan

Post by jvdz »

Thanks for sharing your script!
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.
These are the modifications I made in case somebody is interested:
At the top define a global variable for W to save the value for later use. Add the second line under the "ATTEMPTS = 1" line:

Code: Select all

ATTEMPTS = 1
SAVE_W = 0
Update for the domo function:

Code: Select all

#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&param=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&param=udevice&idx='+str(domoidx)+'&svalue='+str(domovalue)
               r=requests.get(urlmessage)
    else:
        return True
    return True
Solar-sunspec.png
Solar-sunspec.png (22.99 KiB) Viewed 6427 times
Jos
New Garbage collection scripts: https://github.com/jvanderzande/GarbageCalendar
rogerthn
Posts: 25
Joined: Thursday 26 July 2018 12:07
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: Python SolarEdge modbus script via lan

Post by rogerthn »

Thanks for sharing!

I'm running python 3.7 and after

Code: Select all

sudo cp -p /usr/lib/python3.7/configparser.py /usr/lib/python3.7/ConfigParser.py
I could try

Code: Select all

sunspec_ardexa --verbose discover a.b.c.d 1 --port 1502
but the result is

Code: Select all

Cannot find the address:  1
This request took:  0.18040108680725098  seconds.
The result from

Code: Select all

/home/pi/sunspec-monitor/sunspec-status --port=1502 --verbose --meter=0 --debug a.b.c.d
is

Code: Select all

INVERTER:
             Model: SolarEdge  SE7K-RW0TEBNN4
  Firmware version: 4.0008.0019
     Serial Number: 7E164A49

          'C_DeviceAddress' => 1,
          'C_Manufacturer' => 'SolarEdge ',
          'C_Model' => 'SE7K-RW0TEBNN4',
          'C_SerialNumber' => '7E164A49',
          'C_SunSpec_DID' => 1,
          'C_SunSpec_ID' => 'SunS',
          'C_SunSpec_Length' => 65,
          'C_Version' => '0004.0008.0019'

            Status: SLEEPING

 Power Output (AC):            0 W
  Power Input (DC):            0 W
        Efficiency:         0.00 %
  Total Production:      798.244 kWh
      Voltage (AC):       395.70 V (50.02 Hz)
      Current (AC):         0.00 A
      Voltage (DC):         0.00 V
      Current (DC):         0.00 A
       Temperature:         0.00 C (heatsink)

          'C_SunSpec_DID' => 103,
          'C_SunSpec_Length' => 50,
          'I_AC_Current' => 0,
          'I_AC_CurrentA' => 0,
          'I_AC_CurrentB' => 0,
          'I_AC_CurrentC' => 0,
          'I_AC_Current_SF' => -2,
          'I_AC_Energy_WH' => 798244,
          'I_AC_Energy_WH_SF' => 0,
          'I_AC_Frequency' => 5002,
          'I_AC_Frequency_SF' => -2,
          'I_AC_PF' => 0,
          'I_AC_PF_SF' => 0,
          'I_AC_Power' => 0,
          'I_AC_Power_SF' => 0,
          'I_AC_VA' => 0,
          'I_AC_VAR' => 0,
          'I_AC_VAR_SF' => 0,
          'I_AC_VA_SF' => 0,
          'I_AC_VoltageAB' => 3957,
          'I_AC_VoltageAN' => 2287,
          'I_AC_VoltageBC' => 3968,
          'I_AC_VoltageBN' => 2294,
          'I_AC_VoltageCA' => 3957,
          'I_AC_VoltageCN' => 2279,
          'I_AC_Voltage_SF' => -1,
          'I_DC_Current' => 0,
          'I_DC_Current_SF' => 0,
          'I_DC_Power' => 0,
          'I_DC_Power_SF' => 0,
          'I_DC_Voltage' => 0,
          'I_DC_Voltage_SF' => -1,
          'I_Event_1' => 4294967295,
          'I_Event_1_Vendor' => 0,
          'I_Event_2' => 4294967295,
          'I_Event_2_Vendor' => 4294967295,
          'I_Event_3_Vendor' => 4294967295,
          'I_Event_4_Vendor' => 0,
          'I_Status' => 2,
          'I_Status_Vendor' => 0,
          'I_Temp_Sink' => 0,
          'I_Temp_Sink_SF' => -2
Maybe I need stick to Perl :(
User avatar
jvdz
Posts: 2189
Joined: Tuesday 30 December 2014 19:25
Target OS: Raspberry Pi / ODroid
Domoticz version: 4.107
Location: Netherlands
Contact:

Re: Python SolarEdge modbus script via lan

Post by jvdz »

I am using the standard 502 port, but that depends of what you've configured in your solaredge.
I also hope you did fill the IP address for a.b.c.d in this command:
sunspec_ardexa --verbose discover a.b.c.d 1

Jos
New Garbage collection scripts: https://github.com/jvanderzande/GarbageCalendar
rogerthn
Posts: 25
Joined: Thursday 26 July 2018 12:07
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: Python SolarEdge modbus script via lan

Post by rogerthn »

jvdz wrote: Tuesday 25 February 2020 19:39 I am using the standard 502 port, but that depends of what you've configured in your solaredge.
I also hope you did fill the IP address for a.b.c.d in this command:
sunspec_ardexa --verbose discover a.b.c.d 1

Jos
Yes, a.b.c.d is the same for sunspec_ardexa... and sunspec-status... :D
freijn
Posts: 536
Joined: Friday 23 December 2016 16:40
Target OS: Raspberry Pi / ODroid
Domoticz version: Stable
Location: Netherlands Purmerend
Contact:

Re: Python SolarEdge modbus script via lan

Post by freijn »

jvdz wrote: Tuesday 25 February 2020 17:46 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.
Jos
Hey Jos, thanks for the heads-up !!! Was too focussed on getting it to work and forgot to build this one.

I have updated the first post with your suggestion.

Cheers,

Frank
freijn
Posts: 536
Joined: Friday 23 December 2016 16:40
Target OS: Raspberry Pi / ODroid
Domoticz version: Stable
Location: Netherlands Purmerend
Contact:

Re: Python SolarEdge modbus script via lan

Post by freijn »

rogerthn wrote: Tuesday 25 February 2020 19:36

Code: Select all

sunspec_ardexa --verbose discover a.b.c.d 1 --port 1502
but the result is

Code: Select all

Cannot find the address:  1
This request took:  0.18040108680725098  seconds.
Maybe I need stick to Perl :(
Roger,

Is this before or after my mod ?
Did you try with the original files ?
rogerthn
Posts: 25
Joined: Thursday 26 July 2018 12:07
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: Python SolarEdge modbus script via lan

Post by rogerthn »

freijn wrote: Tuesday 25 February 2020 21:20 Is this before or after my mod ?
Did you try with the original files ?
Above is after your mod.
Original sunspec_ardexa.py gives

Code: Select all

Cannot find the address:  1
Cannot find the address:  1
Cannot find the address:  1
Cannot find the address:  1
Cannot find the address:  1
Cannot find the address:  1
Cannot find the address:  1
Cannot find the address:  1
Cannot find the address:  1
Cannot find the address:  1
This request took:  12.260001182556152  seconds.
freijn
Posts: 536
Joined: Friday 23 December 2016 16:40
Target OS: Raspberry Pi / ODroid
Domoticz version: Stable
Location: Netherlands Purmerend
Contact:

Re: Python SolarEdge modbus script via lan

Post by freijn »

First we need to get it to work with the original files.
have you tried :

sunspec_ardexa discover 192.168.1.3 1-20 --port 1502

Change it to your ip.

THis might take some time to discover....
rogerthn
Posts: 25
Joined: Thursday 26 July 2018 12:07
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: Python SolarEdge modbus script via lan

Post by rogerthn »

Code: Select all

sunspec_ardexa discover 10.3.141.85 1-20 --port 1502
With the original sunspec_ardexa.py did take some time and nothing was returned

Code: Select all

ping 10.3.141.85
PING 10.3.141.85 (10.3.141.85) 56(84) bytes of data.
64 bytes from 10.3.141.85: icmp_seq=1 ttl=64 time=3.85 ms
64 bytes from 10.3.141.85: icmp_seq=3 ttl=64 time=5.78 ms
64 bytes from 10.3.141.85: icmp_seq=4 ttl=64 time=3.50 ms
64 bytes from 10.3.141.85: icmp_seq=5 ttl=64 time=6.03 ms
64 bytes from 10.3.141.85: icmp_seq=6 ttl=64 time=3.19 ms
^C
--- 10.3.141.85 ping statistics ---
6 packets transmitted, 5 received, 16.6667% packet loss, time 23ms
rtt min/avg/max/mdev = 3.193/4.468/6.028/1.194 ms
Piacco
Posts: 69
Joined: Friday 14 November 2014 9:33
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: Python SolarEdge modbus script via lan

Post by Piacco »

I try to fire the script from the crontab, but i get an error.

Feb 25 22:36:01 raspberrypi CRON[23476]: (CRON) info (No MTA installed, discarding output)

Crontab entry
*/1 * * * * sunspec_ardexa discover 192.168.x.x 1 --port=502 /dev/null 2>&1
freijn
Posts: 536
Joined: Friday 23 December 2016 16:40
Target OS: Raspberry Pi / ODroid
Domoticz version: Stable
Location: Netherlands Purmerend
Contact:

Re: Python SolarEdge modbus script via lan

Post by freijn »

you are missing a >

*/1 * * * * sunspec_ardexa discover 192.168.x.x 1 --port=502 >/dev/null 2>&1
telewy
Posts: 3
Joined: Tuesday 10 January 2017 13:52
Target OS: Linux
Domoticz version:
Location: Poland
Contact:

Re: Python SolarEdge modbus script via lan

Post by telewy »

freijn wrote: Tuesday 25 February 2020 21:43 First we need to get it to work with the original files.
have you tried :

sunspec_ardexa discover 192.168.1.3 1-20 --port 1502

Change it to your ip.

THis might take some time to discover....
Hi,

First of all I would like to say Hello to everybody. My name is Tomasz. I am owner of SolarEdge inverter SE7K since January and looking for integration it with Domoticz. And looks that here is best place for me 😊 I run domoticz on Debian server.

Since yesterday I have been trying to repeat steps described here. And I was stopped on command:
sunspec_ardexa --verbose discover x.y.z.v 1 --port 1502
which gave me: Cannot find the address: 1

After investigation I found that command
suns.py -i 192.168.1.191 -P 1502 -a 1 -T 60

also didn’t work properly:
Timestamp: 2020-02-27T12:41:56Z
Traceback (most recent call last):
File "/usr/local/lib/python3.8/site-packages/sunspec/core/modbus/client.py", line 676, in connect
self.socket.connect((self.ipaddr, self.ipport))
ConnectionRefusedError: [Errno 111] Connection refused
………………..
It was a little bit strange, because the same command run from Windows machine works properly. After searching internet I found this discussion https://github.com/sunspec/pysunspec/issues/52.
Following it I added at the 333 line, in file .. pysunspec\sunspec\core\client.py:
time.sleep(0.1)
After that both command (sunspec_ardexa and suns.py) work fine. All are done on original files.

Cheers,
Tomasz
Piacco
Posts: 69
Joined: Friday 14 November 2014 9:33
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: Python SolarEdge modbus script via lan

Post by Piacco »

freijn wrote: Tuesday 25 February 2020 22:46 you are missing a >

*/1 * * * * sunspec_ardexa discover 192.168.x.x 1 --port=502 >/dev/null 2>&1
I still have problems to launch the script with crontab, i tried different rules, but none of these works :(

*/1 * * * * sunspec_ardexa discover 192.168.x.x 1 --port=502 > /tmp/SolarEdge.log 2>&1
SolarEdge logfile:
/bin/sh: 1: sunspec_ardexa: not found

*/1 * * * * /usr/local/lib/python2.7/dist-packages/sunspec_ardexa discover 192.168.x.x 1 --port=502 > /tmp/SolarEdge.log 2>&1
SolarEdge logfile:
/bin/sh: 1: /usr/local/lib/python2.7/dist-packages/sunspec_ardexa: not found

*/1 * * * * /usr/local/lib/python2.7/dist-packages/sunspec_ardexa.py discover 192.168.x.x 1 --port=502 > /tmp/SolarEdge.log 2>&1
SolarEdge logfile:
/bin/sh: 1: /usr/local/lib/python2.7/dist-packages/sunspec_ardexa.py: Permission denied

*/1 * * * * sudo /usr/local/lib/python2.7/dist-packages/sunspec_ardexa.py discover 192.168.x.x 1 --port=502 > /tmp/SolarEdge.log 2>&1
SolarEdge logfile:
sudo: /usr/local/lib/python2.7/dist-packages/sunspec_ardexa.py: command not found
User avatar
jvdz
Posts: 2189
Joined: Tuesday 30 December 2014 19:25
Target OS: Raspberry Pi / ODroid
Domoticz version: 4.107
Location: Netherlands
Contact:

Re: Python SolarEdge modbus script via lan

Post by jvdz »

I had the same and put the command in a sh file and added that file to crontab to get it to work.

Jos
New Garbage collection scripts: https://github.com/jvanderzande/GarbageCalendar
rogerthn
Posts: 25
Joined: Thursday 26 July 2018 12:07
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: Python SolarEdge modbus script via lan

Post by rogerthn »

Piacco wrote: Thursday 27 February 2020 18:23
I still have problems to launch the script with crontab, i tried different rules, but none of these works :(
...
Try

Code: Select all

which sunspec_ardexa
and use the result in crontab
Piacco
Posts: 69
Joined: Friday 14 November 2014 9:33
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: Python SolarEdge modbus script via lan

Post by Piacco »

jvdz wrote: Thursday 27 February 2020 18:32 I had the same and put the command in a sh file and added that file to crontab to get it to work.

Jos
I tried to put the command in a sh file, but i get the same error.

Code: Select all

#!/bin/sh
sunspec_ardexa discover 192.168.x.x 1 --port=502
/home/pi/domoticz/scripts/SolarEdge.sh: 3: /home/pi/domoticz/scripts/SolarEdge.sh: sunspec_ardexa: not found
rogerthn
Posts: 25
Joined: Thursday 26 July 2018 12:07
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: Python SolarEdge modbus script via lan

Post by rogerthn »

My SE7K has been running since October last year and today I did get a new all time high with 36.390 kWh :D
telewy wrote: Thursday 27 February 2020 14:58 Hi,
....
Following it I added at the 333 line, in file .. pysunspec\sunspec\core\client.py:
time.sleep(0.1)
....
Cheers,
Tomasz
THANKS Tomasz!
My update as below

Code: Select all

diff /usr/local/lib/python3.7/dist-packages/sunspec/core/client.py.ORG /usr/local/lib/python3.7/dist-packages/sunspec/core/client.py
332a333
>                 time.sleep(0.1)
Post Reply

Who is online

Users browsing this forum: No registered users and 1 guest