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>
139 lines
5.4 KiB
Python
139 lines
5.4 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
from typing import Type
|
|
|
|
from .dt import DT
|
|
from .es import ES
|
|
from .et import ET
|
|
from .exceptions import InverterError, RequestFailedException
|
|
from .goodwe import GoodWeXSProcessor, AbstractDataProcessor, GoodWeInverter
|
|
from .inverter import Inverter, OperationMode, Sensor, SensorKind
|
|
from .model import DT_MODEL_TAGS, ES_MODEL_TAGS, ET_MODEL_TAGS
|
|
from .protocol import ProtocolCommand, UdpInverterProtocol, Aa55ProtocolCommand
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Inverter family names
|
|
ET_FAMILY = ["ET", "EH", "BT", "BH"]
|
|
ES_FAMILY = ["ES", "EM", "BP"]
|
|
DT_FAMILY = ["DT", "MS", "NS", "XS"]
|
|
|
|
# Initial discovery command
|
|
DISCOVERY_COMMAND = Aa55ProtocolCommand("010200", "0182")
|
|
|
|
# supported inverter protocols
|
|
_SUPPORTED_PROTOCOLS = [ET, DT, ES]
|
|
|
|
|
|
async def connect(host: str, family: str = None, comm_addr: int = 0, timeout: int = 1, retries: int = 3,
|
|
do_discover: bool = True) -> Inverter:
|
|
"""Contact the inverter at the specified host/port and answer appropriate Inverter instance.
|
|
|
|
The specific inverter family/type will be detected automatically, but it can be passed explicitly.
|
|
Supported inverter family names are ET, EH, BT, BH, ES, EM, BP, DT, MS, D-NS and XS.
|
|
|
|
Inverter communication address may be explicitly passed, if not the usual default value
|
|
will be used (0xf7 for ET/EH/BT/BH/ES/EM/BP inverters, 0x7f for DT/MS/D-NS/XS inverters).
|
|
|
|
Since the UDP communication is by definition unreliable, when no (valid) response is received by the specified
|
|
timeout, it is considered lost and the command will be re-tried up to retries times.
|
|
|
|
Raise InverterError if unable to contact or recognise supported inverter.
|
|
"""
|
|
if family in ET_FAMILY:
|
|
inv = ET(host, comm_addr, timeout, retries)
|
|
elif family in ES_FAMILY:
|
|
inv = ES(host, comm_addr, timeout, retries)
|
|
elif family in DT_FAMILY:
|
|
inv = DT(host, comm_addr, timeout, retries)
|
|
elif do_discover:
|
|
return await discover(host, timeout, retries)
|
|
|
|
logger.debug("Connecting to %s family inverter at %s.", family, host)
|
|
await inv.read_device_info()
|
|
logger.debug("Connected to inverter %s, S/N:%s.", inv.model_name, inv.serial_number)
|
|
return inv
|
|
|
|
|
|
async def discover(host: str, timeout: int = 1, retries: int = 3) -> Inverter:
|
|
"""Contact the inverter at the specified value and answer appropriate Inverter instance
|
|
|
|
Raise InverterError if unable to contact or recognise supported inverter
|
|
"""
|
|
failures = []
|
|
|
|
# Try the common AA55C07F0102000241 command first and detect inverter type from serial_number
|
|
try:
|
|
logger.debug("Probing inverter at %s.", host)
|
|
response = await DISCOVERY_COMMAND.execute(host, timeout, retries)
|
|
model_name = response[12:22].decode("ascii").rstrip()
|
|
serial_number = response[38:54].decode("ascii")
|
|
|
|
inverter_class: Type[Inverter] | None = None
|
|
for model_tag in ET_MODEL_TAGS:
|
|
if model_tag in serial_number:
|
|
logger.debug("Detected ET/EH/BT/BH/GEH inverter %s, S/N:%s.", model_name, serial_number)
|
|
inverter_class = ET
|
|
for model_tag in ES_MODEL_TAGS:
|
|
if model_tag in serial_number:
|
|
logger.debug("Detected ES/EM/BP inverter %s, S/N:%s.", model_name, serial_number)
|
|
inverter_class = ES
|
|
for model_tag in DT_MODEL_TAGS:
|
|
if model_tag in serial_number:
|
|
logger.debug("Detected DT/MS/D-NS/XS/GEP inverter %s, S/N:%s.", model_name, serial_number)
|
|
inverter_class = DT
|
|
if inverter_class:
|
|
i = inverter_class(host, 0, timeout, retries)
|
|
await i.read_device_info()
|
|
return i
|
|
|
|
except InverterError as ex:
|
|
failures.append(ex)
|
|
|
|
# Probe inverter specific protocols
|
|
for inv in _SUPPORTED_PROTOCOLS:
|
|
i = inv(host, 0, timeout, retries)
|
|
try:
|
|
logger.debug("Probing %s inverter at %s.", inv.__name__, host)
|
|
await i.read_device_info()
|
|
await i.read_runtime_data()
|
|
logger.debug("Detected %s family inverter %s, S/N:%s.", inv.__name__, i.model_name, i.serial_number)
|
|
return i
|
|
except InverterError as ex:
|
|
failures.append(ex)
|
|
raise InverterError(
|
|
"Unable to connect to the inverter at "
|
|
f"host={host}, or your inverter is not supported yet.\n"
|
|
f"Failures={str(failures)}"
|
|
)
|
|
|
|
|
|
async def search_inverters() -> bytes:
|
|
"""Scan the network for inverters.
|
|
Answer the inverter discovery response string (which includes it IP address)
|
|
|
|
Raise InverterError if unable to contact any inverter
|
|
"""
|
|
logger.debug("Searching inverters by broadcast to port 48899")
|
|
loop = asyncio.get_running_loop()
|
|
command = ProtocolCommand("WIFIKIT-214028-READ".encode("utf-8"), lambda r: True)
|
|
response_future = loop.create_future()
|
|
transport, _ = await loop.create_datagram_endpoint(
|
|
lambda: UdpInverterProtocol(response_future, command, 1, 3),
|
|
remote_addr=("255.255.255.255", 48899),
|
|
allow_broadcast=True,
|
|
)
|
|
try:
|
|
await response_future
|
|
result = response_future.result()
|
|
if result is not None:
|
|
return result
|
|
else:
|
|
raise InverterError("No response received to broadcast request.")
|
|
except asyncio.CancelledError:
|
|
raise InverterError("No valid response received to broadcast request.") from None
|
|
finally:
|
|
transport.close()
|