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

236 lines
11 KiB
Python

from __future__ import annotations
from typing import Tuple
from .exceptions import InverterError
from .inverter import Inverter
from .inverter import OperationMode
from .inverter import SensorKind as Kind
from .model import is_3_mptt, is_single_phase
from .protocol import ProtocolCommand, ModbusReadCommand, ModbusWriteCommand, ModbusWriteMultiCommand
from .sensor import *
class DT(Inverter):
"""Class representing inverter of DT/MS/D-NS/XS or GE's GEP(PSB/PSC) families"""
__all_sensors: Tuple[Sensor, ...] = (
Timestamp("timestamp", 0, "Timestamp"),
Voltage("vpv1", 6, "PV1 Voltage", Kind.PV),
Current("ipv1", 8, "PV1 Current", Kind.PV),
Calculated("ppv1",
lambda data: round(read_voltage(data, 6) * read_current(data, 8)),
"PV1 Power", "W", Kind.PV),
Voltage("vpv2", 10, "PV2 Voltage", Kind.PV),
Current("ipv2", 12, "PV2 Current", Kind.PV),
Calculated("ppv2",
lambda data: round(read_voltage(data, 10) * read_current(data, 12)),
"PV2 Power", "W", Kind.PV),
Voltage("vpv3", 14, "PV3 Voltage", Kind.PV),
Current("ipv3", 16, "PV3 Current", Kind.PV),
Calculated("ppv3",
lambda data: round(read_voltage(data, 14) * read_current(data, 16)),
"PV3 Power", "W", Kind.PV),
# Voltage("vpv4", 14, "PV4 Voltage", Kind.PV),
# Current("ipv4", 16, "PV4 Current", Kind.PV),
# Voltage("vpv5", 14, "PV5 Voltage", Kind.PV),
# Current("ipv5", 16, "PV5 Current", Kind.PV),
# Voltage("vpv6", 14, "PV6 Voltage", Kind.PV),
# Current("ipv6", 16, "PV7 Current", Kind.PV),
Voltage("vline1", 30, "On-grid L1-L2 Voltage", Kind.AC),
Voltage("vline2", 32, "On-grid L2-L3 Voltage", Kind.AC),
Voltage("vline3", 34, "On-grid L3-L1 Voltage", Kind.AC),
Voltage("vgrid1", 36, "On-grid L1 Voltage", Kind.AC),
Voltage("vgrid2", 38, "On-grid L2 Voltage", Kind.AC),
Voltage("vgrid3", 40, "On-grid L3 Voltage", Kind.AC),
Current("igrid1", 42, "On-grid L1 Current", Kind.AC),
Current("igrid2", 44, "On-grid L2 Current", Kind.AC),
Current("igrid3", 46, "On-grid L3 Current", Kind.AC),
Frequency("fgrid1", 48, "On-grid L1 Frequency", Kind.AC),
Frequency("fgrid2", 50, "On-grid L2 Frequency", Kind.AC),
Frequency("fgrid3", 52, "On-grid L3 Frequency", Kind.AC),
Calculated("pgrid1",
lambda data: round(read_voltage(data, 36) * read_current(data, 42)),
"On-grid L1 Power", "W", Kind.AC),
Calculated("pgrid2",
lambda data: round(read_voltage(data, 38) * read_current(data, 44)),
"On-grid L2 Power", "W", Kind.AC),
Calculated("pgrid3",
lambda data: round(read_voltage(data, 40) * read_current(data, 46)),
"On-grid L3 Power", "W", Kind.AC),
Integer("xx54", 54, "Unknown sensor@54"),
Power("ppv", 56, "PV Power", Kind.PV),
Integer("work_mode", 58, "Work Mode code"),
Enum2("work_mode_label", 58, WORK_MODES, "Work Mode"),
Long("error_codes", 60, "Error Codes"),
Integer("warning_code", 64, "Warning code"),
Integer("xx66", 66, "Unknown sensor@66"),
Integer("xx68", 68, "Unknown sensor@68"),
Integer("xx70", 70, "Unknown sensor@70"),
Integer("xx72", 72, "Unknown sensor@72"),
Integer("xx74", 74, "Unknown sensor@74"),
Integer("xx76", 76, "Unknown sensor@76"),
Integer("xx78", 78, "Unknown sensor@78"),
Integer("xx80", 80, "Unknown sensor@80"),
Temp("temperature", 82, "Inverter Temperature", Kind.AC),
Integer("xx84", 84, "Unknown sensor@84"),
Integer("xx86", 86, "Unknown sensor@86"),
Energy("e_day", 88, "Today's PV Generation", Kind.PV),
Energy4("e_total", 90, "Total PV Generation", Kind.PV),
Long("h_total", 94, "Hours Total", "h", Kind.PV),
Integer("safety_country", 98, "Safety Country code", "", Kind.AC),
Enum2("safety_country_label", 98, SAFETY_COUNTRIES, "Safety Country", Kind.AC),
Integer("xx100", 100, "Unknown sensor@100"),
Integer("xx102", 102, "Unknown sensor@102"),
Integer("xx104", 104, "Unknown sensor@104"),
Integer("xx106", 106, "Unknown sensor@106"),
Integer("xx108", 108, "Unknown sensor@108"),
Integer("xx110", 110, "Unknown sensor@110"),
Integer("xx112", 112, "Unknown sensor@112"),
Integer("xx114", 114, "Unknown sensor@114"),
Integer("xx116", 116, "Unknown sensor@116"),
Integer("xx118", 118, "Unknown sensor@118"),
Integer("xx120", 120, "Unknown sensor@120"),
Integer("xx122", 122, "Unknown sensor@122"),
Integer("funbit", 124, "FunBit", "", Kind.PV),
Voltage("vbus", 126, "Bus Voltage", Kind.PV),
Voltage("vnbus", 128, "NBus Voltage", Kind.PV),
Integer("xx130", 130, "Unknown sensor@130"),
Integer("xx132", 132, "Unknown sensor@132"),
Integer("xx134", 134, "Unknown sensor@134"),
Integer("xx136", 136, "Unknown sensor@136"),
Integer("xx138", 138, "Unknown sensor@138"),
Integer("xx140", 140, "Unknown sensor@140"),
Integer("xx142", 142, "Unknown sensor@142"),
Integer("xx144", 144, "Unknown sensor@144"),
)
# Modbus registers of inverter settings, offsets are modbus register addresses
__all_settings: Tuple[Sensor, ...] = (
Timestamp("time", 40313, "Inverter time"),
Integer("shadow_scan", 40326, "Shadow Scan", "", Kind.PV),
Integer("grid_export", 40327, "Grid Export Enabled", "", Kind.GRID),
Integer("grid_export_limit", 40328, "Grid Export Limit", "%", Kind.GRID),
)
# Settings for single phase inverters
__settings_single_phase: Tuple[Sensor, ...] = (
Long("grid_export_limit", 40328, "Grid Export Limit", "W", Kind.GRID),
)
# Settings for three phase inverters
__settings_three_phase: Tuple[Sensor, ...] = (
Integer("grid_export_limit", 40336, "Grid Export Limit", "%", Kind.GRID),
)
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 = 0x7f
self._READ_DEVICE_VERSION_INFO: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x7531, 0x0028)
self._READ_DEVICE_RUNNING_DATA: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x7594, 0x0049)
self._sensors = self.__all_sensors
self._settings: dict[str, Sensor] = {s.id_: s for s in self.__all_settings}
@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_ and not s.id_.startswith('xx'))
@staticmethod
def _pv1_pv2_only(s: Sensor) -> bool:
"""Filter to exclude sensors on < 3 PV inverters"""
return not s.id_.endswith('pv3')
async def read_device_info(self):
response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO)
response = response[5:-2]
try:
self.model_name = response[22:32].decode("ascii").rstrip()
except:
print("No model name sent from the inverter.")
self.serial_number = response[6:22].decode("ascii")
self.dsp1_version = read_unsigned_int(response, 66)
self.dsp2_version = read_unsigned_int(response, 68)
self.arm_version = read_unsigned_int(response, 70)
self.firmware = "{}.{}.{:02x}".format(self.dsp1_version, self.dsp2_version, self.arm_version)
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.__all_sensors))
self._settings.update({s.id_: s for s in self.__settings_single_phase})
else:
self._settings.update({s.id_: s for s in self.__settings_three_phase})
if is_3_mptt(self):
# this is 3 PV strings inverter, keep all sensors
pass
else:
# this is only 2 PV strings inverter
self._sensors = tuple(filter(self._pv1_pv2_only, self._sensors))
pass
async def read_runtime_data(self, include_unknown_sensors: bool = False) -> Dict[str, Any]:
raw_data = await self._read_from_socket(self._READ_DEVICE_RUNNING_DATA)
data = self._map_response(raw_data[5:-2], self._sensors, 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}"')
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():
value = await self.read_setting(setting.id_)
data[setting.id_] = value
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:
setting = self._settings.get('grid_export_limit')
if (setting.unit == "%" and 0 <= export_limit <= 100) or (setting.unit != "%" and 0 <= export_limit <= 10000):
return await self.write_setting('grid_export_limit', export_limit)
async def get_operation_modes(self, include_emulated: bool) -> Tuple[OperationMode, ...]:
return ()
async def get_operation_mode(self) -> OperationMode:
raise InverterError("Operation not supported.")
async def set_operation_mode(self, operation_mode: OperationMode, eco_mode_power: int = 100,
eco_mode_soc: int = 100) -> None:
raise InverterError("Operation not supported.")
async def get_ongrid_battery_dod(self) -> int:
raise InverterError("Operation not supported, inverter has no batteries.")
async def set_ongrid_battery_dod(self, dod: int) -> None:
raise InverterError("Operation not supported, inverter has no batteries.")
def sensors(self) -> Tuple[Sensor, ...]:
return self._sensors
def settings(self) -> Tuple[Sensor, ...]:
return tuple(self._settings.values())