Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
599 changes: 304 additions & 295 deletions README.md

Large diffs are not rendered by default.

49 changes: 37 additions & 12 deletions sun2000_modbus/datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,57 @@


class DataType(Enum):
STRING = "string"
UINT16_BE = "uint16"
UINT32_BE = "uint32"
INT16_BE = "int16"
INT32_BE = "int32"
BITFIELD16 = "bitfield16"
BITFIELD32 = "bitfield32"
MULTIDATA = "multidata"
STRING = 'string'
UINT16_BE = 'uint16'
UINT32_BE = 'uint32'
INT16_BE = 'int16'
INT32_BE = 'int32'
BITFIELD16 = 'bitfield16'
BITFIELD32 = 'bitfield32'
MULTIDATA = 'multidata'


def decode_string(value):
return value.decode("utf-8", "replace").strip("\0")
return value.decode('utf-8', 'replace').strip('\0')


def encode_uint_be(value, length):
return int.to_bytes(value, length=length, byteorder='big', signed=False)


def decode_uint_be(value):
return int.from_bytes(value, byteorder="big", signed=False)
return int.from_bytes(value, byteorder='big', signed=False)


def encode_int_be(value, length):
return int.to_bytes(value, length=length, byteorder='big', signed=True)


def decode_int_be(value):
return int.from_bytes(value, byteorder="big", signed=True)
return int.from_bytes(value, byteorder='big', signed=True)


def decode_bitfield(value):
return ''.join(format(byte, '08b') for byte in value)


def encode(value, data_type):
if data_type == DataType.UINT16_BE:
return encode_uint_be(value, 2)
elif data_type == DataType.UINT32_BE:
return encode_uint_be(value, 4)
elif data_type == DataType.INT16_BE:
return encode_int_be(value, 2)
elif data_type == DataType.INT32_BE:
return encode_int_be(value, 4)
elif data_type == DataType.MULTIDATA:
if len(value) % 2 != 0:
raise ValueError('Multidata value length must be a multiple of 2')
return value
else:
raise ValueError('Writing is not supported for register type')


def decode(value, data_type):
if data_type == DataType.STRING:
return decode_string(value)
Expand All @@ -40,4 +65,4 @@ def decode(value, data_type):
elif data_type == DataType.MULTIDATA:
return value
else:
raise ValueError("Unknown register type")
raise ValueError('Unknown register type')
37 changes: 28 additions & 9 deletions sun2000_modbus/inverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pymodbus.exceptions import ModbusIOException, ConnectionException

from . import datatypes
from .registers import AccessType

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
Expand Down Expand Up @@ -35,8 +36,8 @@ def connect(self):
def disconnect(self):
"""Close the underlying tcp socket"""
# Some Sun2000 models with the SDongle WLAN-FE require the TCP connection to be closed
# as soon as possible. Leaving the TCP connection open for an extended time may cause
# dongle reboots and/or FusionSolar portal updates to be delayed or even paused.
# as soon as possible. Leaving the TCP connection open for an extended time may cause
# dongle reboots and/or FusionSolar portal updates to be delayed or even paused.
self.inverter.close()

def isConnected(self):
Expand All @@ -54,10 +55,10 @@ def read_raw_value(self, register):
try:
register_value = self.inverter.read_holding_registers(register.value.address, register.value.quantity, slave=self.slave)
if type(register_value) == ModbusIOException:
logger.error("Inverter unit did not respond")
logger.error('Inverter unit did not respond')
raise register_value
except ConnectionException:
logger.error("A connection error occurred")
logger.error('A connection error occurred')
raise

return datatypes.decode(register_value.encode()[1:], register.value.data_type)
Expand Down Expand Up @@ -85,11 +86,11 @@ def read_formatted(self, register, use_locale=False):

def read_range(self, start_address, quantity=0, end_address=0):
if quantity == 0 and end_address == 0:
raise ValueError("Either parameter quantity or end_address is required and must be greater than 0")
raise ValueError('Either parameter quantity or end_address is required and must be greater than 0')
if quantity != 0 and end_address != 0:
raise ValueError("Only one parameter quantity or end_address should be defined")
raise ValueError('Only one parameter quantity or end_address should be defined')
if end_address != 0 and end_address <= start_address:
raise ValueError("end_address must be greater than start_address")
raise ValueError('end_address must be greater than start_address')

if not self.isConnected():
raise ValueError('Inverter is not connected')
Expand All @@ -99,10 +100,28 @@ def read_range(self, start_address, quantity=0, end_address=0):
try:
register_range_value = self.inverter.read_holding_registers(start_address, quantity, slave=self.slave)
if type(register_range_value) == ModbusIOException:
logger.error("Inverter unit did not respond")
logger.error('Inverter unit did not respond')
raise register_range_value
except ConnectionException:
logger.error("A connection error occurred")
logger.error('A connection error occurred')
raise

return datatypes.decode(register_range_value.encode()[1:], datatypes.DataType.MULTIDATA)

def write(self, register, value):
if not self.isConnected():
raise ValueError('Inverter is not connected')
if not register.value.access_type in [AccessType.RW, AccessType.WO]:
raise ValueError('Register is not writeable')

encoded_value = datatypes.encode(value, register.value.data_type)
chunks = [encoded_value[i:i+2] for i in range(0, len(encoded_value), 2)]

try:
response = self.inverter.write_registers(register.value.address, chunks, slave=self.slave, skip_encode=True)
if type(response) == ModbusIOException:
logger.error('Inverter unit did not respond')
raise response
except ConnectionException:
logger.error('A connection error occurred')
raise
Loading
Loading