Files
adminandClaude Opus 5 79843aa2ae SolarManager unter Versionsverwaltung
Erster Stand der Hintergrundprozesse, die auf der Synology unter
/volume1/homes/wagner/SolarManager laufen: der Manager selbst, die Sammler
je Geraet, die MQTT-Bruecke, der Wecker und - neu hinzugezogen - der
AutoAction-Runner, der als Hintergrundprozess hierher gehoert und nicht ins
Web-Verzeichnis.

Zugangsdaten stehen nicht mehr im Quelltext, sondern in config.ini, die
nicht mit eingecheckt wird. Vorlage ist config.ini.example, gelesen wird sie
von konfig.py. Betroffen waren solarManager.py (Datenbank und Wattpilot),
zeit.py, gatherWaterData.py, wecker.py und skoda_testdaten.py, das sich das
Passwort bisher aus dem Quelltext eines anderen Moduls herausgesucht hat.

Die Kia-Anbindung ist mit dem Fahrzeug entfallen: kiaTest.py,
gatherCarData.py und hyundai_kia_connect_api sind nicht mehr dabei, ebenso
gatherInverterData.py, auf das nur noch eine auskommentierte Zeile zeigte.

Die mitgelieferten Bibliotheken bleiben im Repository - die NAS hat kein
pip, sie muessen neben den Skripten liegen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 20:46:59 +02:00

