Skip to content

Commit

Permalink
Improve support for 745 platform inverters
Browse files Browse the repository at this point in the history
ETT and ESN inverters are based on a new platform called 745
  • Loading branch information
Brumhilde committed Mar 6, 2024
1 parent 925f37d commit a6b1696
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 43 deletions.
9 changes: 5 additions & 4 deletions goodwe/es.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int
if not self.comm_addr:
# Set the default inverter address
self.comm_addr = 0xf7
self._eco_mode: ScheduleType = ScheduleType.ECO_MODE
self._settings: dict[str, Sensor] = {s.id_: s for s in self.__all_settings}

def _supports_eco_mode_v2(self) -> bool:
Expand Down Expand Up @@ -295,9 +296,9 @@ async def get_operation_mode(self) -> OperationMode:
if OperationMode.ECO != mode:
return mode
ecomode = await self.read_setting('eco_mode_1')
if ecomode.is_eco_charge_mode():
if ecomode.is_eco_charge_mode(self._eco_mode):
return OperationMode.ECO_CHARGE
elif ecomode.is_eco_discharge_mode():
elif ecomode.is_eco_discharge_mode(self._eco_mode):
return OperationMode.ECO_DISCHARGE
else:
return OperationMode.ECO
Expand All @@ -322,9 +323,9 @@ async def set_operation_mode(self, operation_mode: OperationMode, eco_mode_power
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))
await self.write_setting('eco_mode_1', eco_mode.encode_charge(self._eco_mode, 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_1', eco_mode.encode_discharge(self._eco_mode, 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)
Expand Down
53 changes: 42 additions & 11 deletions goodwe/et.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from .inverter import Inverter
from .inverter import OperationMode
from .inverter import SensorKind as Kind
from .model import is_2_battery, is_4_mppt, is_single_phase
from .model import is_2_battery, is_4_mppt, is_single_phase, is_745_platform
from .protocol import ProtocolCommand, ModbusReadCommand, ModbusWriteCommand, ModbusWriteMultiCommand
from .sensor import *

Expand Down Expand Up @@ -387,6 +387,10 @@ class ET(Inverter):
Integer("load_control_soc", 47596, "Load Control SoC", "", Kind.AC),

Integer("fast_charging_power", 47603, "Fast Charging Power", "%", Kind.BAT),

Integer("backup_mode_enable", 47605, "Enable Backup Mode"),
Integer("smart_charging_mode_enable", 47609, "Enable Smart Charging Mode"),
Integer("eco_mode_enable", 47612, "Enable Eco Mode"),
)

# Settings added in ARM firmware 22
Expand All @@ -411,8 +415,10 @@ 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._eco_mode: ScheduleType = ScheduleType.ECO_MODE
self._has_eco_mode_v2: bool = True
self._has_peak_shaving: bool = True
self._has_self_use: bool = False
self._has_battery: bool = True
self._has_battery2: bool = False
self._has_meter_extended: bool = False
Expand Down Expand Up @@ -470,6 +476,10 @@ async def read_device_info(self):
else:
self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter))

if is_745_platform(self):
self._eco_mode = ScheduleType.ECO_MODE_745
self._has_self_use = True

