diff --git a/README.md b/README.md index d80556d..d14d971 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/sun2000_modbus/inverter.py b/sun2000_modbus/inverter.py index 736e1cb..0f02323 100644 --- a/sun2000_modbus/inverter.py +++ b/sun2000_modbus/inverter.py @@ -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 @@ -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: @@ -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: @@ -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 @@ -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]: @@ -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 diff --git a/tests/test_sun2000_modbus.py b/tests/test_sun2000_modbus.py index 5735885..58af96b 100644 --- a/tests/test_sun2000_modbus.py +++ b/tests/test_sun2000_modbus.py @@ -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 @@ -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 ) @@ -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 ) @@ -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 ) @@ -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 ) @@ -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' )