Skip to content

Commit

Permalink
Improve detection of EcoMode type
Browse files Browse the repository at this point in the history
Do not rely on firmware number (is limited to ETU type), probe the settings directly if it is present or not.
  • Loading branch information
mletenay committed Feb 17, 2024
1 parent bc0676a commit 925f37d
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 59 deletions.
73 changes: 36 additions & 37 deletions goodwe/es.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import logging
from typing import Tuple, cast
from typing import Tuple

from .exceptions import InverterError
from .inverter import Inverter
Expand Down Expand Up @@ -220,17 +220,20 @@ async def read_setting(self, setting_id: str) -> Any:
setting: Sensor | None = self._settings.get(setting_id)
if not setting:
raise ValueError(f'Unknown setting "{setting_id}"')
count = (setting.size_ + (setting.size_ % 2)) // 2
if self._is_modbus_setting(setting):
response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, count))
return setting.read_value(response)
else:
response = await self._read_from_socket(Aa55ReadCommand(setting.offset, count))
return setting.read_value(response)
return await self._read_setting(setting)
else:
all_settings = await self.read_settings_data()
return all_settings.get(setting_id)

async def _read_setting(self, setting: Sensor) -> Any:
count = (setting.size_ + (setting.size_ % 2)) // 2
if self._is_modbus_setting(setting):
response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, count))
return setting.read_value(response)
else:
response = await self._read_from_socket(Aa55ReadCommand(setting.offset, count))
return setting.read_value(response)

async def write_setting(self, setting_id: str, value: Any):
if setting_id == 'time':
await self._read_from_socket(
Expand All @@ -240,27 +243,30 @@ async def write_setting(self, setting_id: str, value: Any):
setting: Sensor | None = self._settings.get(setting_id)
if not setting:
raise ValueError(f'Unknown setting "{setting_id}"')
if setting.size_ == 1:
# modbus can address/store only 16 bit values, read the other 8 bytes
if self._is_modbus_setting(setting):
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:
response = await self._read_from_socket(Aa55ReadCommand(setting.offset, 1))
raw_value = setting.encode_value(value, response.response_data()[2:4])
await self._write_setting(setting, value)

async def _write_setting(self, setting: Sensor, 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):
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:
value = int.from_bytes(raw_value, byteorder="big", signed=True)
if self._is_modbus_setting(setting):
await self._read_from_socket(ModbusWriteCommand(self.comm_addr, setting.offset, value))
else:
await self._read_from_socket(Aa55WriteCommand(setting.offset, value))
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:
value = int.from_bytes(raw_value, byteorder="big", signed=True)
if self._is_modbus_setting(setting):
await self._read_from_socket(ModbusWriteCommand(self.comm_addr, setting.offset, value))
else:
await self._read_from_socket(Aa55WriteCommand(setting.offset, value))
else:
if self._is_modbus_setting(setting):
await self._read_from_socket(ModbusWriteMultiCommand(self.comm_addr, setting.offset, raw_value))
else:
if self._is_modbus_setting(setting):
await self._read_from_socket(ModbusWriteMultiCommand(self.comm_addr, setting.offset, raw_value))
else:
await self._read_from_socket(Aa55WriteMultiCommand(setting.offset, raw_value))
await self._read_from_socket(Aa55WriteMultiCommand(setting.offset, raw_value))

async def read_settings_data(self) -> Dict[str, Any]:
response = await self._read_from_socket(self._READ_DEVICE_SETTINGS_DATA)
Expand Down Expand Up @@ -313,7 +319,8 @@ async def set_operation_mode(self, operation_mode: OperationMode, eco_mode_power
raise ValueError()
if eco_mode_soc < 0 or eco_mode_soc > 100:
raise ValueError()
eco_mode: EcoMode = self._convert_eco_mode(EcoModeV2("", 0, ""))
eco_mode: EcoMode | Sensor = self._settings.get('eco_mode_1')
await self._read_setting(eco_mode)
if operation_mode == OperationMode.ECO_CHARGE:
await self.write_setting('eco_mode_1', eco_mode.encode_charge(eco_mode_power, eco_mode_soc))
else:
Expand Down Expand Up @@ -427,13 +434,5 @@ async def _set_store_energy_mode(self, mode: int) -> None:
async def _set_work_mode(self, mode: int) -> None:
await self._read_from_socket(Aa55ProtocolCommand("035901" + "{:02x}".format(mode), "03D9"))

def _convert_eco_mode(self, sensor: Sensor) -> Sensor | EcoMode:
if EcoModeV1 == type(sensor) and self._supports_eco_mode_v2():
return cast(EcoModeV1, sensor).as_eco_mode_v2()
elif EcoModeV2 == type(sensor) and not self._supports_eco_mode_v2():
return cast(EcoModeV2, sensor).as_eco_mode_v1()
else:
return sensor

def _is_modbus_setting(self, sensor: Sensor) -> bool:
return EcoModeV2 == type(sensor) or sensor.offset > 30000
return sensor.offset > 30000
46 changes: 27 additions & 19 deletions goodwe/et.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import logging
from typing import Tuple, cast
from typing import Tuple

from .exceptions import RequestRejectedException
from .inverter import Inverter
Expand Down Expand Up @@ -411,6 +411,8 @@ def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int
self._READ_BATTERY_INFO: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x9088, 0x0018)
self._READ_BATTERY2_INFO: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x9858, 0x0016)
self._READ_MPPT_DATA: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x89e5, 0x3d)
self._has_eco_mode_v2: bool = True
self._has_peak_shaving: bool = True
self._has_battery: bool = True
self._has_battery2: bool = False
self._has_meter_extended: bool = False
Expand All @@ -422,12 +424,6 @@ def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int
self._sensors_mppt = self.__all_sensors_mppt
self._settings: dict[str, Sensor] = {s.id_: s for s in self.__all_settings}