# Check and add EcoModeV2 settings added in (ETU fw 19)
try:
await self._read_from_socket(ModbusReadCommand(self.comm_addr, 47547, 6))
Expand Down Expand Up @@ -601,41 +611,55 @@ async def get_operation_modes(self, include_emulated: bool) -> Tuple[OperationMo
if not include_emulated:
result.remove(OperationMode.ECO_CHARGE)
result.remove(OperationMode.ECO_DISCHARGE)
if not self._has_self_use:
result.remove(OperationMode.SELF_USE)
return tuple(result)

async def get_operation_mode(self) -> OperationMode:
mode = OperationMode(await self.read_setting('work_mode'))
if OperationMode.ECO != mode:
return mode
ecomode = await self.read_setting('eco_mode_1')
if ecomode.is_eco_charge_mode():
if ecomode.is_eco_charge_mode(self._eco_mode):
return OperationMode.ECO_CHARGE
elif ecomode.is_eco_discharge_mode():
elif ecomode.is_eco_discharge_mode(self._eco_mode):
return OperationMode.ECO_DISCHARGE
else:
return OperationMode.ECO

async def set_operation_mode(self, operation_mode: OperationMode, eco_mode_power: int = 100,
eco_mode_soc: int = 100) -> None:
if operation_mode == OperationMode.GENERAL:
await self.write_setting('work_mode', 0)
await self.write_setting('work_mode', operation_mode)
await self._set_offline(False)
await self._clear_battery_mode_param()
await self.disable_all_modes()
elif operation_mode == OperationMode.OFF_GRID:
await self.write_setting('work_mode', 1)
await self.write_setting('work_mode', operation_mode)
await self.disable_all_modes()
await self._set_offline(True)
await self.write_setting('backup_supply', 1)
await self.write_setting('cold_start', 4)
elif operation_mode == OperationMode.BACKUP:
await self.write_setting('work_mode', 2)
await self.write_setting('work_mode', operation_mode)
await self._set_offline(False)
await self._clear_battery_mode_param()
await self.disable_all_modes()
await self.write_setting("backup_mode_enable", 1)
elif operation_mode == OperationMode.ECO:
await self.write_setting('work_mode', 3)
await self.write_setting('work_mode', operation_mode)
await self._set_offline(False)
await self.disable_all_modes()
await self.write_setting("eco_mode_enable", 1)
elif operation_mode == OperationMode.PEAK_SHAVING:
await self.write_setting('work_mode', 4)
await self.write_setting('work_mode', operation_mode)
await self._set_offline(False)
await self.disable_all_modes()
elif operation_mode == OperationMode.SELF_USE:
await self.write_setting('work_mode', operation_mode)
await self._set_offline(False)
await self._clear_battery_mode_param()
await self.disable_all_modes()
elif operation_mode in (OperationMode.ECO_CHARGE, OperationMode.ECO_DISCHARGE):
if eco_mode_power < 0 or eco_mode_power > 100:
raise ValueError()
Expand All @@ -644,14 +668,21 @@ async def set_operation_mode(self, operation_mode: OperationMode, eco_mode_power
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))
await self.write_setting('eco_mode_1', eco_mode.encode_charge(self._eco_mode, 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_1', eco_mode.encode_discharge(self._eco_mode, 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.write_setting('work_mode', 3)
await self.write_setting('work_mode', OperationMode.ECO)
await self._set_offline(False)
await self.disable_all_modes()
await self.write_setting("eco_mode_enable", 1)

async def disable_all_modes(self):
await self.write_setting("backup_mode_enable", 0)
await self.write_setting("smart_charging_mode_enable", 0)
await self.write_setting("eco_mode_enable", 0)

async def get_ongrid_battery_dod(self) -> int:
return 100 - await self.read_setting('battery_discharge_depth')
Expand Down
6 changes: 4 additions & 2 deletions goodwe/inverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class OperationMode(IntEnum):
BACKUP - Backup mode
ECO - Eco mode
PEAK_SHAVING - Peak shaving mode
SELF_USE - Self use mode
ECO_CHARGE - Eco mode with a single "Charge" group valid all the time (from 00:00-23:59, Mon-Sun)
ECO_DISCHARGE - Eco mode with a single "Discharge" group valid all the time (from 00:00-23:59, Mon-Sun)
"""
Expand All @@ -76,8 +77,9 @@ class OperationMode(IntEnum):
BACKUP = 2
ECO = 3
PEAK_SHAVING = 4
ECO_CHARGE = 5
ECO_DISCHARGE = 6
SELF_USE = 5
ECO_CHARGE = 6
ECO_DISCHARGE = 7


class Inverter(ABC):
Expand Down
6 changes: 5 additions & 1 deletion goodwe/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Serial number tags to identify inverter type
ET_MODEL_TAGS = ["ETU", "ETL", "ETR", "ETC", "EHU", "EHR", "EHB", "BTU", "BTN", "BTC", "BHU", "AES", "ABP", "HHI",
"HSB", "HUA", "CUA",
"HSB", "HUA", "CUA", "ETT",
"ESN", "EMN", "ERN", "EBN", # ES Gen 2
"HLB", "HMB", "HBB", "SPN"] # Gen 2
ES_MODEL_TAGS = ["ESU", "EMU", "ESA", "BPS", "BPU", "EMJ", "IJL"]
Expand All @@ -23,6 +23,7 @@

BAT_2_MODELS = ["25KET", "29K9ET"]

PLATFORM_745_MODELS = ["ETT", "ESN"]

def is_single_phase(inverter: Inverter) -> bool:
return any(model in inverter.serial_number for model in SINGLE_PHASE_MODELS)
Expand All @@ -38,3 +39,6 @@ def is_4_mppt(inverter: Inverter) -> bool:

def is_2_battery(inverter: Inverter) -> bool:
return any(model in inverter.serial_number for model in BAT_2_MODELS)

def is_745_platform(inverter: Inverter) -> bool:
return any(model in inverter.serial_number for model in PLATFORM_745_MODELS)
53 changes: 28 additions & 25 deletions goodwe/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ class ScheduleType(IntEnum):
PEAK_SHAVING = 3,
BACKUP_MODE = 4,
SMART_CHARGE_MODE = 5,
ECO_MODE_745 = 6
ECO_MODE_745 = 6,
NOT_SET = 85

@classmethod
def detect_schedule_type(cls, value: int) -> ScheduleType:
"""Detect schedule type from its on/off value"""
if value in (0, -1, 85):
if value in (0, -1):
return ScheduleType.ECO_MODE
elif value in (1, -2):
return ScheduleType.DRY_CONTACT_LOAD
Expand All @@ -40,6 +41,8 @@ def detect_schedule_type(cls, value: int) -> ScheduleType:
return ScheduleType.SMART_CHARGE_MODE
elif value in (6, -7):
return ScheduleType.ECO_MODE_745
elif value == 85:
return ScheduleType.NOT_SET
else:
raise ValueError(f"{value}: on_off value {value} out of range.")

Expand All @@ -52,22 +55,21 @@ def power_unit(self):

def decode_power(self, value: int) -> int:
"""Decode human readable value of power parameter"""
if self == ScheduleType.ECO_MODE:
return value
elif self == ScheduleType.PEAK_SHAVING:
if self == ScheduleType.PEAK_SHAVING:
return value * 10
if self == ScheduleType.ECO_MODE_745:
elif self == ScheduleType.ECO_MODE_745:
return int(value / 10)
elif self == ScheduleType.NOT_SET:
if value < -100 or value > 100:
return int(value / 10)
else:
return value

def encode_power(self, value: int) -> int:
def encode_power(self, eco_mode, value: int) -> int:
"""Encode human readable value of power parameter"""
if self == ScheduleType.ECO_MODE:
return value
elif self == ScheduleType.PEAK_SHAVING:
if self == ScheduleType.PEAK_SHAVING:
return int(value / 10)
if self == ScheduleType.ECO_MODE_745:
elif eco_mode == ScheduleType.ECO_MODE_745:
return value * 10
else:
return value
Expand All @@ -76,7 +78,7 @@ def is_in_range(self, value: int) -> bool:
"""Check if the value fits in allowed values range"""
if self == ScheduleType.ECO_MODE:
return -100 <= value <= 100
if self == ScheduleType.ECO_MODE_745:
elif self == ScheduleType.ECO_MODE_745:
return -1000 <= value <= 1000
else:
return True
Expand Down Expand Up @@ -595,6 +597,7 @@ def read_value(self, data: ProtocolResponse):
self.power = read_bytes2(data) # negative=charge, positive=discharge
if not self.schedule_type.is_in_range(self.power):
raise ValueError(f"{self.id_}: power value {self.power} out of range.")
self.power = self.schedule_type.decode_power(self.power)
self.soc = read_bytes2(data)
if self.soc < 0 or self.soc > 100:
raise ValueError(f"{self.id_}: SoC value {self.soc} out of range.")
Expand All @@ -609,46 +612,46 @@ def encode_value(self, value: Any, register_value: bytes = None) -> bytes:
return value
raise ValueError

def encode_charge(self, eco_mode_power: int, eco_mode_soc: int = 100) -> bytes:
def encode_charge(self, eco_mode, eco_mode_power: int, eco_mode_soc: int = 100) -> bytes:
"""Answer bytes representing all the time enabled charging eco mode group"""
return bytes.fromhex(
"0000173b{:02x}7f{:04x}{:04x}{:04x}".format(
255 - self.schedule_type,
(-1 * abs(self.schedule_type.encode_power(eco_mode_power))) & (2 ** 16 - 1),
255 - eco_mode,
(-1 * abs(self.schedule_type.encode_power(eco_mode, eco_mode_power))) & (2 ** 16 - 1),
eco_mode_soc,
0 if self.schedule_type != ScheduleType.ECO_MODE_745 else 0x0fff))
0 if eco_mode != ScheduleType.ECO_MODE_745 else 0x0fff))

def encode_discharge(self, eco_mode_power: int) -> bytes:
def encode_discharge(self, eco_mode, eco_mode_power: int) -> bytes:
"""Answer bytes representing all the time enabled discharging eco mode group"""
return bytes.fromhex("0000173b{:02x}7f{:04x}0064{:04x}".format(
255 - self.schedule_type,
abs(self.schedule_type.encode_power(eco_mode_power)),
0 if self.schedule_type != ScheduleType.ECO_MODE_745 else 0x0fff))
255 - eco_mode,
abs(self.schedule_type.encode_power(eco_mode, eco_mode_power)),
0 if eco_mode != ScheduleType.ECO_MODE_745 else 0x0fff))

def encode_off(self) -> bytes:
"""Answer bytes representing empty and disabled schedule group"""
return bytes.fromhex("30003000{:02x}00{:04x}00640000".format(
self.schedule_type.value,
self.schedule_type.encode_power(100)))
self.schedule_type.encode_power(ScheduleType.NOT_SET, 100)))

def is_eco_charge_mode(self) -> bool:
def is_eco_charge_mode(self, eco_mode) -> bool:
"""Answer if it represents the emulated 24/7 fulltime discharge mode"""
return self.start_h == 0 \
and self.start_m == 0 \
and self.end_h == 23 \
and self.end_m == 59 \
and self.on_off == (-1 - self.schedule_type) \
and self.on_off == (-1 - eco_mode) \
and self.day_bits == 127 \
and self.power < 0 \
and (self.month_bits == 0 or self.month_bits == 0x0fff)

def is_eco_discharge_mode(self) -> bool:
def is_eco_discharge_mode(self, eco_mode) -> bool:
"""Answer if it represents the emulated 24/7 fulltime discharge mode"""
return self.start_h == 0 \
and self.start_m == 0 \
and self.end_h == 23 \
and self.end_m == 59 \
and self.on_off == (-1 - self.schedule_type) \
and self.on_off == (-1 - eco_mode) \
and self.day_bits == 127 \
and self.power > 0 \
and (self.month_bits == 0 or self.month_bits == 0x0fff)
Expand Down

0 comments on commit a6b1696

Please sign in to comment.