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
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ The above code snippet prints out the current input power value, e.g. `8.342 kW`

During instantiation of a Sun2000 object the following parameters are accepted:

| Parameter | Description |
|-----------|-------------------------------------------------------------------------------------------------------------------------------------|
| host | IP address |
| port | Port, usually 502, changed to 6607 on newer firmware versions. |
| timeout | Connection timeout |
| wait | Time to wait after connection before a register read can be performed. Increases stability. |
| slave | Number of inverter unit to be read, used in cascading scenarios. Defaults to 0, but some devices need it to be set to other values. |
| Parameter | Description |
|-----------|------------------------------------------------------------------------------------------------------------------------------------------------|
| host | IP address |
| port | Port, usually 502, changed to 6607 on newer firmware versions. |
| timeout | Connection timeout |
| wait | Time to wait after connection before a register read can be performed. Increases stability. |
| slave | Number of inverter unit to be read by default, used in cascading scenarios. Defaults to 0, but some devices need it to be set to other values. |

### Read metrics

Expand All @@ -70,10 +70,14 @@ Looking at the [above example](#usage) the different methods would return the fo
Furthermore, a method `read_range` exists accepting the address of the register to start reading and either a quantity of registers or the address of the last
register to be read. The result is returned as byte-string for further processing.

Each `read*` method accepts a `slave` argument which is used in cascading scenarios to address the desired inverter unit.

### Write settings

For writing a register the `write` method can be used, taking the register address and the value as arguments.

Furthermore, the `write` method accepts a `slave` argument which is used in cascading scenarios to address the desired inverter unit.

## Registers

The following registers are provided by the Sun2000's Modbus interface and can be read accordingly. Documentation can be found
Expand Down
20 changes: 10 additions & 10 deletions sun2000_modbus/inverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ def isConnected(self):
def connected(self):
return self.isConnected()

def read_raw_value(self, register):
def read_raw_value(self, register, slave=None):
if not self.isConnected():
raise ValueError('Inverter is not connected')

try:
register_value = self.inverter.read_holding_registers(address=register.value.address, count=register.value.quantity, slave=self.slave)
register_value = self.inverter.read_holding_registers(address=register.value.address, count=register.value.quantity, slave=self.slave if slave is None else slave)
if type(register_value) == ModbusIOException:
logger.error('Inverter unit did not respond')
raise register_value
Expand All @@ -63,16 +63,16 @@ def read_raw_value(self, register):

return datatypes.decode(register_value.encode()[1:], register.value.data_type)

def read(self, register):
raw_value = self.read_raw_value(register)
def read(self, register, slave=None):
raw_value = self.read_raw_value(register, slave)

if register.value.gain is None:
return raw_value
else:
return raw_value / register.value.gain

def read_formatted(self, register, use_locale=False):
value = self.read(register)
def read_formatted(self, register, slave=None, use_locale=False):
value = self.read(register, slave)

if register.value.unit is not None:
if use_locale:
Expand All @@ -84,7 +84,7 @@ def read_formatted(self, register, use_locale=False):
else:
return value

def read_range(self, start_address, quantity=0, end_address=0):
def read_range(self, start_address, quantity=0, end_address=0, slave=None):
if quantity == 0 and end_address == 0:
raise ValueError('Either parameter quantity or end_address is required and must be greater than 0')
if quantity != 0 and end_address != 0:
Expand All @@ -98,7 +98,7 @@ def read_range(self, start_address, quantity=0, end_address=0):
if end_address != 0:
quantity = end_address - start_address + 1
try:
register_range_value = self.inverter.read_holding_registers(address=start_address, count=quantity, slave=self.slave)
register_range_value = self.inverter.read_holding_registers(address=start_address, count=quantity, slave=self.slave if slave is None else slave)
if type(register_range_value) == ModbusIOException:
logger.error('Inverter unit did not respond')
raise register_range_value
Expand All @@ -108,7 +108,7 @@ def read_range(self, start_address, quantity=0, end_address=0):

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

def write(self, register, value):
def write(self, register, value, slave=None):
if not self.isConnected():
raise ValueError('Inverter is not connected')
if not register.value.access_type in [AccessType.RW, AccessType.WO]:
Expand All @@ -118,7 +118,7 @@ def write(self, register, value):
chunks = [int.from_bytes(encoded_value[i:i+2], byteorder='big', signed=False) for i in range(0, len(encoded_value), 2)]

try:
response = self.inverter.write_registers(address=register.value.address, values=chunks, slave=self.slave)
response = self.inverter.write_registers(address=register.value.address, values=chunks, slave=self.slave if slave is None else slave)
if type(response) == ModbusIOException:
logger.error('Inverter unit did not respond')
raise response
Expand Down
145 changes: 143 additions & 2 deletions tests/test_sun2000_modbus.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import unittest
from unittest.mock import patch
import sun2000mock

from pymodbus.exceptions import ModbusIOException, ConnectionException
from sun2000_modbus.inverter import Sun2000

import sun2000mock
from sun2000_modbus.datatypes import encode, decode, DataType
from sun2000_modbus.inverter import Sun2000
from sun2000_modbus.registers import InverterEquipmentRegister, MeterEquipmentRegister, BatteryEquipmentRegister


Expand Down Expand Up @@ -147,6 +148,34 @@ def test_read_raw_value_string_connection_unexpectedly_closed(self):
self.test_inverter.read_raw_value(InverterEquipmentRegister.Model)
self.assertEqual(str(cm.exception), 'Modbus Error: [Connection] Connection unexpectedly closed')

@patch(
'pymodbus.client.ModbusTcpClient.read_holding_registers'
)
@patch(
'pymodbus.client.ModbusTcpClient.connect', sun2000mock.connect_success
)
@patch(
'pymodbus.client.ModbusTcpClient.is_socket_open', sun2000mock.connect_success
)
def test_read_raw_value_without_slave_argument_takes_default(self, mock_read_holding_registers):
self.test_inverter.connect()
self.test_inverter.read_raw_value(InverterEquipmentRegister.Model)
mock_read_holding_registers.assert_called_once_with(address=30000, count=15, slave=1)

@patch(
'pymodbus.client.ModbusTcpClient.read_holding_registers'
)
@patch(
'pymodbus.client.ModbusTcpClient.connect', sun2000mock.connect_success
)
@patch(
'pymodbus.client.ModbusTcpClient.is_socket_open', sun2000mock.connect_success
)
def test_read_raw_value_with_slave_argument(self, mock_read_holding_registers):
self.test_inverter.connect()
self.test_inverter.read_raw_value(InverterEquipmentRegister.Model, slave=123)
mock_read_holding_registers.assert_called_once_with(address=30000, count=15, slave=123)

@patch(
'pymodbus.client.ModbusTcpClient.read_holding_registers', sun2000mock.mock_read_holding_registers
)
Expand Down Expand Up @@ -189,6 +218,34 @@ def test_read_raw_value_uint32be(self):
result = self.test_inverter.read_raw_value(InverterEquipmentRegister.RatedPower)
self.assertEqual(result, 10000)

@patch(
'pymodbus.client.ModbusTcpClient.read_holding_registers'
)
@patch(
'pymodbus.client.ModbusTcpClient.connect', sun2000mock.connect_success
)
@patch(
'pymodbus.client.ModbusTcpClient.is_socket_open', sun2000mock.connect_success
)
def test_read_without_slave_argument_takes_default(self, mock_read_holding_registers):
self.test_inverter.connect()
self.test_inverter.read(InverterEquipmentRegister.RatedPower)
mock_read_holding_registers.assert_called_once_with(address=30073, count=2, slave=1)

@patch(
'pymodbus.client.ModbusTcpClient.read_holding_registers'
)
@patch(
'pymodbus.client.ModbusTcpClient.connect', sun2000mock.connect_success
)
@patch(
'pymodbus.client.ModbusTcpClient.is_socket_open', sun2000mock.connect_success
)
def test_read_with_slave_argument(self, mock_read_holding_registers):
self.test_inverter.connect()
self.test_inverter.read(InverterEquipmentRegister.RatedPower, slave=123)
mock_read_holding_registers.assert_called_once_with(address=30073, count=2, slave=123)

@patch(
'pymodbus.client.ModbusTcpClient.read_holding_registers', sun2000mock.mock_read_holding_registers
)
Expand All @@ -203,6 +260,34 @@ def test_read_uint32be(self):
result = self.test_inverter.read(InverterEquipmentRegister.RatedPower)
self.assertEqual(result, 10000.0)

@patch(
'pymodbus.client.ModbusTcpClient.read_holding_registers'
)
@patch(
'pymodbus.client.ModbusTcpClient.connect', sun2000mock.connect_success
)
@patch(
'pymodbus.client.ModbusTcpClient.is_socket_open', sun2000mock.connect_success
)
def test_read_formatted_without_slave_argument_takes_default(self, mock_read_holding_registers):
self.test_inverter.connect()
self.test_inverter.read_formatted(InverterEquipmentRegister.RatedPower)
mock_read_holding_registers.assert_called_once_with(address=30073, count=2, slave=1)

@patch(
'pymodbus.client.ModbusTcpClient.read_holding_registers'
)
@patch(
'pymodbus.client.ModbusTcpClient.connect', sun2000mock.connect_success
)
@patch(
'pymodbus.client.ModbusTcpClient.is_socket_open', sun2000mock.connect_success
)
def test_read_formatted_with_slave_argument(self, mock_read_holding_registers):
self.test_inverter.connect()
self.test_inverter.read_formatted(InverterEquipmentRegister.RatedPower, slave=123)
mock_read_holding_registers.assert_called_once_with(address=30073, count=2, slave=123)

@patch(
'pymodbus.client.ModbusTcpClient.read_holding_registers', sun2000mock.mock_read_holding_registers
)
Expand Down Expand Up @@ -302,6 +387,34 @@ def test_read_returns_float(self):
self.assertEqual(result, 1000.0)
self.assertTrue(isinstance(result, float))

@patch(
'pymodbus.client.ModbusTcpClient.read_holding_registers'
)
@patch(
'pymodbus.client.ModbusTcpClient.connect', sun2000mock.connect_success
)
@patch(
'pymodbus.client.ModbusTcpClient.is_socket_open', sun2000mock.connect_success
)
def test_read_range_without_slave_argument_takes_default(self, mock_read_holding_registers):
self.test_inverter.connect()
self.test_inverter.read_range(30000, quantity=35)
mock_read_holding_registers.assert_called_once_with(address=30000, count=35, slave=1)

@patch(
'pymodbus.client.ModbusTcpClient.read_holding_registers'
)
@patch(
'pymodbus.client.ModbusTcpClient.connect', sun2000mock.connect_success
)
@patch(
'pymodbus.client.ModbusTcpClient.is_socket_open', sun2000mock.connect_success
)
def test_read_range_with_slave_argument(self, mock_read_holding_registers):
self.test_inverter.connect()
self.test_inverter.read_range(30000, quantity=35, slave=123)
mock_read_holding_registers.assert_called_once_with(address=30000, count=35, slave=123)

@patch(
'pymodbus.client.ModbusTcpClient.read_holding_registers', sun2000mock.mock_read_holding_registers
)
Expand Down Expand Up @@ -449,6 +562,34 @@ def test_write_uint16be_connection_unexpectedly_closed(self):
self.test_inverter.write(BatteryEquipmentRegister.BackupPowerSOC, 10)
self.assertEqual(str(cm.exception), 'Modbus Error: [Connection] Connection unexpectedly closed')

@patch(
'pymodbus.client.ModbusTcpClient.write_registers'
)
@patch(
'pymodbus.client.ModbusTcpClient.connect', sun2000mock.connect_success
)
@patch(
'pymodbus.client.ModbusTcpClient.is_socket_open', sun2000mock.connect_success
)
def test_write_without_slave_argument_takes_default(self, write_registers_mock):
self.test_inverter.connect()
self.test_inverter.write(BatteryEquipmentRegister.BackupPowerSOC, 10)
write_registers_mock.assert_called_once_with(address=47102, values=[10], slave=1)

@patch(
'pymodbus.client.ModbusTcpClient.write_registers'
)
@patch(
'pymodbus.client.ModbusTcpClient.connect', sun2000mock.connect_success
)
@patch(
'pymodbus.client.ModbusTcpClient.is_socket_open', sun2000mock.connect_success
)
def test_write_with_slave_argument(self, write_registers_mock):
self.test_inverter.connect()
self.test_inverter.write(BatteryEquipmentRegister.BackupPowerSOC, 10, slave=123)
write_registers_mock.assert_called_once_with(address=47102, values=[10], slave=123)

@patch(
'pymodbus.client.ModbusTcpClient.write_registers'
)
Expand Down