def _supports_eco_mode_v2(self) -> bool:
return self.arm_version >= 19 or 'ETU' not in self.serial_number

def _supports_peak_shaving(self) -> bool:
return self.arm_version >= 22 or 'ETU' not in self.serial_number

@staticmethod
def _single_phase_only(s: Sensor) -> bool:
"""Filter to exclude phase2/3 sensors on single phase inverters"""
Expand Down Expand Up @@ -474,10 +470,23 @@ async def read_device_info(self):
else:
self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter))

if self.arm_version >= 19 or 'ETU' not in self.serial_number:
# Check and add EcoModeV2 settings added in (ETU fw 19)
try:
await self._read_from_socket(ModbusReadCommand(self.comm_addr, 47547, 6))
self._settings.update({s.id_: s for s in self.__settings_arm_fw_19})
if self.arm_version >= 22 or 'ETU' not in self.serial_number:
except RequestRejectedException as ex:
if ex.message == 'ILLEGAL DATA ADDRESS':
logger.debug("Cannot read EcoModeV2 settings, using to EcoModeV1.")
self._has_eco_mode_v2 = False

# Check and add Peak Shaving settings added in (ETU fw 22)
try:
await self._read_from_socket(ModbusReadCommand(self.comm_addr, 47589, 6))
self._settings.update({s.id_: s for s in self.__settings_arm_fw_22})
except RequestRejectedException as ex:
if ex.message == 'ILLEGAL DATA ADDRESS':
logger.debug("Cannot read PeakShaving setting, disabling it.")
self._has_peak_shaving = False

async def read_runtime_data(self) -> Dict[str, Any]:
response = await self._read_from_socket(self._READ_RUNNING_DATA)
Expand Down Expand Up @@ -541,6 +550,9 @@ 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}"')
return await self._read_setting(setting)

async def _read_setting(self, setting: Sensor) -> Any:
count = (setting.size_ + (setting.size_ % 2)) // 2
response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, count))
return setting.read_value(response)
Expand All @@ -549,6 +561,9 @@ async def write_setting(self, setting_id: str, value: Any):
setting = self._settings.get(setting_id)
if not setting:
raise ValueError(f'Unknown setting "{setting_id}"')
await self._write_setting(setting, value)

async def _write_setting(self, setting: Sensor, value: Any):
if setting.size_ == 1:
# modbus can address/store only 16 bit values, read the other 8 bytes
response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, 1))
Expand Down Expand Up @@ -581,7 +596,7 @@ async def set_grid_export_limit(self, export_limit: int) -> None:

async def get_operation_modes(self, include_emulated: bool) -> Tuple[OperationMode, ...]:
result = [e for e in OperationMode]
if not self._supports_peak_shaving():
if not self._has_peak_shaving:
result.remove(OperationMode.PEAK_SHAVING)
if not include_emulated:
result.remove(OperationMode.ECO_CHARGE)
Expand Down Expand Up @@ -626,7 +641,8 @@ async def set_operation_mode(self, operation_mode: OperationMode, eco_mode_power
raise ValueError()
if eco_mode_soc < 0 or eco_mode_soc > 100:
raise ValueError()
eco_mode: EcoMode = self._convert_eco_mode(EcoModeV2("", 0, ""))
eco_mode: EcoMode | Sensor = self._settings.get('eco_mode_1')
await self._read_setting(eco_mode)
if operation_mode == OperationMode.ECO_CHARGE:
await self.write_setting('eco_mode_1', eco_mode.encode_charge(eco_mode_power, eco_mode_soc))
else:
Expand Down Expand Up @@ -663,11 +679,3 @@ async def _clear_battery_mode_param(self) -> None:
async def _set_offline(self, mode: bool) -> None:
value = bytes.fromhex('00070000') if mode else bytes.fromhex('00010000')
await self._read_from_socket(ModbusWriteMultiCommand(self.comm_addr, 0xb997, value))

def _convert_eco_mode(self, sensor: Sensor) -> Sensor | EcoMode:
if EcoModeV1 == type(sensor) and self._supports_eco_mode_v2():
return cast(EcoModeV1, sensor).as_eco_mode_v2()
elif EcoModeV2 == type(sensor) and not self._supports_eco_mode_v2():
return cast(EcoModeV2, sensor).as_eco_mode_v1()
else:
return sensor
21 changes: 20 additions & 1 deletion goodwe/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def _retry_mechanism(self) -> None:
class ProtocolResponse:
"""Definition of response to protocol command"""

def __init__(self, raw_data: bytes, command: ProtocolCommand):
def __init__(self, raw_data: bytes, command: Optional[ProtocolCommand]):
self.raw_data: bytes = raw_data
self.command: ProtocolCommand = command
self._bytes: io.BytesIO = io.BytesIO(self.response_data())
Expand Down Expand Up @@ -116,6 +116,15 @@ def __init__(self, request: bytes, validator: Callable[[bytes], bool]):
self.request: bytes = request
self.validator: Callable[[bytes], bool] = validator

def __eq__(self, other):
if not isinstance(other, ProtocolCommand):
# don't attempt to compare against unrelated types
return NotImplemented
return self.request == other.request

def __hash__(self):
return hash(self.request)

def __repr__(self):
return self.request.hex()

Expand Down Expand Up @@ -276,6 +285,7 @@ def __init__(self, request: bytes, cmd: int, offset: int, value: int):
lambda x: validate_modbus_response(x, cmd, offset, value),
)
self.first_address: int = offset
self.value = value

