diff --git a/custom_components/aurora_solar/sensor.py b/custom_components/aurora_solar/sensor.py index 1780f8c..a9dea10 100644 --- a/custom_components/aurora_solar/sensor.py +++ b/custom_components/aurora_solar/sensor.py @@ -1,56 +1,87 @@ """Support for ABB Aurora Solar Inverters via Waveshare RS485-to-Ethernet adapter.""" from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_HOST, CONF_PORT -import socket import logging +from aurorapy.client import AuroraTCPClient, AuroraError from .const import DOMAIN, CONF_SLAVE_ID _LOGGER = logging.getLogger(__name__) -# Vollständige ABB Aurora Befehlsliste (inkl. Diagnose, Seriennummern, Events) -COMMANDS = { - # Standard-Sensoren - "DSP_GRID_POWER": b"\x30\x33\x0D", # Netzleistung (W) - "DSP_DAILY_ENERGY": b"\x31\x33\x0D", # Tagesenergie (Wh) - "DSP_TOTAL_ENERGY": b"\x31\x34\x0D", # Gesamtenergie (kWh) - "DSP_GRID_VOLTAGE": b"\x32\x33\x0D", # Netzspannung (V) - "DSP_GRID_CURRENT": b"\x33\x33\x0D", # Netzstrom (A) - "DSP_GRID_FREQUENCY": b"\x34\x33\x0D", # Netzfrequenz (Hz) - "DSP_TEMPERATURE": b"\x35\x33\x0D", # Temperatur (°C) - "DSP_DC_VOLTAGE": b"\x36\x33\x0D", # Gleichspannung (V) - "DSP_DC_CURRENT": b"\x37\x33\x0D", # Gleichstrom (A) - "DSP_DC_POWER": b"\x38\x33\x0D", # Gleichleistung (W) - "DSP_EFFICIENCY": b"\x39\x33\x0D", # Wirkungsgrad (%) - "DSP_PF": b"\x3A\x33\x0D", # Leistungsfaktor - "DSP_AC_VOLTAGE_PHASE": b"\x3B\x33\x0D", # Phasenspannung (V) - "DSP_DC_VOLTAGE2": b"\x3C\x33\x0D", # Gleichspannung 2 (falls vorhanden) - "DSP_DC_CURRENT2": b"\x3D\x33\x0D", # Gleichstrom 2 (falls vorhanden) - "DSP_RADIATOR_TEMP": b"\x3E\x33\x0D", # Kühlkörpertemperatur (°C) +class AuroraSensorBase(SensorEntity): + """Basis-Klasse für alle ABB Aurora Sensoren.""" - # Diagnose und Status - "DSP_ALARMS": b"\x50\x33\x0D", # Alarme (Bitmaske) - "DSP_STATUS": b"\x51\x33\x0D", # Betriebsstatus - "DSP_EVENTS": b"\x52\x33\x0D", # Ereignisse (z. B. Start/Stop) - "DSP_FAULT_CODE": b"\x53\x33\x0D", # Fehlercode - "DSP_SERIAL_NUMBER": b"\x54\x33\x0D", # Seriennummer (String) - "DSP_VERSION": b"\x55\x33\x0D", # Software-Version - "DSP_MODEL": b"\x56\x33\x0D", # Modellbezeichnung - "DSP_DC_INPUT_VOLTAGE": b"\x57\x33\x0D", # PV-Eingangsspannung (V) - "DSP_MPPT_POWER": b"\x58\x33\x0D", # MPPT-Leistung (W) - "DSP_ISOLATION": b"\x59\x33\x0D", # Isolationswiderstand (kΩ) - "DSP_AMBIENT_TEMP": b"\x5A\x33\x0D", # Umgebungs-Temperatur (°C) + def __init__(self, host, port, slave_id, name, sensor_type, unit, factor=1, is_string=False, text_mapping=None): + """Initialisiere den Sensor.""" + self._host = host + self._port = port + self._slave_id = slave_id + self._name = name + self._sensor_type = sensor_type + self._unit = unit + self._factor = factor + self._is_string = is_string + self._text_mapping = text_mapping + self._state = None + self._attr_native_unit_of_measurement = unit if not is_string else None + self._attr_unique_id = f"aurora_{slave_id}_{sensor_type.lower()}" - # Erweitere Diagnose (falls unterstützt) - "DSP_DC_POWER2": b"\x5B\x33\x0D", # MPPT2-Leistung (W) - "DSP_DC_VOLTAGE3": b"\x5C\x33\x0D", # MPPT3-Spannung (V) - "DSP_LAST_ERROR": b"\x5D\x33\x0D", # Letzter Fehler (Code) - "DSP_OPERATING_HOURS": b"\x5E\x33\x0D", # Betriebsstunden (h) - "DSP_GRID_POWER_LIMIT": b"\x5F\x33\x0D", # Netzleistungsbegrenzung (%) + @property + def name(self): + """Return the name of the sensor.""" + return self._name - # Spezifische Events (z. B. Relaiszustände) - "DSP_RELAY_STATUS": b"\x60\x33\x0D", # Relais-Status -} + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Fetch new state data for the sensor.""" + try: + client = AuroraTCPClient(ip=self._host, port=self._port, address=self._slave_id, timeout=10) + client.connect() + + if self._sensor_type == "DSP_GRID_POWER": + self._state = client.measure(3) * self._factor + elif self._sensor_type == "DSP_DAILY_ENERGY": + self._state = client.cumulated_energy(0) + elif self._sensor_type == "DSP_TOTAL_ENERGY": + self._state = client.cumulated_energy(1) * 0.1 + elif self._sensor_type == "DSP_GRID_VOLTAGE": + self._state = client.measure(6) + elif self._sensor_type == "DSP_GRID_CURRENT": + self._state = client.measure(7) + elif self._sensor_type == "DSP_GRID_FREQUENCY": + self._state = client.measure(8) + elif self._sensor_type == "DSP_PF": + self._state = client.measure(9) * 0.01 + elif self._sensor_type == "DSP_DC_VOLTAGE": + self._state = client.measure(10) + elif self._sensor_type == "DSP_DC_CURRENT": + self._state = client.measure(11) + elif self._sensor_type == "DSP_DC_POWER": + self._state = client.measure(12) + elif self._sensor_type == "DSP_TEMPERATURE": + self._state = client.measure(13) + elif self._sensor_type == "DSP_SERIAL_NUMBER": + self._state = client.serial_number() + elif self._sensor_type == "DSP_VERSION": + self._state = client.version() + elif self._sensor_type == "DSP_ALARMS": + alarms = client.alarms() + self._state = self._text_mapping.get(alarms, f"Unbekannt (0x{alarms:04X})") + elif self._sensor_type == "DSP_STATUS": + status = client.status() + self._state = self._text_mapping.get(status, f"Unbekannt (0x{status:04X})") + elif self._sensor_type == "DSP_FAULT_CODE": + fault = client.fault_code() + self._state = self._text_mapping.get(fault, f"Unbekannt (0x{fault:04X})") + + client.close() + except AuroraError as e: + self._state = None + _LOGGER.error("Fehler bei %s: %s", self._name, e) # Mapping für lesbare Texte ALARM_MESSAGES = { @@ -76,112 +107,39 @@ FAULT_MESSAGES = { 0x0002: "Kommunikationsfehler", } -class AuroraSensorBase(SensorEntity): - """Basis-Klasse für alle ABB Aurora Sensoren.""" - - def __init__(self, host, port, slave_id, name, command_key, unit, data_index=0, factor=1, is_string=False, text_mapping=None): - """Initialisiere den Sensor.""" - self._host = host - self._port = port - self._slave_id = slave_id - self._name = f"{name} {command_key.split('_')[-1].title()}" - self._command = bytes([slave_id]) + COMMANDS[command_key] - self._unit = unit - self._data_index = data_index - self._factor = factor - self._is_string = is_string - self._text_mapping = text_mapping # Für lesbare Texte - self._state = None - self._attr_native_unit_of_measurement = unit if not is_string else None - self._attr_unique_id = f"aurora_{slave_id}_{command_key.lower()}" - - @property - def state(self): - """Aktueller Zustand des Sensors.""" - return self._state - - def update(self): - """Aktualisiere die Sensordaten.""" - try: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.settimeout(3) - s.connect((self._host, self._port)) - s.send(self._command) - response = s.recv(1024) - if response: - if self._is_string: - # Seriennummer/Modell als String - self._state = response[self._data_index:].decode('ascii').strip() - elif self._text_mapping: - # Alarme/Status/Fehler als lesbarer Text - raw_value = int.from_bytes(response[:2], byteorder='little', signed=False) - self._state = self._text_mapping.get(raw_value, f"Unbekannt (0x{raw_value:04X})") - else: - # Standardwerte (Integer mit Skalierung) - self._state = int.from_bytes( - response[self._data_index:self._data_index + 2], - byteorder='little', - signed=True - ) * self._factor - else: - self._state = None - _LOGGER.warning("Keine Antwort für %s", self._name) - except Exception as e: - self._state = None - _LOGGER.error("Fehler bei %s: %s", self._name, e) - def setup_platform(hass, config, add_entities, discovery_info=None): - """Richte ALLE ABB Aurora Sensoren ein.""" + """Set up the ABB Aurora sensors.""" host = config[CONF_HOST] port = config[CONF_PORT] slave_id = config.get(CONF_SLAVE_ID, 2) name = config.get("name", f"Aurora WR {slave_id}") - # Erstelle alle Sensoren für diesen Wechselrichter + # Create all sensors for this inverter sensors = [ # Leistung und Energie - AuroraSensorBase(host, port, slave_id, name, "DSP_GRID_POWER", "W"), - AuroraSensorBase(host, port, slave_id, name, "DSP_DAILY_ENERGY", "Wh", data_index=2), - AuroraSensorBase(host, port, slave_id, name, "DSP_TOTAL_ENERGY", "kWh", factor=0.1), - AuroraSensorBase(host, port, slave_id, name, "DSP_GRID_VOLTAGE", "V"), - AuroraSensorBase(host, port, slave_id, name, "DSP_GRID_CURRENT", "A"), - AuroraSensorBase(host, port, slave_id, name, "DSP_GRID_FREQUENCY", "Hz"), - AuroraSensorBase(host, port, slave_id, name, "DSP_PF", "", 0, 0.01), # Skaliert mit 0.01 + AuroraSensorBase(host, port, slave_id, f"{name} Power", "DSP_GRID_POWER", "W"), + AuroraSensorBase(host, port, slave_id, f"{name} Daily Energy", "DSP_DAILY_ENERGY", "Wh"), + AuroraSensorBase(host, port, slave_id, f"{name} Total Energy", "DSP_TOTAL_ENERGY", "kWh"), + AuroraSensorBase(host, port, slave_id, f"{name} Grid Voltage", "DSP_GRID_VOLTAGE", "V"), + AuroraSensorBase(host, port, slave_id, f"{name} Grid Current", "DSP_GRID_CURRENT", "A"), + AuroraSensorBase(host, port, slave_id, f"{name} Grid Frequency", "DSP_GRID_FREQUENCY", "Hz"), + AuroraSensorBase(host, port, slave_id, f"{name} PF", "DSP_PF", "", 0.01), # Gleichstromkreis - AuroraSensorBase(host, port, slave_id, name, "DSP_DC_VOLTAGE", "V"), - AuroraSensorBase(host, port, slave_id, name, "DSP_DC_CURRENT", "A"), - AuroraSensorBase(host, port, slave_id, name, "DSP_DC_POWER", "W"), - AuroraSensorBase(host, port, slave_id, name, "DSP_MPPT_POWER", "W"), + AuroraSensorBase(host, port, slave_id, f"{name} DC Voltage", "DSP_DC_VOLTAGE", "V"), + AuroraSensorBase(host, port, slave_id, f"{name} DC Current", "DSP_DC_CURRENT", "A"), + AuroraSensorBase(host, port, slave_id, f"{name} DC Power", "DSP_DC_POWER", "W"), # Temperatur und Umwelt - AuroraSensorBase(host, port, slave_id, name, "DSP_TEMPERATURE", "°C"), - AuroraSensorBase(host, port, slave_id, name, "DSP_RADIATOR_TEMP", "°C"), - AuroraSensorBase(host, port, slave_id, name, "DSP_AMBIENT_TEMP", "°C"), + AuroraSensorBase(host, port, slave_id, f"{name} Temperature", "DSP_TEMPERATURE", "°C"), # Diagnose (wichtig!) - AuroraSensorBase( - host, port, slave_id, name, "DSP_ALARMS", "", - text_mapping=ALARM_MESSAGES - ), - AuroraSensorBase( - host, port, slave_id, name, "DSP_FAULT_CODE", "", - text_mapping=FAULT_MESSAGES - ), - AuroraSensorBase( - host, port, slave_id, name, "DSP_STATUS", "", - text_mapping=STATUS_MESSAGES - ), - AuroraSensorBase(host, port, slave_id, name, "DSP_EVENTS", "", is_string=False), - AuroraSensorBase(host, port, slave_id, name, "DSP_LAST_ERROR", "", is_string=False), + AuroraSensorBase(host, port, slave_id, f"{name} Alarms", "DSP_ALARMS", "", text_mapping=ALARM_MESSAGES), + AuroraSensorBase(host, port, slave_id, f"{name} Status", "DSP_STATUS", "", text_mapping=STATUS_MESSAGES), + AuroraSensorBase(host, port, slave_id, f"{name} Fault Code", "DSP_FAULT_CODE", "", text_mapping=FAULT_MESSAGES), # Seriennummern und Modell (als String) - AuroraSensorBase(host, port, slave_id, name, "DSP_SERIAL_NUMBER", "", is_string=True), - AuroraSensorBase(host, port, slave_id, name, "DSP_MODEL", "", is_string=True), - AuroraSensorBase(host, port, slave_id, name, "DSP_VERSION", "", is_string=True), - - # Erweiterte Diagnose - AuroraSensorBase(host, port, slave_id, name, "DSP_ISOLATION", "kΩ"), - AuroraSensorBase(host, port, slave_id, name, "DSP_OPERATING_HOURS", "h"), + AuroraSensorBase(host, port, slave_id, f"{name} Serial Number", "DSP_SERIAL_NUMBER", "", is_string=True), + AuroraSensorBase(host, port, slave_id, f"{name} Version", "DSP_VERSION", "", is_string=True), ] add_entities(sensors, True)