Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ES support for older ARM versions. #54

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions goodwe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ async def connect(host: str, family: str = None, comm_addr: int = 0, timeout: in
inv = DT(host, comm_addr, timeout, retries)
elif do_discover:
return await discover(host, timeout, retries)
else:
raise InverterError("Specify either an inverter family or set do_discover True")

logger.debug("Connecting to %s family inverter at %s.", family, host)
await inv.read_device_info()
Expand Down
137 changes: 82 additions & 55 deletions goodwe/es.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,12 @@ class ES(Inverter):
Integer("backup_supply", 12, "Backup Supply"),
Integer("off-grid_charge", 14, "Off-grid Charge"),
Integer("shadow_scan", 16, "Shadow Scan", "", Kind.PV),
Integer("grid_export", 18, "Grid Export Enabled", "", Kind.GRID),
Integer("grid_export", 18, "Export Limit Enabled", "", Kind.GRID),
Integer("capacity", 22, "Capacity"),
Integer("charge_v", 24, "Charge Voltage", "V"),
Decimal("charge_v", 24, 10, "Charge Voltage", "V"),
Integer("charge_i", 26, "Charge Current", "A", ),
Integer("discharge_i", 28, "Discharge Current", "A", ),
Integer("discharge_v", 30, "Discharge Voltage", "V"),
Decimal("discharge_v", 30, 10, "Discharge Voltage", "V"),
Calculated("dod", lambda data: 100 - read_bytes2(data, 32), "Depth of Discharge", "%"),
Integer("battery_activated", 34, "Battery Activated"),
Integer("bp_off_grid_charge", 36, "BP Off-grid Charge"),
Expand All @@ -145,7 +145,16 @@ class ES(Inverter):
Integer("battery_soc_protection", 56, "Battery SoC Protection", "", Kind.BAT),
Integer("work_mode", 66, "Work Mode"),
Integer("grid_quality_check", 68, "Grid Quality Check"),
)

# Settings available in ARM firmware 3
__settings_arm_fw_3: Tuple[Sensor, ...] = (
EcoModeV0("eco_charge", 0, "Eco Mode Charge"),
EcoModeV0("eco_discharge", 6, "Eco Mode Discharge"),
)

# Settings added after ARM firmware 3
__settings_arm_fw_4: Tuple[Sensor, ...] = (
EcoModeV1("eco_mode_1", 1793, "Eco Mode Group 1"), # 0x701
ByteH("eco_mode_1_switch", 1796, "Eco Mode Group 1 Switch", "", Kind.BAT),
EcoModeV1("eco_mode_2", 1797, "Eco Mode Group 2"),
Expand Down Expand Up @@ -175,6 +184,9 @@ def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int
self.comm_addr = 0xf7
self._settings: dict[str, Sensor] = {s.id_: s for s in self.__all_settings}

def _supports_eco_mode_v1(self):
return self.arm_version > 3

def _supports_eco_mode_v2(self) -> bool:
if self.arm_version < 14:
return False
Expand Down Expand Up @@ -202,6 +214,10 @@ async def read_device_info(self):
except ValueError:
logger.exception("Error decoding firmware version %s.", self.firmware)

if self._supports_eco_mode_v1():
self._settings.update({s.id_: s for s in self.__settings_arm_fw_4})
else:
self._settings.update({s.id_: s for s in self.__settings_arm_fw_3})
if self._supports_eco_mode_v2():
self._settings.update({s.id_: s for s in self.__settings_arm_fw_14})

Expand Down Expand Up @@ -247,7 +263,7 @@ async def write_setting(self, setting_id: str, value: Any):
register_data = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, 1))
raw_value = setting.encode_value(value, register_data[5:7])
else:
register_data = await self._read_from_socket(Aa55ReadCommand(self.comm_addr, setting.offset, 1))
register_data = await self._read_from_socket(Aa55ReadCommand(setting.offset, 1))
raw_value = setting.encode_value(value, register_data[7:9])
else:
raw_value = setting.encode_value(value)
Expand All @@ -265,17 +281,28 @@ async def write_setting(self, setting_id: str, value: Any):

async def read_settings_data(self) -> Dict[str, Any]:
raw_data = await self._read_from_socket(self._READ_DEVICE_SETTINGS_DATA)
# print("Received: ", end="")
# for byte in raw_data:
# print("%02X " % byte, end="")
# print("")
data = self._map_response(raw_data[7:-2], self.settings())
return data

