


2. Delta Solivia protocol description
Messages on the RS485-bus look like this:
| Size | Name | Value | Description | 
| 1 byte | STX | 0x02 | Start of message | 
| 1 byte | ENQ | Enquiry type (0x05 for requests, 0x06 for responses) | |
| 1 byte | Inverter ID | ||
| 1 byte | LEN | Number of bytes to follow, excluding CRC and ETX | |
| 2 bytes | CMD | command and subcommand e.g. 0x10 0x02 to request the current DC voltage, 0x10 0x02 to request the current DC current, etc. | |
| LEN-2 bytes | data bytes | ||
| 2 bytes | CRC | CRC-16, over preceding bytes excluding STX, LSB first (little-endian) | |
| 1 byte | ETX | 0x03 | End of message | 


| Request | Response | Value | Meaning | 
| [02][05][01][02][00][01][AD][FC][03] | [02][06][01][0C][00][01][30][30][30][30][30][30][37][38][39][34][2D][D0][03] | 0000007894 | Serial number | 
| [02][05][01][02][10][02][E0][3D][03] | [02][06][01][04][10][02][01][88][32][E7][03] | 0x0188 | DC voltage (actual) | 
| [02][05][01][02][11][02][E1][AD][03] | [02][06][01][04][11][02][01][78][33][5F][03] | 0x0178 | DC voltage (average) | 
| 
 [02][05][01][02][10][01][A0][3C][03] 
 | 
[02][06][01][04][10][01][00][09][03][17][03] | 0x0009 | DC current (actual). Unit is 0.1 A | 
| 
 [02][05][01][02][11][01][A1][AC][03] 
 | 
[02][06][01][04][11][01][00][05][02][EE][03] | 0x0005 | DC current (average). Unit is 0.1 A | 
| 
 [02][05][01][02][11][03][20][6D][03] 
 | 
[02][06][01][04][11][03][00][D7][23][73][03] | 0x00d7 | DC power | 
| 
 [02][05][01][02][21][04][75][AF][03] 
 | 
[02][06][01][04][21][04][07][D0][DE][40][03] | 0x07d0 | Isolation (actual, MOhm) | 
| 
 [02][05][01][02][11][0F][20][68][03] 
 | 
[02][06][01][04][11][0F][07][DA][20][85][03] | 0x07da | Isolation (average, MOhm) | 
| 
 [02][05][01][02][10][08][60][3A][03] 
 | 
[02][06][01][04][10][08][00][EC][12][9E][03] | 0x00ec | AC Voltage (actual) | 
| 
 [02][05][01][02][11][08][61][AA][03] 
 | 
[02][06][01][04][11][08][00][EA][93][60][03] | 0x00ea | AC Voltage (average) | 
| 
 [02][05][01][02][10][07][20][3E][03] 
 | 
[02][06][01][04][10][07][00][11][E3][1C][03] | 0x0011 | AC current (actual). Unit is 0.1 A | 
| 
 [02][05][01][02][11][07][21][AE][03] 
 | 
[02][06][01][04][11][07][00][0B][63][2B][03] | 0x000b | AC current (average). Unit is 0.1 A | 
| 
 [02][05][01][02][10][09][A1][FA][03] 
 | 
[02][06][01][04][10][09][01][72][C3][66][03] | 0x0172 | AC power (actual) | 
| 
 [02][05][01][02][11][09][A0][6A][03] 
 | 
[02][06][01][04][11][09][00][EE][C3][63][03] | 0x00ee | AC power (average) | 
| 
 [02][05][01][02][21][06][F4][6E][03] 
 | 
[02][06][01][04][21][06][00][1B][3C][27][03] | 0x001b | DC NTC Temperature | 
| 
 [02][05][01][02][20][05][B5][FF][03] 
 | 
[02][06][01][04][20][05][00][18][8D][DA][03] | 0x0018 | AC NTC Temperature | 
| [02] | STX | 
| [05] | ENQ | 
| [01] | Inverter ID | 
| [02] | Number of data bytes | 
| [21][06] | Command and subcommand (DC NTC Temperature in this case) | 
| [F4][6E] | CRC-16 | 
| [03] | ETX | 
| [02] | STX | 
| [06] | RESPONSE | 
| [01] | Inverter ID | 
| [04] | Number of data bytes | 
| [20][06] | Command and subcommand (the same of the request) | 
| [00][1B] | 16-bit value of the requested parameter | 
| [3C][27] | CRC-16 | 
| [03] | ETX | 

