Skip to content

Commit

Permalink
Create ProtocolResponse class
Browse files Browse the repository at this point in the history
Create ProtocolResponse class providing both response data and the original command.
  • Loading branch information
mletenay committed Dec 16, 2023
1 parent 62b5ab3 commit 2decd98
Show file tree
Hide file tree
Showing 10 changed files with 97 additions and 66 deletions.
5 changes: 3 additions & 2 deletions goodwe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 5 additions & 5 deletions goodwe/dt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -173,17 +173,17 @@ 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:
setting = self._settings.get(setting_id)
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):
Expand Down
33 changes: 17 additions & 16 deletions goodwe/es.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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:
Expand All @@ -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()
Expand All @@ -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:
Expand All @@ -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:
Expand Down
40 changes: 21 additions & 19 deletions goodwe/et.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.")
Expand All @@ -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.")
Expand All @@ -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.")
Expand All @@ -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):
Expand All @@ -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:
Expand Down
10 changes: 5 additions & 5 deletions goodwe/inverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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"):
Expand Down
30 changes: 28 additions & 2 deletions goodwe/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand All @@ -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
Expand All @@ -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."
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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):
"""
Expand Down
4 changes: 2 additions & 2 deletions tests/inverter_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


# -----------------
Expand Down
8 changes: 4 additions & 4 deletions tests/test_dt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
Expand All @@ -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))
Expand Down
Loading

0 comments on commit 2decd98

Please sign in to comment.