async def get_grid_export_limit(self) -> int:
return await self.read_setting('grid_export_limit')

async def set_grid_export_limit(self, export_limit: int) -> None:
if 0 <= export_limit <= 10000:
await self._read_from_socket(
Aa55ProtocolCommand("033502" + "{:04x}".format(export_limit), "03b5")
)
enabled = export_limit >= 0
await self._read_from_socket(
Aa55ProtocolCommand("035301" + "{:02x}".format(int(enabled)), "03d3")
)
if enabled:
if 0 <= export_limit <= 10000:
await self._read_from_socket(
Aa55ProtocolCommand("033502" + "{:04x}".format(export_limit), "03b5")
)
else:
raise ValueError()

async def get_operation_modes(self, include_emulated: bool) -> Tuple[OperationMode, ...]:
result = [e for e in OperationMode]
Expand All @@ -286,16 +313,21 @@ async def get_operation_modes(self, include_emulated: bool) -> Tuple[OperationMo
return tuple(result)

async def get_operation_mode(self) -> OperationMode:
mode = OperationMode(await self.read_setting('work_mode'))
settings = await self.read_settings_data()
mode = OperationMode(settings.get('work_mode'))
if OperationMode.ECO != mode:
return mode
ecomode = await self.read_setting('eco_mode_1')
if ecomode.is_eco_charge_mode():
if 'eco_mode_1' in self.settings():
ecomode = await self.read_setting('eco_mode_1')
if ecomode.is_eco_charge_mode():
return OperationMode.ECO_CHARGE
elif ecomode.is_eco_discharge_mode():
return OperationMode.ECO_DISCHARGE
elif 'eco_charge' in settings and settings.get('eco_charge').is_fulltime():
return OperationMode.ECO_CHARGE
elif ecomode.is_eco_discharge_mode():
elif 'eco_discharge' in settings and settings.get('eco_discharge').is_fulltime():
return OperationMode.ECO_DISCHARGE
else:
return OperationMode.ECO
return mode

async def set_operation_mode(self, operation_mode: OperationMode, eco_mode_power: int = 100,
eco_mode_soc: int = 100) -> None:
Expand All @@ -314,15 +346,24 @@ 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, ""))
if operation_mode == OperationMode.ECO_CHARGE:
await self.write_setting('eco_mode_1', eco_mode.encode_charge(eco_mode_power, eco_mode_soc))
else:
await self.write_setting('eco_mode_1', eco_mode.encode_discharge(eco_mode_power))
await self.write_setting('eco_mode_2_switch', 0)
await self.write_setting('eco_mode_3_switch', 0)
await self.write_setting('eco_mode_4_switch', 0)
await self._set_eco_mode()
if self._settings.get('eco_mode_1') is not None:
eco_mode: EcoMode = self._convert_eco_mode(EcoModeV2("", 0, ""))
if operation_mode == OperationMode.ECO_CHARGE:
await self.write_setting('eco_mode_1', eco_mode.encode_charge(eco_mode_power, eco_mode_soc))
else:
await self.write_setting('eco_mode_1', eco_mode.encode_discharge(eco_mode_power))
await self.write_setting('eco_mode_2_switch', 0)
await self.write_setting('eco_mode_3_switch', 0)
await self.write_setting('eco_mode_4_switch', 0)
await self._set_eco_mode()
elif self._settings.get('eco_charge') is not None:
if operation_mode == OperationMode.ECO_CHARGE:
await self._set_limit_power_for_discharge(TimeLimit.none())
await self._set_limit_power_for_charge(TimeLimit.fulltime(eco_mode_power))
else:
await self._set_limit_power_for_charge(TimeLimit.none())
await self._set_limit_power_for_discharge(TimeLimit.fulltime(eco_mode_power))
await self._set_eco_mode()

async def get_ongrid_battery_dod(self) -> int:
return await self.read_setting('dod')
Expand All @@ -345,62 +386,48 @@ async def _set_general_mode(self) -> None:
if self._supports_eco_mode_v2():
await self._clear_battery_mode_param()
else:
await self._set_limit_power_for_charge(0, 0, 0, 0, 0)
await self._set_limit_power_for_discharge(0, 0, 0, 0, 0)
await self._set_limit_power_for_charge(TimeLimit())
await self._set_limit_power_for_discharge(TimeLimit())
await self._clear_battery_mode_param()
else:
await self._set_limit_power_for_charge(0, 0, 0, 0, 0)
await self._set_limit_power_for_discharge(0, 0, 0, 0, 0)
await self._set_offgrid_work_mode(0)
await self._set_work_mode(0)
await self._set_work_mode(OperationMode.GENERAL)

