From a4b8c7fa6a45f037135b9061aa3500e5c3a7befb Mon Sep 17 00:00:00 2001 From: mle Date: Sun, 16 Jun 2024 23:21:47 +0200 Subject: [PATCH] Add potential meter values to DT inverter --- goodwe/dt.py | 29 +++++++++++++++++++++++++---- tests/test_dt.py | 47 ++++++++++++++++++++++++++++++----------------- 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/goodwe/dt.py b/goodwe/dt.py index a957212..4cd4bfb 100644 --- a/goodwe/dt.py +++ b/goodwe/dt.py @@ -3,7 +3,7 @@ import logging from typing import Tuple -from .exceptions import InverterError, RequestRejectedException +from .exceptions import InverterError, RequestFailedException, RequestRejectedException from .inverter import Inverter from .inverter import OperationMode from .inverter import SensorKind as Kind @@ -113,6 +113,12 @@ class DT(Inverter): # 30172 reserved ) + # Inverter's meter data + # Modbus registers from offset 0x75f4 (30196) + __all_sensors_meter: Tuple[Sensor, ...] = ( + PowerS("active_power", 30196, "Active Power", Kind.GRID), + ) + # Modbus registers of inverter settings, offsets are modbus register addresses __all_settings: Tuple[Sensor, ...] = ( Timestamp("time", 40313, "Inverter time"), @@ -139,9 +145,12 @@ class DT(Inverter): def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3): super().__init__(host, port, comm_addr if comm_addr else 0x7f, timeout, retries) self._READ_DEVICE_VERSION_INFO: ProtocolCommand = self._read_command(0x7531, 0x0028) - self._READ_DEVICE_RUNNING_DATA: ProtocolCommand = self._read_command(0x7594, 0x0049) + self._READ_RUNNING_DATA: ProtocolCommand = self._read_command(0x7594, 0x0049) + self._READ_METER_DATA: ProtocolCommand = self._read_command(0x75f4, 0x01) self._sensors = self.__all_sensors + self._sensors_meter = self.__all_sensors_meter self._settings: dict[str, Sensor] = {s.id_: s for s in self.__all_settings} + self._has_meter: bool = True @staticmethod def _single_phase_only(s: Sensor) -> bool: @@ -185,8 +194,17 @@ async def read_device_info(self): pass async def read_runtime_data(self) -> Dict[str, Any]: - response = await self._read_from_socket(self._READ_DEVICE_RUNNING_DATA) + response = await self._read_from_socket(self._READ_RUNNING_DATA) data = self._map_response(response, self._sensors) + + if self._has_meter: + try: + response = await self._read_from_socket(self._READ_METER_DATA) + data.update(self._map_response(response, self._sensors_meter)) + except (RequestRejectedException, RequestFailedException): + logger.info("Meter values not supported, disabling further attempts.") + self._has_meter = False + return data async def read_setting(self, setting_id: str) -> Any: @@ -266,7 +284,10 @@ async def set_ongrid_battery_dod(self, dod: int) -> None: raise InverterError("Operation not supported, inverter has no batteries.") def sensors(self) -> Tuple[Sensor, ...]: - return self._sensors + result = self._sensors + if self._has_meter: + result = result + self._sensors_meter + return result def settings(self) -> Tuple[Sensor, ...]: return tuple(self._settings.values()) diff --git a/tests/test_dt.py b/tests/test_dt.py index da9521d..5831198 100644 --- a/tests/test_dt.py +++ b/tests/test_dt.py @@ -4,7 +4,8 @@ from unittest import TestCase from goodwe.dt import DT -from goodwe.exceptions import RequestFailedException +from goodwe.exceptions import RequestFailedException, RequestRejectedException +from goodwe.modbus import ILLEGAL_DATA_ADDRESS from goodwe.protocol import ProtocolCommand, ProtocolResponse @@ -13,7 +14,7 @@ class DtMock(TestCase, DT): def __init__(self, methodName='runTest', port=8899): TestCase.__init__(self, methodName) DT.__init__(self, "localhost", port) - self.sensor_map = {s.id_: s.unit for s in self.sensors()} + self.sensor_map = {s.id_: s for s in self.sensors()} self._mock_responses = {} def mock_response(self, command: ProtocolCommand, filename: str): @@ -24,6 +25,10 @@ async def _read_from_socket(self, command: ProtocolCommand) -> ProtocolResponse: root_dir = os.path.dirname(os.path.abspath(__file__)) filename = self._mock_responses.get(command) if filename is not None: + if ILLEGAL_DATA_ADDRESS == filename: + raise RequestRejectedException(ILLEGAL_DATA_ADDRESS) + if 'NO RESPONSE' == filename: + raise RequestFailedException() with open(root_dir + '/sample/dt/' + filename, 'r') as f: response = bytes.fromhex(f.read()) if not command.validator(response): @@ -33,10 +38,11 @@ async def _read_from_socket(self, command: ProtocolCommand) -> ProtocolResponse: self.request = command.request return ProtocolResponse(bytes.fromhex("aa557f00010203040506070809"), command) - def assertSensor(self, sensor, expected_value, expected_unit, data): - self.assertEqual(expected_value, data.get(sensor)) - self.assertEqual(expected_unit, self.sensor_map.get(sensor)) - self.sensor_map.pop(sensor) + def assertSensor(self, sensor_name, expected_value, expected_unit, data): + self.assertEqual(expected_value, data.get(sensor_name)) + sensor = self.sensor_map.get(sensor_name); + self.assertEqual(expected_unit, sensor.unit) + self.sensor_map.pop(sensor_name) @classmethod def setUpClass(cls): @@ -47,13 +53,16 @@ class GW6000_DT_Test(DtMock): def __init__(self, methodName='runTest'): DtMock.__init__(self, methodName) - self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW6000-DT_running_data.hex') + self.mock_response(self._READ_RUNNING_DATA, 'GW6000-DT_running_data.hex') + self.mock_response(self._READ_METER_DATA, ILLEGAL_DATA_ADDRESS) def test_GW6000_DT_runtime_data(self): self.loop.run_until_complete(self.read_device_info()) data = self.loop.run_until_complete(self.read_runtime_data()) self.assertEqual(42, len(data)) + self.sensor_map = {s.id_: s for s in self.sensors()} + self.assertSensor('timestamp', datetime.strptime('2021-08-31 12:03:02', '%Y-%m-%d %H:%M:%S'), '', data) self.assertSensor('vpv1', 320.8, 'V', data) self.assertSensor('ipv1', 3.1, 'A', data) @@ -61,9 +70,6 @@ def test_GW6000_DT_runtime_data(self): self.assertSensor('vpv2', 324.1, 'V', data) self.assertSensor('ipv2', 3.2, 'A', data) self.assertSensor('ppv2', 1037, 'W', data) - self.assertSensor('vpv3', None, 'V', data) - self.assertSensor('ipv3', None, 'A', data) - self.assertSensor('ppv3', None, 'W', data) self.assertSensor('ppv', 2031, 'W', data) self.assertSensor('vline1', 0, 'V', data) self.assertSensor('vline2', 0, 'V', data) @@ -122,8 +128,9 @@ class GW8K_DT_Test(DtMock): def __init__(self, methodName='runTest'): DtMock.__init__(self, methodName) - self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW8K-DT_running_data.hex') self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW8K-DT_device_info.hex') + self.mock_response(self._READ_RUNNING_DATA, 'GW8K-DT_running_data.hex') + self.mock_response(self._READ_METER_DATA, ILLEGAL_DATA_ADDRESS) def test_GW8K_DT_device_info(self): self.loop.run_until_complete(self.read_device_info()) @@ -199,8 +206,9 @@ class GW5000D_NS_Test(DtMock): def __init__(self, methodName='runTest'): DtMock.__init__(self, methodName) - self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW5000D-NS_running_data.hex') self.mock_response(self._READ_DEVICE_VERSION_INFO, 'Mock_device_info.hex') + self.mock_response(self._READ_RUNNING_DATA, 'GW5000D-NS_running_data.hex') + self.mock_response(self._READ_METER_DATA, ILLEGAL_DATA_ADDRESS) def test_GW5000D_NS_runtime_data(self): self.loop.run_until_complete(self.read_device_info()) @@ -255,8 +263,9 @@ class GW5000_MS_Test(DtMock): def __init__(self, methodName='runTest'): DtMock.__init__(self, methodName) - self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW5000-MS_running_data.hex') self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW5000-MS_device_info.hex') + self.mock_response(self._READ_RUNNING_DATA, 'GW5000-MS_running_data.hex') + self.mock_response(self._READ_METER_DATA, ILLEGAL_DATA_ADDRESS) def test_GW6000_MS_device_info(self): self.loop.run_until_complete(self.read_device_info()) @@ -316,7 +325,8 @@ class GW10K_MS_30_Test(DtMock): def __init__(self, methodName='runTest'): DtMock.__init__(self, methodName) self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW10K-MS-30_device_info.hex') - self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW10K-MS-30_running_data.hex') + self.mock_response(self._READ_RUNNING_DATA, 'GW10K-MS-30_running_data.hex') + self.mock_response(self._READ_METER_DATA, ILLEGAL_DATA_ADDRESS) def test_GW10K_MS_30_device_info(self): self.loop.run_until_complete(self.read_device_info()) @@ -375,7 +385,8 @@ class GW10K_MS_TCP_Test(DtMock): def __init__(self, methodName='runTest'): DtMock.__init__(self, methodName, 502) - self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW10K-MS-30_tcp_running_data.hex') + self.mock_response(self._READ_RUNNING_DATA, 'GW10K-MS-30_tcp_running_data.hex') + self.mock_response(self._READ_METER_DATA, ILLEGAL_DATA_ADDRESS) def test_GW10K_MS_TCP_runtime_data(self): self.loop.run_until_complete(self.read_device_info()) @@ -430,8 +441,9 @@ class GW20KAU_DT_Test(DtMock): def __init__(self, methodName='runTest'): DtMock.__init__(self, methodName) - self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW20KAU-DT_running_data.hex') self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW20KAU-DT_device_info.hex') + self.mock_response(self._READ_RUNNING_DATA, 'GW20KAU-DT_running_data.hex') + self.mock_response(self._READ_METER_DATA, ILLEGAL_DATA_ADDRESS) def test_GW20KAU_DT_device_info(self): self.loop.run_until_complete(self.read_device_info()) @@ -497,8 +509,9 @@ class GW17K_DT_Test(DtMock): def __init__(self, methodName='runTest'): DtMock.__init__(self, methodName) - self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW17K-DT_running_data.hex') self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW17K-DT_device_info.hex') + self.mock_response(self._READ_RUNNING_DATA, 'GW17K-DT_running_data.hex') + self.mock_response(self._READ_METER_DATA, ILLEGAL_DATA_ADDRESS) def test_GW20KAU_DT_device_info(self): self.loop.run_until_complete(self.read_device_info())