509 lines
27 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import logging
from typing import Tuple, cast
from .inverter import Inverter
from .inverter import OperationMode
from .inverter import SensorKind as Kind
from .model import is_4_mptt, is_single_phase
from .protocol import ProtocolCommand, ModbusReadCommand, ModbusWriteCommand, ModbusWriteMultiCommand
from .sensor import *
logger = logging.getLogger(__name__)
class ET(Inverter):
"""Class representing inverter of ET/EH/BT/BH or GE's GEH families"""
# Modbus registers from offset 0x891c (35100), count 0x7d (125)
__all_sensors: Tuple[Sensor, ...] = (
Timestamp("timestamp", 0, "Timestamp"),
Voltage("vpv1", 6, "PV1 Voltage", Kind.PV),
Current("ipv1", 8, "PV1 Current", Kind.PV),
Power4("ppv1", 10, "PV1 Power", Kind.PV),
Voltage("vpv2", 14, "PV2 Voltage", Kind.PV),
Current("ipv2", 16, "PV2 Current", Kind.PV),
Power4("ppv2", 18, "PV2 Power", Kind.PV),
Voltage("vpv3", 22, "PV3 Voltage", Kind.PV), # modbus35111
Current("ipv3", 24, "PV3 Current", Kind.PV),
Power4("ppv3", 26, "PV3 Power", Kind.PV),
Voltage("vpv4", 30, "PV4 Voltage", Kind.PV),
Current("ipv4", 32, "PV4 Current", Kind.PV),
Power4("ppv4", 34, "PV4 Power", Kind.PV),
# ppv1 + ppv2 + ppv3 + ppv4
Calculated("ppv",
lambda data:
read_bytes4(data, 10) +
read_bytes4(data, 18) +
read_bytes4(data, 26) +
read_bytes4(data, 34),
"PV Power", "W", Kind.PV),
Byte("pv4_mode", 38, "PV4 Mode code", "", Kind.PV),
Enum("pv4_mode_label", 38, PV_MODES, "PV4 Mode", Kind.PV),
Byte("pv3_mode", 39, "PV3 Mode code", "", Kind.PV),
Enum("pv3_mode_label", 39, PV_MODES, "PV3 Mode", Kind.PV),
Byte("pv2_mode", 40, "PV2 Mode code", "", Kind.PV),
Enum("pv2_mode_label", 40, PV_MODES, "PV2 Mode", Kind.PV),
Byte("pv1_mode", 41, "PV1 Mode code", "", Kind.PV),
Enum("pv1_mode_label", 41, PV_MODES, "PV1 Mode", Kind.PV),
Voltage("vgrid", 42, "On-grid L1 Voltage", Kind.AC), # modbus 35121
Current("igrid", 44, "On-grid L1 Current", Kind.AC),
Frequency("fgrid", 46, "On-grid L1 Frequency", Kind.AC),
# 48 reserved
Power("pgrid", 50, "On-grid L1 Power", Kind.AC),
Voltage("vgrid2", 52, "On-grid L2 Voltage", Kind.AC),
Current("igrid2", 54, "On-grid L2 Current", Kind.AC),
Frequency("fgrid2", 56, "On-grid L2 Frequency", Kind.AC),
# 58 reserved
Power("pgrid2", 60, "On-grid L2 Power", Kind.AC),
Voltage("vgrid3", 62, "On-grid L3 Voltage", Kind.AC),
Current("igrid3", 64, "On-grid L3 Current", Kind.AC),
Frequency("fgrid3", 66, "On-grid L3 Frequency", Kind.AC),
# 68 reserved
Power("pgrid3", 70, "On-grid L3 Power", Kind.AC),
Integer("grid_mode", 72, "Grid Mode code", "", Kind.PV),
Enum2("grid_mode_label", 72, GRID_MODES, "Grid Mode", Kind.PV),
# 74 reserved
Power("total_inverter_power", 76, "Total Power", Kind.AC),
# 78 reserved
Power("active_power", 80, "Active Power", Kind.GRID),
Calculated("grid_in_out",
lambda data: read_grid_mode(data, 80),
"On-grid Mode code", "", Kind.GRID),
EnumCalculated("grid_in_out_label",
lambda data: read_grid_mode(data, 80), GRID_IN_OUT_MODES,
"On-grid Mode", Kind.GRID),
# 82 reserved
Reactive("reactive_power", 84, "Reactive Power", Kind.GRID),
# 86 reserved
Apparent("apparent_power", 88, "Apparent Power", Kind.GRID),
Voltage("backup_v1", 90, "Back-up L1 Voltage", Kind.UPS), # modbus 35145
Current("backup_i1", 92, "Back-up L1 Current", Kind.UPS),
Frequency("backup_f1", 94, "Back-up L1 Frequency", Kind.UPS),
Integer("load_mode1", 96, "Load Mode L1"),
# 98 reserved
Power("backup_p1", 100, "Back-up L1 Power", Kind.UPS),
Voltage("backup_v2", 102, "Back-up L2 Voltage", Kind.UPS),
Current("backup_i2", 104, "Back-up L2 Current", Kind.UPS),
Frequency("backup_f2", 106, "Back-up L2 Frequency", Kind.UPS),
Integer("load_mode2", 108, "Load Mode L2"),
# 110 reserved
Power("backup_p2", 112, "Back-up L2 Power", Kind.UPS),
Voltage("backup_v3", 114, "Back-up L3 Voltage", Kind.UPS),
Current("backup_i3", 116, "Back-up L3 Current", Kind.UPS),
Frequency("backup_f3", 118, "Back-up L3 Frequency", Kind.UPS),
Integer("load_mode3", 120, "Load Mode L3"),
# 122 reserved
Power("backup_p3", 124, "Back-up L3 Power", Kind.UPS),
# 126 reserved
Power("load_p1", 128, "Load L1", Kind.AC),
# 130 reserved
Power("load_p2", 132, "Load L2", Kind.AC),
# 134 reserved
Power("load_p3", 136, "Load L3", Kind.AC),
# 138 reserved
Power("backup_ptotal", 140, "Back-up Load", Kind.UPS),
# 142 reserved
Power("load_ptotal", 144, "Load", Kind.AC),
Integer("ups_load", 146, "Ups Load", "%", Kind.UPS),
Temp("temperature_air", 148, "Inverter Temperature (Air)", Kind.AC),
Temp("temperature_module", 150, "Inverter Temperature (Module)"),
Temp("temperature", 152, "Inverter Temperature (Radiator)", Kind.AC),
Integer("function_bit", 154, "Function Bit"),
Voltage("bus_voltage", 156, "Bus Voltage", None),
Voltage("nbus_voltage", 158, "NBus Voltage", None),
Voltage("vbattery1", 160, "Battery Voltage", Kind.BAT), # modbus 35180
Current("ibattery1", 162, "Battery Current", Kind.BAT),
# round(vbattery1 * ibattery1),
Calculated("pbattery1",
lambda data: round(read_voltage(data, 160) * read_current(data, 162)),
"Battery Power", "W", Kind.BAT),
Integer("battery_mode", 168, "Battery Mode code", "", Kind.BAT),
Enum2("battery_mode_label", 168, BATTERY_MODES, "Battery Mode", Kind.BAT),
Integer("warning_code", 170, "Warning code"),
Integer("safety_country", 172, "Safety Country code", "", Kind.AC),
Enum2("safety_country_label", 172, SAFETY_COUNTRIES, "Safety Country", Kind.AC),
Integer("work_mode", 174, "Work Mode code"),
Enum2("work_mode_label", 174, WORK_MODES_ET, "Work Mode"),
Integer("operation_mode", 176, "Operation Mode code"),
Long("error_codes", 178, "Error Codes"),
EnumBitmap4("errors", 178, ERROR_CODES, "Errors"),
Energy4("e_total", 182, "Total PV Generation", Kind.PV),
Energy4("e_day", 186, "Today's PV Generation", Kind.PV),
Energy4("e_total_exp", 190, "Total Energy (export)", Kind.AC),
Long("h_total", 194, "Hours Total", "h", Kind.PV),
Energy("e_day_exp", 198, "Today Energy (export)", Kind.AC),
Energy4("e_total_imp", 200, "Total Energy (import)", Kind.AC),
Energy("e_day_imp", 204, "Today Energy (import)", Kind.AC),
Energy4("e_load_total", 206, "Total Load", Kind.AC),
Energy("e_load_day", 210, "Today Load", Kind.AC),
Energy4("e_bat_charge_total", 212, "Total Battery Charge", Kind.BAT),
Energy("e_bat_charge_day", 216, "Today Battery Charge", Kind.BAT),
Energy4("e_bat_discharge_total", 218, "Total Battery Discharge", Kind.BAT),
Energy("e_bat_discharge_day", 222, "Today Battery Discharge", Kind.BAT),
Long("diagnose_result", 240, "Diag Status Code"),
EnumBitmap4("diagnose_result_label", 240, DIAG_STATUS_CODES, "Diag Status"),
# ppv1 + ppv2 + pbattery - active_power
Calculated("house_consumption",
lambda data:
read_bytes4(data, 10) +
read_bytes4(data, 18) +
read_bytes4(data, 26) +
read_bytes4(data, 34) +
round(read_voltage(data, 160) * read_current(data, 162)) -
read_bytes2(data, 80),
"House Consumption", "W", Kind.AC),
)
# Modbus registers from offset 0x9088 (37000)
__all_sensors_battery: Tuple[Sensor, ...] = (
Integer("battery_bms", 0, "Battery BMS", "", Kind.BAT),
Integer("battery_index", 2, "Battery Index", "", Kind.BAT),
Integer("battery_status", 4, "Battery Status", "", Kind.BAT),
Temp("battery_temperature", 6, "Battery Temperature", Kind.BAT),
Integer("battery_charge_limit", 8, "Battery Charge Limit", "A", Kind.BAT),
Integer("battery_discharge_limit", 10, "Battery Discharge Limit", "A", Kind.BAT),
Integer("battery_error_l", 12, "Battery Error L", "", Kind.BAT),
Integer("battery_soc", 14, "Battery State of Charge", "%", Kind.BAT),
Integer("battery_soh", 16, "Battery State of Health", "%", Kind.BAT),
Integer("battery_modules", 18, "Battery Modules", "", Kind.BAT), # modbus 37009
Integer("battery_warning_l", 20, "Battery Warning L", "", Kind.BAT),
Integer("battery_protocol", 22, "Battery Protocol", "", Kind.BAT),
Integer("battery_error_h", 24, "Battery Error H", "", Kind.BAT),
EnumBitmap22("battery_error", 24, 12, BMS_ALARM_CODES, "Battery Error", Kind.BAT),
Integer("battery_warning_h", 28, "Battery Warning H", "", Kind.BAT),
EnumBitmap22("battery_warning", 28, 20, BMS_WARNING_CODES, "Battery Warning", Kind.BAT),
Integer("battery_sw_version", 30, "Battery Software Version", "", Kind.BAT),
Integer("battery_hw_version", 32, "Battery Hardware Version", "", Kind.BAT),
Integer("battery_max_cell_temp_id", 34, "Battery Max Cell Temperature ID", "", Kind.BAT),
Integer("battery_min_cell_temp_id", 36, "Battery Min Cell Temperature ID", "", Kind.BAT),
Integer("battery_max_cell_voltage_id", 38, "Battery Max Cell Voltage ID", "", Kind.BAT),
Integer("battery_min_cell_voltage_id", 40, "Battery Min Cell Voltage ID", "", Kind.BAT),
Temp("battery_max_cell_temp", 42, "Battery Max Cell Temperature", Kind.BAT),
Temp("battery_min_cell_temp", 44, "Battery Min Cell Temperature", Kind.BAT),
Voltage("battery_max_cell_voltage", 46, "Battery Max Cell Voltage", Kind.BAT),
Voltage("battery_min_cell_voltage", 48, "Battery Min Cell Voltage", Kind.BAT),
)
# Inverter's meter data
# Modbus registers from offset 0x8ca0 (36000)
__all_sensors_meter: Tuple[Sensor, ...] = (
Integer("commode", 0, "Commode"),
Integer("rssi", 2, "RSSI"),
Integer("manufacture_code", 4, "Manufacture Code"),
Integer("meter_test_status", 6, "Meter Test Status"), # 1: correct2: reverse3: incorrect0: not checked
Integer("meter_comm_status", 8, "Meter Communication Status"), # 1 OK, 0 NotOK
Power("active_power1", 10, "Active Power L1", Kind.GRID), # modbus 36005
Power("active_power2", 12, "Active Power L2", Kind.GRID),
Power("active_power3", 14, "Active Power L3", Kind.GRID),
Power("active_power_total", 16, "Active Power Total", Kind.GRID),
Reactive("reactive_power_total", 18, "Reactive Power Total", Kind.GRID),
Decimal("meter_power_factor1", 20, 1000, "Meter Power Factor L1", "", Kind.GRID),
Decimal("meter_power_factor2", 22, 1000, "Meter Power Factor L2", "", Kind.GRID),
Decimal("meter_power_factor3", 24, 1000, "Meter Power Factor L3", "", Kind.GRID),
Decimal("meter_power_factor", 26, 1000, "Meter Power Factor", "", Kind.GRID),
Frequency("meter_freq", 28, "Meter Frequency", Kind.GRID), # modbus 36014
Float("meter_e_total_exp", 30, 1000, "Meter Total Energy (export)", "kWh", Kind.GRID),
Float("meter_e_total_imp", 34, 1000, "Meter Total Energy (import)", "kWh", Kind.GRID),
Power4("meter_active_power1", 38, "Meter Active Power L1", Kind.GRID),
Power4("meter_active_power2", 42, "Meter Active Power L2", Kind.GRID),
Power4("meter_active_power3", 46, "Meter Active Power L3", Kind.GRID),
Power4("meter_active_power_total", 50, "Meter Active Power Total", Kind.GRID),
Reactive4("meter_reactive_power1", 54, "Meter Reactive Power L1", Kind.GRID),
Reactive4("meter_reactive_power2", 58, "Meter Reactive Power L2", Kind.GRID),
Reactive4("meter_reactive_power3", 62, "Meter Reactive Power L2", Kind.GRID),
Reactive4("meter_reactive_power_total", 66, "Meter Reactive Power Total", Kind.GRID),
Apparent4("meter_apparent_power1", 70, "Meter Apparent Power L1", Kind.GRID),
Apparent4("meter_apparent_power2", 74, "Meter Apparent Power L2", Kind.GRID),
Apparent4("meter_apparent_power3", 78, "Meter Apparent Power L3", Kind.GRID),
Apparent4("meter_apparent_power_total", 82, "Meter Apparent Power Total", Kind.GRID),
Integer("meter_type", 86, "Meter Type", "", Kind.GRID),
Integer("meter_sw_version", 88, "Meter Software Version", "", Kind.GRID),
)
# Modbus registers of inverter settings, offsets are modbus register addresses
__all_settings: Tuple[Sensor, ...] = (
Integer("comm_address", 45127, "Communication Address", ""),
Timestamp("time", 45200, "Inverter time"),
Integer("sensitivity_check", 45246, "Sensitivity Check Mode", "", Kind.AC),
Integer("cold_start", 45248, "Cold Start", "", Kind.AC),
Integer("shadow_scan", 45251, "Shadow Scan", "", Kind.PV),
Integer("backup_supply", 45252, "Backup Supply", "", Kind.UPS),
Integer("unbalanced_output", 45264, "Unbalanced Output", "", Kind.AC),
Integer("pen_relay", 45288, "PE-N Relay", "", Kind.AC),
Integer("battery_capacity", 45350, "Battery Capacity", "Ah", Kind.BAT),
Integer("battery_modules", 45351, "Battery Modules", "", Kind.BAT),
Voltage("battery_charge_voltage", 45352, "Battery Charge Voltage", Kind.BAT),
Current("battery_charge_current", 45353, "Battery Charge Current", Kind.BAT),
Voltage("battery_discharge_voltage", 45354, "Battery Discharge Voltage", Kind.BAT),
Current("battery_discharge_current", 45355, "Battery Discharge Current", Kind.BAT),
Integer("battery_discharge_depth", 45356, "Battery Discharge Depth", "%", Kind.BAT),
Voltage("battery_discharge_voltage_offline", 45357, "Battery Discharge Voltage (off-line)", Kind.BAT),
Integer("battery_discharge_depth_offline", 45358, "Battery Discharge Depth (off-line)", "%", Kind.BAT),
Decimal("power_factor", 45482, 100, "Power Factor"),
Integer("work_mode", 47000, "Work Mode", "", Kind.AC),
Integer("dred", 47010, "DRED/Remote Shutdown", "", Kind.AC),
Integer("battery_soc_protection", 47500, "Battery SoC Protection", "", Kind.BAT),
Integer("grid_export", 47509, "Grid Export Enabled", "", Kind.GRID),
Integer("grid_export_limit", 47510, "Grid Export Limit", "W", Kind.GRID),
Integer("battery_protocol_code", 47514, "Battery Protocol Code", "", Kind.BAT),
EcoModeV1("eco_mode_1", 47515, "Eco Mode Group 1"),
ByteH("eco_mode_1_switch", 47518, "Eco Mode Group 1 Switch"),
EcoModeV1("eco_mode_2", 47519, "Eco Mode Group 2"),
ByteH("eco_mode_2_switch", 47522, "Eco Mode Group 2 Switch"),
EcoModeV1("eco_mode_3", 47523, "Eco Mode Group 3"),
ByteH("eco_mode_3_switch", 47526, "Eco Mode Group 3 Switch"),
EcoModeV1("eco_mode_4", 47527, "Eco Mode Group 4"),
ByteH("eco_mode_4_switch", 47530, "Eco Mode Group 4 Switch"),
)
# Settings added in ARM firmware 19
__settings_arm_fw_19: Tuple[Sensor, ...] = (
Integer("fast_charging", 47545, "Fast Charging Enabled", "", Kind.BAT),
Integer("fast_charging_soc", 47546, "Fast Charging SoC", "%", Kind.BAT),
EcoModeV2("eco_mode_1", 47547, "Eco Mode Group 1"),
ByteH("eco_mode_1_switch", 47549, "Eco Mode Group 1 Switch"),
EcoModeV2("eco_mode_2", 47553, "Eco Mode Group 2"),
ByteH("eco_mode_2_switch", 47555, "Eco Mode Group 2 Switch"),
EcoModeV2("eco_mode_3", 47559, "Eco Mode Group 3"),
ByteH("eco_mode_3_switch", 47561, "Eco Mode Group 3 Switch"),
EcoModeV2("eco_mode_4", 47565, "Eco Mode Group 4"),
ByteH("eco_mode_4_switch", 47567, "Eco Mode Group 4 Switch"),
Integer("load_control_mode", 47595, "Load Control Mode", "", Kind.AC),
Integer("load_control_switch", 47596, "Load Control Switch", "", Kind.AC),
Integer("load_control_soc", 47596, "Load Control SoC", "", Kind.AC),
Integer("fast_charging_power", 47603, "Fast Charging Power", "%", Kind.BAT),
)
# Settings added in ARM firmware 22
__settings_arm_fw_22: Tuple[Sensor, ...] = (
# EcoModeV2("eco_modeV2_5", 47571, "Eco Mode Version 2 Power Group 5"),
# EcoModeV2("eco_modeV2_6", 47577, "Eco Mode Version 2 Power Group 6"),
# EcoModeV2("eco_modeV2_7", 47583, "Eco Mode Version 2 Power Group 7"),
PeakShavingMode("peak_shaving_mode", 47589, "Peak Shaving Mode"),
Integer("dod_holding", 47602, "DoD Holding", "", Kind.BAT),
)
def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
super().__init__(host, comm_addr, timeout, retries)
if not self.comm_addr:
# Set the default inverter address
self.comm_addr = 0xf7
self._READ_DEVICE_VERSION_INFO: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x88b8, 0x0021)
self._READ_RUNNING_DATA: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x891c, 0x007d)
self._READ_METER_DATA: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x8ca0, 0x2d)
self._READ_BATTERY_INFO: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x9088, 0x0018)
self._has_battery: bool = True
# By default, we set up only PV1 on PV2 sensors, only few inverters support PV3 and PV4
# In case they are needed, they are added later in read_device_info
self._sensors = tuple(filter(self._pv1_pv2_only, self.__all_sensors))
self._sensors_battery = self.__all_sensors_battery
self._sensors_meter = self.__all_sensors_meter
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
def _supports_peak_shaving(self) -> bool:
return self.arm_version >= 22
@staticmethod
def _single_phase_only(s: Sensor) -> bool:
"""Filter to exclude phase2/3 sensors on single phase inverters"""
return not ((s.id_.endswith('2') or s.id_.endswith('3')) and 'pv' not in s.id_)
@staticmethod
def _pv1_pv2_only(s: Sensor) -> bool:
"""Filter to exclude sensors on < 4 PV inverters"""
return not (('pv3' in s.id_) or ('pv4' in s.id_))
async def read_device_info(self):
response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO)
response = response[5:-2]
# Modbus registers from offset (35000)
self.modbus_version = read_unsigned_int(response, 0)
self.rated_power = read_unsigned_int(response, 2)
self.ac_output_type = read_unsigned_int(response, 4) # 0: 1-phase, 1: 3-phase (4 wire), 2: 3-phase (3 wire)
self.serial_number = response[6:22].decode("ascii")
self.model_name = response[22:32].decode("ascii").rstrip()
self.dsp1_version = read_unsigned_int(response, 32)
self.dsp2_version = read_unsigned_int(response, 34)
self.dsp_svn_version = read_unsigned_int(response, 36)
self.arm_version = read_unsigned_int(response, 38)
self.arm_svn_version = read_unsigned_int(response, 40)
self.firmware = self._decode(response[42:54])
self.arm_firmware = self._decode(response[54:66])
if is_4_mptt(self):
# this is PV3/PV4 re-include all sensors
self._sensors = tuple(self.__all_sensors)
self._sensors_meter = tuple(self._sensors_meter)
if is_single_phase(self):
# this is single phase inverter, filter out all L2 and L3 sensors
self._sensors = tuple(filter(self._single_phase_only, self._sensors))
self._sensors_meter = tuple(filter(self._single_phase_only, self._sensors_meter))
if self.arm_version >= 19:
self._settings.update({s.id_: s for s in self.__settings_arm_fw_19})
if self.arm_version >= 22:
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)
self._has_battery = data.get('battery_mode', 0) != 0
if self._has_battery:
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))
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))
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:
return setting.read_value(buffer)
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}"')
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])
else:
raw_value = setting.encode_value(value)
if len(raw_value) <= 2:
value = int.from_bytes(raw_value, byteorder="big", signed=True)
await self._read_from_socket(ModbusWriteCommand(self.comm_addr, setting.offset, value))
else:
await self._read_from_socket(ModbusWriteMultiCommand(self.comm_addr, setting.offset, raw_value))
async def read_settings_data(self) -> Dict[str, Any]:
data = {}
for setting in self.settings():
try:
value = await self.read_setting(setting.id_)
data[setting.id_] = value
except ValueError:
logger.exception("Error reading setting %s.", setting.id_)
data[setting.id_] = None
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.write_setting('grid_export_limit', export_limit)
async def get_operation_modes(self, include_emulated: bool) -> Tuple[OperationMode, ...]:
result = [e for e in OperationMode]
if not self._supports_peak_shaving():
result.remove(OperationMode.PEAK_SHAVING)
if not include_emulated:
result.remove(OperationMode.ECO_CHARGE)
result.remove(OperationMode.ECO_DISCHARGE)
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():
return OperationMode.ECO_CHARGE
elif ecomode.is_eco_discharge_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._set_offline(False)
await self._clear_battery_mode_param()
elif operation_mode == OperationMode.OFF_GRID:
await self.write_setting('work_mode', 1)
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._set_offline(False)
await self._clear_battery_mode_param()
elif operation_mode == OperationMode.ECO:
await self.write_setting('work_mode', 3)
await self._set_offline(False)
elif operation_mode == OperationMode.PEAK_SHAVING:
await self.write_setting('work_mode', 4)
await self._set_offline(False)
elif operation_mode in (OperationMode.ECO_CHARGE, OperationMode.ECO_DISCHARGE):
if eco_mode_power < 0 or eco_mode_power > 100:
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.write_setting('work_mode', 3)
await self._set_offline(False)
async def get_ongrid_battery_dod(self) -> int:
return 100 - await self.read_setting('battery_discharge_depth')
async def set_ongrid_battery_dod(self, dod: int) -> None:
if 0 <= dod <= 90:
await self.write_setting('battery_discharge_depth', 100 - dod)
def sensors(self) -> Tuple[Sensor, ...]:
if self._has_battery:
return self._sensors + self._sensors_battery + self._sensors_meter
else:
return self._sensors + self._sensors_meter
def settings(self) -> Tuple[Sensor, ...]:
return tuple(self._settings.values())
async def _clear_battery_mode_param(self) -> None:
await self._read_from_socket(ModbusWriteCommand(self.comm_addr, 0xb9ad, 1))
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