| Request | Response | Value | Meaning | 
| [02][05][01][02][13][03][21][0D][03] | [02][06][01][04][13][03][00][AE][E3][29][02] | 0x00AE (174) | Daily yield (Wh) | 
To implement the Delta Solivia protocol, I started from this project. I had to make some changes to use python3 (the library was written to support python2) and to use the correct commands.
Here is a brief description of the functions involved
3.1 solivia_readData
This function reads a specific value from the Delta Solivia inverter (DC power, AC power, etc). The only caveat here is that I had to add a check to remove some trailing 0xff characters that are probably mistakenly read by the USB-to-RS485 converter when inverter drives RS485 from RX to TX.
def solivia_readData(s):
    log.debug("Reading %s"%(s))
    cmd = solivia_inverter.getCmdStringFor(s)
    log.debug("Command: %s"%binascii.hexlify(cmd))
    solivia_connection.write(cmd)
    rawResponse = solivia_connection.read(100)
    cntr = 0
    while (cntr < len(rawResponse) and rawResponse[cntr] == 0xff):
       cntr += 1
    response = rawResponse[cntr:]
    floatValue = float("0")
    if response:
        log.debug("Received response %s\n"%binascii.hexlify(response))
        value = solivia_inverter.getValueFromResponse(response)
        #log.debug("Value %s"%value)
        if solivia_isFloat(value):
            floatValue = float(value)
    return floatValue 
The code leverages functions in the deltaInv.py file to extract a value as a floating point number from the inverter response
3.2 solivia_updateChannel
This function updates all the Modbus registers required to mimic one of Omega OM240 channels. The following Modbus registers are initialized
- 2 16-bits registers with the value
 - 8 16-bits registers with the timestamp string
 - 1 16-bits registers with the flags
 
Here is the code
def solivia_updateChannel(ch, value, ts):
    """ Registers are as follow
    0-1 Input A (IEE754)
    2-3 Input B
    4-5 Temperature
    6-14 Timestamp "dd/mm/yy hh:mm:ss"
    15  Flags
    """
    vHex = solivia_floatToHex(value)[2:]
    log.debug("value=%f, vHex=%s"%(value,vHex))
    stime = ts.strftime("%d/%m/%y %H:%M:%S").encode('utf-8').hex().ljust(36, '0')
    values = [int(vHex[0:4],16), int(vHex[4:8],16),
                 0, 0,
                 0, 0,
              int(stime[0:4],16), int(stime[4:8],16), int(stime[8:12],16), int(stime[12:16],16), int(stime[16:20],16), int(stime[20:24],16), int(stime[24:28],16), int(stime[28:32],16), int(stime[32:36],16),
                 3]
    return values 
3.3 solivia_reader
This functions reads all the measures and update the corresponding channels
def solivia_reader(a):
    """ A worker process that runs every so often and
    updates live values of the context. It should be noted
    that there is a race condition for the update.
    :param arguments: The input arguments to the call
    """
    log.info("Updating inverter data\n")
    dcVolts = solivia_readData('DC Volts1')
    dcCur = solivia_readData('DC Cur1')
    dcPow = solivia_readData('DC Pwr1')
    acVolts = solivia_readData('AC Volts')
    acCur = solivia_readData('AC Current')
    acPow = solivia_readData('AC Power')
    acTemp = solivia_readData('AC Temp')
    dcTemp = solivia_readData('DC Temp')
    context = a[0]
    register = 3
    slaveId = 0x00
    address = 0
    values = solivia_updateChannel(0, dcPow, datetime.now())
    context[slaveId].setValues(register, address, values)
    address = 16
    values = solivia_updateChannel(0, acPow, datetime.now())
    context[slaveId].setValues(register, address, values)
    address = 32
    values = solivia_updateChannel(0, acTemp, datetime.now())
    context[slaveId].setValues(register, address, values)
    address = 48
    values = solivia_updateChannel(0, dcTemp, datetime.now())
    context[slaveId].setValues(register, address, values)
    currDate = datetime.now().strftime("%d/%m/%y").encode('utf-8').hex().ljust(10, '0')
    currTime = datetime.now().strftime("%H:%M:%S").encode('utf-8').hex().ljust(8, '0')
    log.debug(currDate+"-"+currTime+" %.6f %.6f %.6f %.6f %.6f %.6f %.6f %.6f OK\n" %(dcVolts, dcCur, dcPow, acVolts, acCur, acPow, dcTemp, acTemp )) 
    log.info("Scheduling task")
    time = 5.0
    loop = LoopingCall(f=solivia_reader, a=(context,))
    loop.start(time, now=True)  # initially delay by time #!/bin/sh
# launcher.sh
# navigate to home directory, then to this directory, then execute python script, then back home
cd /home/pi/nmodbus
sudo python3 server.py
cd /
# Edit this file to introduce tasks to be run by cron.
#
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
#
# To define the time you can provide concrete values for
# minute (m), hour (h), day of month (dom), month (mon),
# and day of week (dow) or use '*' in these fields (for 'any').
#
# Notice that tasks will be started based on the cron's system
# daemon's notion of time and timezones.
#
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
#
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
#
# For more information see the manual pages of crontab(5) and cron(8)
#
# m h dom mon dow command
@reboot sh /home/pi/launcher.sh >/home/pi/logs/server.log 2>&1