From 2decd981cc2b19adc0675bc20bd0978c85c42f16 Mon Sep 17 00:00:00 2001 From: mle Date: Sat, 16 Dec 2023 11:58:17 +0100 Subject: [PATCH] Create ProtocolResponse class Create ProtocolResponse class providing both response data and the original command. --- goodwe/__init__.py | 5 +++-- goodwe/dt.py | 10 +++++----- goodwe/es.py | 33 +++++++++++++++++---------------- goodwe/et.py | 40 +++++++++++++++++++++------------------- goodwe/inverter.py | 10 +++++----- goodwe/protocol.py | 30 ++++++++++++++++++++++++++++-- tests/inverter_check.py | 4 ++-- tests/test_dt.py | 8 ++++---- tests/test_es.py | 15 ++++++++------- tests/test_et.py | 8 ++++---- 10 files changed, 97 insertions(+), 66 deletions(-) diff --git a/goodwe/__init__.py b/goodwe/__init__.py index c759c44..45ade9e 100644 --- a/goodwe/__init__.py +++ b/goodwe/__init__.py @@ -69,8 +69,9 @@ async def discover(host: str, timeout: int = 1, retries: int = 3) -> Inverter: try: logger.debug("Probing inverter at %s.", host) response = await DISCOVERY_COMMAND.execute(host, timeout, retries) - model_name = response[12:22].decode("ascii").rstrip() - serial_number = response[38:54].decode("ascii") + response = response.response_data() + model_name = response[5:15].decode("ascii").rstrip() + serial_number = response[31:47].decode("ascii") inverter_class: Type[Inverter] | None = None for model_tag in ET_MODEL_TAGS: diff --git a/goodwe/dt.py b/goodwe/dt.py index 67f87c4..0959e47 100644 --- a/goodwe/dt.py +++ b/goodwe/dt.py @@ -146,7 +146,7 @@ def _pv1_pv2_only(s: Sensor) -> bool: async def read_device_info(self): response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO) - response = response[5:-2] + response = response.response_data() try: self.model_name = response[22:32].decode("ascii").rstrip() except: @@ -173,8 +173,8 @@ async def read_device_info(self): pass async def read_runtime_data(self, include_unknown_sensors: bool = False) -> Dict[str, Any]: - raw_data = await self._read_from_socket(self._READ_DEVICE_RUNNING_DATA) - data = self._map_response(raw_data[5:-2], self._sensors, include_unknown_sensors) + response = await self._read_from_socket(self._READ_DEVICE_RUNNING_DATA) + data = self._map_response(response, self._sensors, include_unknown_sensors) return data async def read_setting(self, setting_id: str) -> Any: @@ -182,8 +182,8 @@ async def read_setting(self, setting_id: str) -> Any: if not setting: raise ValueError(f'Unknown setting "{setting_id}"') count = (setting.size_ + (setting.size_ % 2)) // 2 - raw_data = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, count)) - with io.BytesIO(raw_data[5:-2]) as buffer: + response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, count)) + with io.BytesIO(response.response_data()) as buffer: return setting.read_value(buffer) async def write_setting(self, setting_id: str, value: Any): diff --git a/goodwe/es.py b/goodwe/es.py index 5e46079..ded05ea 100644 --- a/goodwe/es.py +++ b/goodwe/es.py @@ -188,10 +188,11 @@ def _supports_eco_mode_v2(self) -> bool: async def read_device_info(self): response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO) - self.firmware = self._decode(response[7:12]).rstrip() - self.model_name = self._decode(response[12:22]).rstrip() - self.serial_number = response[38:54].decode("ascii") - self.software_version = self._decode(response[58:70]) + response = response.response_data() + self.firmware = self._decode(response[0:5]).rstrip() + self.model_name = self._decode(response[5:15]).rstrip() + self.serial_number = response[31:47].decode("ascii") + self.software_version = self._decode(response[51:63]) try: if len(self.firmware) >= 2: self.dsp1_version = int(self.firmware[0:2]) @@ -206,8 +207,8 @@ async def read_device_info(self): self._settings.update({s.id_: s for s in self.__settings_arm_fw_14}) async def read_runtime_data(self, include_unknown_sensors: bool = False) -> Dict[str, Any]: - raw_data = await self._read_from_socket(self._READ_DEVICE_RUNNING_DATA) - data = self._map_response(raw_data[7:-2], self.__sensors, include_unknown_sensors) + response = await self._read_from_socket(self._READ_DEVICE_RUNNING_DATA) + data = self._map_response(response, self.__sensors, include_unknown_sensors) return data async def read_setting(self, setting_id: str) -> Any: @@ -221,12 +222,12 @@ async def read_setting(self, setting_id: str) -> Any: raise ValueError(f'Unknown setting "{setting_id}"') count = (setting.size_ + (setting.size_ % 2)) // 2 if self._is_modbus_setting(setting): - raw_data = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, count)) - with io.BytesIO(raw_data[5:-2]) as buffer: + response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, count)) + with io.BytesIO(response.response_data()) as buffer: return setting.read_value(buffer) else: - raw_data = await self._read_from_socket(Aa55ReadCommand(setting.offset, count)) - with io.BytesIO(raw_data[7:-2]) as buffer: + response = await self._read_from_socket(Aa55ReadCommand(setting.offset, count)) + with io.BytesIO(response.response_data()) as buffer: return setting.read_value(buffer) else: all_settings = await self.read_settings_data() @@ -244,11 +245,11 @@ async def write_setting(self, setting_id: str, value: Any): if setting.size_ == 1: # modbus can address/store only 16 bit values, read the other 8 bytes if self._is_modbus_setting(setting): - register_data = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, 1)) - raw_value = setting.encode_value(value, register_data[5:7]) + response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, 1)) + raw_value = setting.encode_value(value, response.response_data()[0:2]) else: - register_data = await self._read_from_socket(Aa55ReadCommand(setting.offset, 1)) - raw_value = setting.encode_value(value, register_data[7:9]) + response = await self._read_from_socket(Aa55ReadCommand(setting.offset, 1)) + raw_value = setting.encode_value(value, response.response_data()[2:4]) else: raw_value = setting.encode_value(value) if len(raw_value) <= 2: @@ -264,8 +265,8 @@ async def write_setting(self, setting_id: str, value: Any): await self._read_from_socket(Aa55WriteMultiCommand(setting.offset, raw_value)) async def read_settings_data(self) -> Dict[str, Any]: - raw_data = await self._read_from_socket(self._READ_DEVICE_SETTINGS_DATA) - data = self._map_response(raw_data[7:-2], self.settings()) + response = await self._read_from_socket(self._READ_DEVICE_SETTINGS_DATA) + data = self._map_response(response, self.settings()) return data async def get_grid_export_limit(self) -> int: diff --git a/goodwe/et.py b/goodwe/et.py index f2449e4..ef08462 100644 --- a/goodwe/et.py +++ b/goodwe/et.py @@ -443,7 +443,7 @@ def _not_extended_meter(s: Sensor) -> bool: async def read_device_info(self): response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO) - response = response[5:-2] + response = response.response_data() # Modbus registers from offset (35000) self.modbus_version = read_unsigned_int(response, 0) self.rated_power = read_unsigned_int(response, 2) @@ -485,14 +485,14 @@ async def read_device_info(self): self._settings.update({s.id_: s for s in self.__settings_arm_fw_22}) async def read_runtime_data(self, include_unknown_sensors: bool = False) -> Dict[str, Any]: - raw_data = await self._read_from_socket(self._READ_RUNNING_DATA) - data = self._map_response(raw_data[5:-2], self._sensors, include_unknown_sensors) + response = await self._read_from_socket(self._READ_RUNNING_DATA) + data = self._map_response(response, self._sensors, include_unknown_sensors) self._has_battery = data.get('battery_mode', 0) != 0 if self._has_battery: try: - raw_data = await self._read_from_socket(self._READ_BATTERY_INFO) - data.update(self._map_response(raw_data[5:-2], self._sensors_battery, include_unknown_sensors)) + response = await self._read_from_socket(self._READ_BATTERY_INFO) + data.update(self._map_response(response, self._sensors_battery, include_unknown_sensors)) except RequestRejectedException as ex: if ex.message == 'ILLEGAL DATA ADDRESS': logger.warning("Cannot read battery values, disabling further attempts.") @@ -501,8 +501,9 @@ async def read_runtime_data(self, include_unknown_sensors: bool = False) -> Dict raise ex if self._has_battery2: try: - raw_data = await self._read_from_socket(self._READ_BATTERY2_INFO) - data.update(self._map_response(raw_data[5:-2], self._sensors_battery2, include_unknown_sensors)) + response = await self._read_from_socket(self._READ_BATTERY2_INFO) + data.update( + self._map_response(response, self._sensors_battery2, include_unknown_sensors)) except RequestRejectedException as ex: if ex.message == 'ILLEGAL DATA ADDRESS': logger.warning("Cannot read battery 2 values, disabling further attempts.") @@ -512,25 +513,26 @@ async def read_runtime_data(self, include_unknown_sensors: bool = False) -> Dict if self._has_meter_extended: try: - raw_data = await self._read_from_socket(self._READ_METER_DATA_EXTENDED) - data.update(self._map_response(raw_data[5:-2], self._sensors_meter, include_unknown_sensors)) + response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED) + data.update(self._map_response(response, self._sensors_meter, include_unknown_sensors)) except RequestRejectedException as ex: if ex.message == 'ILLEGAL DATA ADDRESS': logger.warning("Cannot read extended meter values, disabling further attempts.") self._has_meter_extended = False self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter)) - raw_data = await self._read_from_socket(self._READ_METER_DATA) - data.update(self._map_response(raw_data[5:-2], self._sensors_meter, include_unknown_sensors)) + response = await self._read_from_socket(self._READ_METER_DATA) + data.update( + self._map_response(response, self._sensors_meter, include_unknown_sensors)) else: raise ex else: - raw_data = await self._read_from_socket(self._READ_METER_DATA) - data.update(self._map_response(raw_data[5:-2], self._sensors_meter, include_unknown_sensors)) + response = await self._read_from_socket(self._READ_METER_DATA) + data.update(self._map_response(response, self._sensors_meter, include_unknown_sensors)) if self._has_mptt: try: - raw_data = await self._read_from_socket(self._READ_MPTT_DATA) - data.update(self._map_response(raw_data[5:-2], self._sensors_mptt, include_unknown_sensors)) + response = await self._read_from_socket(self._READ_MPTT_DATA) + data.update(self._map_response(response, self._sensors_mptt, include_unknown_sensors)) except RequestRejectedException as ex: if ex.message == 'ILLEGAL DATA ADDRESS': logger.warning("Cannot read MPPT values, disabling further attempts.") @@ -545,8 +547,8 @@ async def read_setting(self, setting_id: str) -> Any: if not setting: raise ValueError(f'Unknown setting "{setting_id}"') count = (setting.size_ + (setting.size_ % 2)) // 2 - raw_data = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, count)) - with io.BytesIO(raw_data[5:-2]) as buffer: + response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, count)) + with io.BytesIO(response.response_data()) as buffer: return setting.read_value(buffer) async def write_setting(self, setting_id: str, value: Any): @@ -555,8 +557,8 @@ async def write_setting(self, setting_id: str, value: Any): raise ValueError(f'Unknown setting "{setting_id}"') if setting.size_ == 1: # modbus can address/store only 16 bit values, read the other 8 bytes - register_data = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, 1)) - raw_value = setting.encode_value(value, register_data[5:7]) + response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, 1)) + raw_value = setting.encode_value(value, response.response_data()[0:2]) else: raw_value = setting.encode_value(value) if len(raw_value) <= 2: diff --git a/goodwe/inverter.py b/goodwe/inverter.py index bf8ec53..4925978 100644 --- a/goodwe/inverter.py +++ b/goodwe/inverter.py @@ -9,7 +9,7 @@ from typing import Any, Callable, Dict, Tuple, Optional from .exceptions import MaxRetriesException, RequestFailedException -from .protocol import ProtocolCommand +from .protocol import ProtocolCommand, ProtocolResponse logger = logging.getLogger(__name__) @@ -126,7 +126,7 @@ def _ensure_lock(self) -> asyncio.Lock: self._running_loop = asyncio.get_event_loop() return self._lock - async def _read_from_socket(self, command: ProtocolCommand) -> bytes: + async def _read_from_socket(self, command: ProtocolCommand) -> ProtocolResponse: async with self._ensure_lock(): try: result = await command.execute(self.host, self.timeout, self.retries) @@ -191,7 +191,7 @@ async def read_settings_data(self) -> Dict[str, Any]: async def send_command( self, command: bytes, validator: Callable[[bytes], bool] = lambda x: True - ) -> bytes: + ) -> ProtocolResponse: """ Send low level udp command (as bytes). Answer command's raw response data. @@ -281,9 +281,9 @@ def settings(self) -> Tuple[Sensor, ...]: raise NotImplementedError() @staticmethod - def _map_response(resp_data: bytes, sensors: Tuple[Sensor, ...], incl_xx: bool = True) -> Dict[str, Any]: + def _map_response(response: ProtocolResponse, sensors: Tuple[Sensor, ...], incl_xx: bool = True) -> Dict[str, Any]: """Process the response data and return dictionary with runtime values""" - with io.BytesIO(resp_data) as buffer: + with io.BytesIO(response.response_data()) as buffer: result = {} for sensor in sensors: if incl_xx or not sensor.id_.startswith("xx"): diff --git a/goodwe/protocol.py b/goodwe/protocol.py index 3a0123d..bb11e65 100644 --- a/goodwe/protocol.py +++ b/goodwe/protocol.py @@ -81,6 +81,20 @@ def _retry_mechanism(self) -> None: self.response_future.set_exception(MaxRetriesException) +class ProtocolResponse: + """Definition of response to protocol command""" + + def __init__(self, raw_data: bytes, command: ProtocolCommand): + self.raw_data: bytes = raw_data + self.command: ProtocolCommand = command + + def __repr__(self): + return self.raw_data.hex() + + def response_data(self) -> bytes: + return self.command.trim_response(self.raw_data) + + class ProtocolCommand: """Definition of inverter protocol command""" @@ -91,7 +105,11 @@ def __init__(self, request: bytes, validator: Callable[[bytes], bool]): def __repr__(self): return self.request.hex() - async def execute(self, host: str, timeout: int, retries: int) -> bytes: + def trim_response(self, raw_response: bytes): + """Trim raw response from header and checksum data""" + return raw_response + + async def execute(self, host: str, timeout: int, retries: int) -> ProtocolResponse: """ Execute the udp protocol command on the specified address/port. Since the UDP communication is by definition unreliable, when no (valid) response is received by specified @@ -109,7 +127,7 @@ async def execute(self, host: str, timeout: int, retries: int) -> bytes: await response_future result = response_future.result() if result is not None: - return result + return ProtocolResponse(result, self) else: raise RequestFailedException( "No response received to '" + self.request.hex() + "' request." @@ -179,6 +197,10 @@ def _validate_response(data: bytes, response_type: str) -> bool: return False return True + def trim_response(self, raw_response: bytes): + """Trim raw response from header and checksum data""" + return raw_response[7:-2] + class Aa55ReadCommand(Aa55ProtocolCommand): """ @@ -236,6 +258,10 @@ def __init__(self, request: bytes, cmd: int, offset: int, value: int): lambda x: validate_modbus_response(x, cmd, offset, value), ) + def trim_response(self, raw_response: bytes): + """Trim raw response from header and checksum data""" + return raw_response[5:-2] + class ModbusReadCommand(ModbusProtocolCommand): """ diff --git a/tests/inverter_check.py b/tests/inverter_check.py index 7740cf4..a511ad7 100644 --- a/tests/inverter_check.py +++ b/tests/inverter_check.py @@ -84,13 +84,13 @@ # Execute modbus protocol command # ------------------------------- # response = asyncio.run(goodwe.protocol.ModbusReadCommand(COMM_ADDR, 0x88b8, 0x21).execute(IP_ADDRESS, TIMEOUT, RETRIES)) -# print(response.hex()) +# print(response) # ------------------------------- # Execute AA55 protocol command # ------------------------------- # response = asyncio.run(goodwe.protocol.Aa55ProtocolCommand("010200", "0182").execute(IP_ADDRESS, TIMEOUT, RETRIES)) -# print(response.hex()) +# print(response) # ----------------- diff --git a/tests/test_dt.py b/tests/test_dt.py index 6c96956..0683904 100644 --- a/tests/test_dt.py +++ b/tests/test_dt.py @@ -5,7 +5,7 @@ from goodwe.dt import DT from goodwe.exceptions import RequestFailedException -from goodwe.protocol import ProtocolCommand +from goodwe.protocol import ProtocolCommand, ProtocolResponse class DtMock(TestCase, DT): @@ -19,7 +19,7 @@ def __init__(self, methodName='runTest'): def mock_response(self, command: ProtocolCommand, filename: str): self._mock_responses[command] = filename - async def _read_from_socket(self, command: ProtocolCommand) -> bytes: + async def _read_from_socket(self, command: ProtocolCommand) -> ProtocolResponse: """Mock UDP communication""" root_dir = os.path.dirname(os.path.abspath(__file__)) filename = self._mock_responses.get(command) @@ -28,10 +28,10 @@ async def _read_from_socket(self, command: ProtocolCommand) -> bytes: response = bytes.fromhex(f.read()) if not command.validator(response): raise RequestFailedException - return response + return ProtocolResponse(response, command) else: self.request = command.request - return bytes.fromhex("aa557f00010203040506070809") + return ProtocolResponse(bytes.fromhex("aa557f00010203040506070809"), command) def assertSensor(self, sensor, expected_value, expected_unit, data): self.assertEqual(expected_value, data.get(sensor)) diff --git a/tests/test_es.py b/tests/test_es.py index 812607e..b0d3a31 100644 --- a/tests/test_es.py +++ b/tests/test_es.py @@ -7,7 +7,7 @@ from goodwe.es import ES from goodwe.exceptions import RequestFailedException from goodwe.inverter import OperationMode -from goodwe.protocol import ProtocolCommand +from goodwe.protocol import ProtocolCommand, ProtocolResponse class EsMock(TestCase, ES): @@ -21,7 +21,7 @@ def __init__(self, methodName='runTest'): def mock_response(self, command: ProtocolCommand, filename: str): self._mock_responses[command] = filename - async def _read_from_socket(self, command: ProtocolCommand) -> bytes: + async def _read_from_socket(self, command: ProtocolCommand) -> ProtocolResponse: """Mock UDP communication""" root_dir = os.path.dirname(os.path.abspath(__file__)) filename = self._mock_responses.get(command) @@ -30,10 +30,10 @@ async def _read_from_socket(self, command: ProtocolCommand) -> bytes: response = bytes.fromhex(f.read()) if not command.validator(response): raise RequestFailedException - return response + return ProtocolResponse(response, command) else: self.request = command.request - return bytes.fromhex("010203040506070809") + return ProtocolResponse(bytes.fromhex("010203040506070809"), command) def assertSensor(self, sensor, expected_value, expected_unit, data): self.assertEqual(expected_value, data.get(sensor)) @@ -464,9 +464,10 @@ def __init__(self, methodName='runTest'): def test_GW5048_ESA_discovery(self): response = self.loop.run_until_complete(self._read_from_socket(DISCOVERY_COMMAND)) - self.assertEqual(86, len(response)) - self.assertEqual('GW5048-ESA', response[12:22].decode("ascii").rstrip()) - self.assertEqual('95048ESA223W0000', response[38:54].decode("ascii")) + raw_data = response.raw_data + self.assertEqual(86, len(raw_data)) + self.assertEqual('GW5048-ESA', raw_data[12:22].decode("ascii").rstrip()) + self.assertEqual('95048ESA223W0000', raw_data[38:54].decode("ascii")) def test_GW5048_ESA_device_info(self): self.loop.run_until_complete(self.read_device_info()) diff --git a/tests/test_et.py b/tests/test_et.py index dbda968..93c9fee 100644 --- a/tests/test_et.py +++ b/tests/test_et.py @@ -6,7 +6,7 @@ from goodwe.et import ET from goodwe.exceptions import RequestFailedException from goodwe.inverter import OperationMode -from goodwe.protocol import ProtocolCommand +from goodwe.protocol import ProtocolCommand, ProtocolResponse class EtMock(TestCase, ET): @@ -21,7 +21,7 @@ def __init__(self, methodName='runTest'): def mock_response(self, command: ProtocolCommand, filename: str): self._mock_responses[command] = filename - async def _read_from_socket(self, command: ProtocolCommand) -> bytes: + async def _read_from_socket(self, command: ProtocolCommand) -> ProtocolResponse: """Mock UDP communication""" root_dir = os.path.dirname(os.path.abspath(__file__)) filename = self._mock_responses.get(command) @@ -30,11 +30,11 @@ async def _read_from_socket(self, command: ProtocolCommand) -> bytes: response = bytes.fromhex(f.read()) if not command.validator(response): raise RequestFailedException - return response + return ProtocolResponse(response, command) else: self.request = command.request self._list_of_requests.append(command.request) - return bytes.fromhex("aa55f700010203040506070809") + return ProtocolResponse(bytes.fromhex("aa55f700010203040506070809"), command) def assertSensor(self, sensor, expected_value, expected_unit, data): self.assertEqual(expected_value, data.get(sensor))