diff --git a/goodwe/es.py b/goodwe/es.py index 3ace672..660c061 100644 --- a/goodwe/es.py +++ b/goodwe/es.py @@ -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: @@ -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 @@ -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) diff --git a/goodwe/et.py b/goodwe/et.py index 3dae067..9f22c36 100644 --- a/goodwe/et.py +++ b/goodwe/et.py @@ -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 * @@ -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 @@ -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 @@ -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)) @@ -601,6 +611,8 @@ 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: @@ -608,9 +620,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 @@ -618,24 +630,36 @@ async def get_operation_mode(self) -> OperationMode: 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() @@ -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') diff --git a/goodwe/inverter.py b/goodwe/inverter.py index 32b2e99..32348ef 100644 --- a/goodwe/inverter.py +++ b/goodwe/inverter.py @@ -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) """ @@ -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): diff --git a/goodwe/model.py b/goodwe/model.py index add1277..ebfb5a9 100644 --- a/goodwe/model.py +++ b/goodwe/model.py @@ -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"] @@ -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) @@ -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) diff --git a/goodwe/sensor.py b/goodwe/sensor.py index a704e1d..7855edd 100644 --- a/goodwe/sensor.py +++ b/goodwe/sensor.py @@ -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 @@ -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.") @@ -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 @@ -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 @@ -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.") @@ -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)