diff --git a/restricted/deviceDiscovery/device_discovery.py b/restricted/deviceDiscovery/device_discovery.py index 38f0ed9..eb1798e 100644 --- a/restricted/deviceDiscovery/device_discovery.py +++ b/restricted/deviceDiscovery/device_discovery.py @@ -20,7 +20,8 @@ from modules.wled_module import WLEDModule from modules.mqtt_module import MQTTModule from modules.shelly_module import ShellyModule from modules.gartenwasser_module import GartenwasserModule -from modules.logic_module import LogicModule #nicht Vergessen neue Module auch in Zeile 546 einzufügen!! +from modules.solarmanager_module import SolarManagerModule +from modules.logic_module import LogicModule #nicht Vergessen neue Module auch unten in die Liste "modules" einzufügen!! # Logging logging.basicConfig( @@ -157,6 +158,13 @@ class Config: if not self.config.has_section('gartenwasser'): return False return self._get_bool('gartenwasser', 'enable', False) + + # SOLARMANAGER + @property + def solarmanager_enable(self) -> bool: + if not self.config.has_section('solarmanager'): + return False + return self._get_bool('solarmanager', 'enable', False) # MQTT @@ -580,7 +588,8 @@ def main(): MQTTModule(config), ShellyModule(config), LogicModule(config), - GartenwasserModule(config) + GartenwasserModule(config), + SolarManagerModule(config) ] # Jedes Modul durchlaufen diff --git a/restricted/deviceDiscovery/modules/solarmanager_module.py b/restricted/deviceDiscovery/modules/solarmanager_module.py new file mode 100644 index 0000000..8914944 --- /dev/null +++ b/restricted/deviceDiscovery/modules/solarmanager_module.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +""" +SolarManager Module +Die wenigen Messwerte aus solarManager/#, die eine Automatik braucht +KEINE Datenbank-Operationen! +""" + +import logging +from typing import List, Dict, Tuple +from modules.base_module import BaseModule + +logger = logging.getLogger(__name__) + +# Wurzel aller Topics, die solarManager.py über mqttClient.publish() abgibt. +# Dasselbe Wort steht dort als Vorgabe im Quelltext; bewusst kein +# Konfigurationseintrag, er müsste in beiden Repositorien gleich lauten. +WURZEL = "solarManager" + +# Aus dieser Nachricht wird hier bewusst nur ein kleiner Teil zu Messwerten. +# Sie trägt gut 130 Topics: jeden Wechselrichter einzeln, jede Strangleistung, +# jeden Phasenstrom, jede Vorlauf- und Rücklauftemperatur. Alles anzulegen +# hieße, die Auswahl im Automatik-Editor unbrauchbar zu machen - man sucht +# dann in einer Liste, statt eine Regel zu schreiben. Aufgenommen ist, was +# das Realtime-SVG auf der Solarseite anzeigt, und von der PV nur die Summe. +# +# Nicht dabei sind die beiden Shelly EM3 (Erd- und Untergeschoss). Die melden +# sich per Home-Assistant-Discovery und stehen über das MQTT-Modul längst als +# eigene Geräte in der Datenbank; ein zweiter Satz derselben Zahlen machte die +# Auswahl nur unübersichtlich. Zu beachten: solarManager/eg wäre ohnehin nicht +# dasselbe - dort ist die Wallbox im Carport mit eingerechnet. +# +# Die beiden Modbus-Zähler am Gen24 gibt es dagegen *nur* hier. Sie hängen +# unter 192.168.179.155 an derselben Adresse wie der Wechselrichter und +# sprechen sonst mit niemandem (siehe gatherModbusData.py im SolarManager, +# meter0 und meter1). +# +# Jedes dieser Topics trägt eine blanke Zahl, kein JSON - deshalb hat keiner +# der Messwerte einen value_path. Wer später einen vermisst, trägt hier eine +# Zeile ein; die vollständige Liste steht im Browser unter mqttData auf der +# Solarseite. +# +# Die URL eines Geräts ist das Topic seines ersten Messwerts. Ein eigener +# Namensraum wäre erfunden: der SolarManager kennt keine Geräte, nur Zahlen. +# Sie muss nur eindeutig sein und mit mqtt:// beginnen, damit der Runner den +# MQTT-Transport wählt; abonniert wird über die Topics der Messwerte. +GERAETE = [ + { + # meter0 am Gen24, der Zähler am Hausanschluss. + # + # Bezug und Einspeisung statt P_Grid: P_Grid ist eine Zahl mit + # Vorzeichen (negativ heißt Einspeisung), und eine Bedingung darauf + # liest sich falsch herum. Diese beiden sind immer positiv, jeweils + # eine von beiden ist null. + "type": "Stromzähler", + "name": "Netzanschluss", + "messwerte": [ + ("Bezug", "P_GridCons", "W"), + ("Einspeisung", "P_GridFeed", "W"), + ], + }, + { + # meter1 am Gen24, der Zähler für das Obergeschoss. + # + # ACHTUNG, Vorzeichen: der Zähler meldet Verbrauch negativ, und + # solarManager.py rechnet die Wallbox im Obergeschoss heraus, ohne das + # Vorzeichen zu drehen. Das Realtime-SVG zeigt deshalb -og an. Eine + # Bedingung "das Obergeschoss verbraucht mehr als 500 W" heißt hier + # also "Leistung < -500". + "type": "Stromzähler", + "name": "Stromzähler Obergeschoss", + "messwerte": [("Leistung", "og", "W")], + }, + { + "type": "Solaranlage", + "name": "Solaranlage", + "messwerte": [ + # Summe aller Stränge. Die einzelnen stehen in P_PVn, einem + # JSON-Feld - für eine Bedingung taugt die Summe besser, und + # genau darum ging es bei der Auswahl. + ("PV-Leistung", "P_PV", "W"), + ("Batterie-Ladestand", "SOC", "%"), + ], + }, + { + "type": "Heizung", + "name": "Pufferspeicher", + "messwerte": [ + ("Temperatur oben", "t_buffT", "°C"), + ("Temperatur Mitte", "t_buffM", "°C"), + ("Temperatur unten", "t_buffB", "°C"), + # Was der Heizstab gerade in den Puffer schiebt. Nur zum Lesen: + # gestellt wird er über ajax/heater.php, nicht über MQTT. + ("Heizstab-Leistung", "pHeat", "W"), + ], + }, +] + + +class SolarManagerModule(BaseModule): + """ + SolarManager Modul - Implementiert BaseModule Interface + + Wie das Gartenwasser- und das Logic-Modul sucht dieses nichts, sondern + schreibt feste Geräte hin. Der SolarManager meldet sich nicht per + Home-Assistant-Discovery; das MQTT-Modul findet ihn also nicht, und ohne + dieses Modul kann eine Automatik weder auf den Netzbezug noch auf den + Ladestand der Batterie oder die Temperatur im Puffer reagieren. + + Alles hier ist zum Lesen. Geschaltet wird am SolarManager nichts: die + Wallbox und die Heizung regelt er selbst, und wo das Web-UI eingreift, + tut es das über eigene Endpunkte (ajax/heater.php, ajax/carSteuerung.php) + und nicht über diese Topics. + """ + + def is_enabled(self) -> bool: + """Prüft ob das SolarManager-Modul aktiviert ist""" + return self.config.solarmanager_enable + + def discover(self) -> Tuple[List[Dict], List[Dict]]: + """ + Erzeugt die Geräte aus GERAETE - alle als Sensoren, ohne Kommandos. + + Returns: + Tuple (actors, sensors) + """ + logger.info("\n" + "=" * 60) + logger.info("SOLARMANAGER-MESSWERTE WERDEN ERZEUGT") + logger.info("=" * 60) + + sensoren = [self._geraet(g) for g in GERAETE] + anzahl = sum(len(g["messwerte"]) for g in GERAETE) + logger.info("%d Geräte mit %d Messwerten", len(sensoren), anzahl) + return [], sensoren + + @staticmethod + def _geraet(geraet: Dict) -> Dict: + """Ein Gerät mit seinen Messwerten.""" + states = [{ + "name": name, + "url": WURZEL + "/" + topic, + "type": "float", + "unit": einheit, + } for name, topic, einheit in geraet["messwerte"]] + + return { + "type": geraet["type"], + "name": geraet["name"], + "url": "mqtt://" + states[0]["url"], + "states": states, + }