feat(modbus): add DDSU666 profile and select read function code per profile
- Add CHINT DDSU666 profile (ddsu666.yaml): FC03 holding registers, voltage/ current/active power (kW)/reactive power (kvar)/PF/frequency, import+export active energy. Word/byte order left at big-endian as a documented best guess (manual has no float example) — to be confirmed on-device. - Add DDSU666-Modbus-Protocol.md reference extracted from the official manual, plus the source PDF (parity with the SDM120 reference). - Generalize driver.read_blocks to dispatch FC03 (holding) or FC04 (input) based on a function_code argument (default 4, SDM120 behaviour unchanged); the code is validated before any connection is attempted. - Wire profile.function_code through the CLI read command, the background poller, and the device /test endpoint — previously the profile field was declared but never honoured (read path was hardcoded to FC04). - Tests: default -> FC04, function_code=3 -> FC03 holding, invalid FC rejected before connecting.
This commit is contained in:
@@ -217,6 +217,57 @@ class TestReadBlocks:
|
||||
|
||||
mock_client.close.assert_called_once()
|
||||
|
||||
@patch("app.integrations.modbus.driver.ModbusTcpClient")
|
||||
def test_default_function_code_uses_fc04_input_registers(
|
||||
self, mock_client_cls: MagicMock
|
||||
) -> None:
|
||||
"""With no function_code given, read_blocks uses FC04 (read_input_registers)."""
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
mock_client.connect.return_value = True
|
||||
mock_client.read_input_registers.return_value = _make_ok_response([0x4366, 0x3334])
|
||||
|
||||
read_blocks("127.0.0.1", 502, 1, [{"start": 0x0000, "count": 2}])
|
||||
|
||||
mock_client.read_input_registers.assert_called_once_with(0x0000, count=2, device_id=1)
|
||||
mock_client.read_holding_registers.assert_not_called()
|
||||
|
||||
@patch("app.integrations.modbus.driver.ModbusTcpClient")
|
||||
def test_function_code_3_uses_fc03_holding_registers(
|
||||
self, mock_client_cls: MagicMock
|
||||
) -> None:
|
||||
"""function_code=3 dispatches FC03 (read_holding_registers), e.g. DDSU666."""
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
mock_client.connect.return_value = True
|
||||
mock_client.read_holding_registers.return_value = _make_ok_response(
|
||||
[0x4366, 0x3334, 0x3F80, 0x0000]
|
||||
)
|
||||
|
||||
blocks = [{"start": 0x2000, "count": 4}]
|
||||
result = read_blocks("127.0.0.1", 502, 1, blocks, function_code=3)
|
||||
|
||||
assert result == {0x2000: 0x4366, 0x2001: 0x3334, 0x2002: 0x3F80, 0x2003: 0x0000}
|
||||
mock_client.read_holding_registers.assert_called_once_with(
|
||||
0x2000, count=4, device_id=1
|
||||
)
|
||||
mock_client.read_input_registers.assert_not_called()
|
||||
|
||||
@patch("app.integrations.modbus.driver.ModbusTcpClient")
|
||||
def test_invalid_function_code_raises_before_connecting(
|
||||
self, mock_client_cls: MagicMock
|
||||
) -> None:
|
||||
"""An unsupported function code is rejected without opening a connection."""
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
|
||||
with pytest.raises(ModbusDriverError, match="Unsupported read function code"):
|
||||
read_blocks("127.0.0.1", 502, 1, [{"start": 0, "count": 2}], function_code=16)
|
||||
|
||||
# No client should have been constructed or connected for a bad FC.
|
||||
mock_client_cls.assert_not_called()
|
||||
mock_client.connect.assert_not_called()
|
||||
|
||||
@patch("app.integrations.modbus.driver.ModbusTcpClient")
|
||||
def test_unit_id_passed_as_device_id(self, mock_client_cls: MagicMock) -> None:
|
||||
"""unit_id is forwarded as device_id= keyword argument (pymodbus 3.13.x)."""
|
||||
|
||||
Reference in New Issue
Block a user