async def _set_offgrid_mode(self) -> None:
if self.arm_version >= 7:
await self._clear_battery_mode_param()
else:
await self._set_limit_power_for_charge(0, 0, 23, 59, 0)
await self._set_limit_power_for_discharge(0, 0, 0, 0, 0)
await self._set_offgrid_work_mode(1)
await self._set_relay_control(3)
await self._set_store_energy_mode(0)
await self._set_work_mode(1)
await self._set_work_mode(OperationMode.OFF_GRID)

async def _set_backup_mode(self) -> None:
if self.arm_version >= 7:
if self._supports_eco_mode_v2():
await self._clear_battery_mode_param()
else:
await self._clear_battery_mode_param()
await self._set_limit_power_for_charge(0, 0, 23, 59, 10)
else:
await self._set_limit_power_for_charge(0, 0, 23, 59, 10)
await self._set_limit_power_for_discharge(0, 0, 0, 0, 0)
await self._set_limit_power_for_charge(TimeLimit.fulltime(10))
await self._set_offgrid_work_mode(0)
await self._set_work_mode(2)
await self._set_work_mode(OperationMode.BACKUP)

async def _set_eco_mode(self) -> None:
await self._set_offgrid_work_mode(0)
await self._set_work_mode(3)
await self._set_work_mode(OperationMode.ECO)

async def _clear_battery_mode_param(self) -> None:
await self._read_from_socket(Aa55WriteCommand(0x0700, 1))

async def _set_limit_power_for_charge(self, startH: int, startM: int, stopH: int, stopM: int, limit: int) -> None:
if limit < 0 or limit > 100:
raise ValueError()
await self._read_from_socket(Aa55ProtocolCommand("032c05"
+ "{:02x}".format(startH) + "{:02x}".format(startM)
+ "{:02x}".format(stopH) + "{:02x}".format(stopM)
+ "{:02x}".format(limit), "03AC"))

async def _set_limit_power_for_discharge(self, startH: int, startM: int, stopH: int, stopM: int,
limit: int) -> None:
if limit < 0 or limit > 100:
raise ValueError()
await self._read_from_socket(Aa55ProtocolCommand("032d05"
+ "{:02x}".format(startH) + "{:02x}".format(startM)
+ "{:02x}".format(stopH) + "{:02x}".format(stopM)
+ "{:02x}".format(limit), "03AD"))
async def _set_limit_power(self, command: int, limit: TimeLimit) -> None:
data = limit.encode_value()
data_len = len(data)
await self._read_from_socket(
Aa55ProtocolCommand(f"03{command:02x}{data_len:02x}{data.hex()}", f"03{command + 0x80:02X}"))

async def _set_limit_power_for_charge(self, limit: TimeLimit) -> None:
await self._set_limit_power(0x2c, limit)

async def _set_limit_power_for_discharge(self, limit: TimeLimit) -> None:
await self._set_limit_power(0x2d, limit)

async def _set_offgrid_work_mode(self, mode: int) -> None:
await self._read_from_socket(Aa55ProtocolCommand("033601" + "{:02x}".format(mode), "03B6"))
Expand Down
5 changes: 2 additions & 3 deletions goodwe/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ def datagram_received(self, data: bytes, addr: Tuple[str, int]) -> None:
self.response_future.set_result(data)
else:
logger.debug("Received invalid response: %s", data.hex())
self._retries += 1
self._send_request()
# Ignore invalid response, wait until our time is up for the correct response.
except RequestRejectedException as ex:
logger.debug("Received exception response: %s", data.hex())
self.response_future.set_exception(ex)
Expand All @@ -66,7 +65,7 @@ def _send_request(self) -> None:
logger.debug("Sending: %s%s", self.command,
f' - retry #{self._retries}/{self._max_retries}' if self._retries > 0 else '')
self._transport.sendto(self.command.request)
asyncio.get_event_loop().call_later(self._retry_timeout, self._retry_mechanism)
asyncio.get_event_loop().call_later(self._retry_timeout * (self._retries + 1), self._retry_mechanism)

def _retry_mechanism(self) -> None:
"""Retry mechanism to prevent hanging transport"""
Expand Down
Loading