def trim_response(self, raw_response: bytes):
"""Trim raw response from header and checksum data"""
Expand All @@ -296,6 +306,12 @@ def __init__(self, comm_addr: int, offset: int, count: int):
create_modbus_request(comm_addr, MODBUS_READ_CMD, offset, count),
MODBUS_READ_CMD, offset, count)

def __repr__(self):
if self.value > 1:
return f'READ {self.value} registers from {self.first_address} ({self.request.hex()})'
else:
return f'READ register {self.first_address} ({self.request.hex()})'


class ModbusWriteCommand(ModbusProtocolCommand):
"""
Expand All @@ -307,6 +323,9 @@ def __init__(self, comm_addr: int, register: int, value: int):
create_modbus_request(comm_addr, MODBUS_WRITE_CMD, register, value),
MODBUS_WRITE_CMD, register, value)

def __repr__(self):
return f'WRITE {self.value} to register {self.first_address} ({self.request.hex()})'


class ModbusWriteMultiCommand(ModbusProtocolCommand):
"""
Expand Down
1 change: 1 addition & 0 deletions tests/sample/et/eco_mode_v1.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
aa55f703080000173b0014007f6f5a
1 change: 1 addition & 0 deletions tests/sample/et/eco_mode_v2.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
aa55f7030c0000173b007f0014006400008d53
11 changes: 9 additions & 2 deletions tests/test_et.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
from unittest import TestCase

from goodwe.et import ET
from goodwe.exceptions import RequestFailedException
from goodwe.exceptions import RequestRejectedException, RequestFailedException
from goodwe.inverter import OperationMode
from goodwe.protocol import ProtocolCommand, ProtocolResponse
from goodwe.protocol import ModbusReadCommand, ProtocolCommand, ProtocolResponse


class EtMock(TestCase, ET):
Expand All @@ -26,6 +26,8 @@ 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')
with open(root_dir + '/sample/et/' + filename, 'r') as f:
response = bytes.fromhex(f.read())
if not command.validator(response):
Expand Down Expand Up @@ -54,6 +56,9 @@ def __init__(self, methodName='runTest'):
self.mock_response(self._READ_RUNNING_DATA, 'GW10K-ET_running_data.hex')
self.mock_response(self._READ_METER_DATA, 'GW10K-ET_meter_data.hex')
self.mock_response(self._READ_BATTERY_INFO, 'GW10K-ET_battery_info.hex')
self.mock_response(ModbusReadCommand(self.comm_addr, 47547, 6), 'ILLEGAL DATA ADDRESS')
self.mock_response(ModbusReadCommand(self.comm_addr, 47589, 6), 'ILLEGAL DATA ADDRESS')
self.mock_response(ModbusReadCommand(self.comm_addr, 47515, 4), 'eco_mode_v1.hex')

def test_GW10K_ET_device_info(self):
self.loop.run_until_complete(self.read_device_info())
Expand Down Expand Up @@ -305,6 +310,8 @@ class GW10K_ET_fw819_Test(EtMock):
def __init__(self, methodName='runTest'):
EtMock.__init__(self, methodName)
self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW10K-ET_device_info_fw819.hex')
self.mock_response(ModbusReadCommand(self.comm_addr, 47547, 6), 'eco_mode_v2.hex')
self.mock_response(ModbusReadCommand(self.comm_addr, 47589, 6), 'ILLEGAL DATA ADDRESS')
asyncio.get_event_loop().run_until_complete(self.read_device_info())

def test_GW10K_ET_fw819_device_info(self):
Expand Down

0 comments on commit 925f37d

Please sign in to comment.