From 1b58579c22cb8e6794c57a34820c3593525d96ed Mon Sep 17 00:00:00 2001 From: "m0@nas" Date: Tue, 8 Sep 2026 22:45:06 +0200 Subject: [PATCH] Neue Shellys ueber MQTT statt ueber HTTP erkennen Die Shelly-Erkennung schrieb bisher fuer jedes Geraet eine HTTP-Adresse in actor_states.url; der Automatik-Runner musste die Werte deshalb im Takt abfragen, obwohl die Gen2-Geraete ihren Zustand ohnehin von sich aus an den Broker melden. Sichtbar wurde das an den beiden Stromzaehlern: ihre Messwerte standen nirgends als Topic, weshalb sie weder in den Kachelwerten noch im Automatik-Editor auftauchten. Gen2-Geraete werden jetzt nach ihrer MQTT-Einrichtung gefragt (/rpc/Mqtt.GetConfig). Ist sie eingeschaltet, meldet das Geraet Statusaenderungen und hat ein Topic-Praefix, so steht in url das Topic (/status/:) und in value_path der Schluessel in der Nachricht. Fehlt eines davon - und bei allen Gen1-Geraeten, die kein MQTT koennen - bleibt alles beim HTTP-Weg. Co-Authored-By: Claude Opus 5 --- .../deviceDiscovery/modules/shelly_module.py | 88 ++++++++++++++++++- 1 file changed, 84 insertions(+), 4 deletions(-) diff --git a/restricted/deviceDiscovery/modules/shelly_module.py b/restricted/deviceDiscovery/modules/shelly_module.py index 864ee77..3ce7d36 100644 --- a/restricted/deviceDiscovery/modules/shelly_module.py +++ b/restricted/deviceDiscovery/modules/shelly_module.py @@ -173,6 +173,32 @@ class ShellyDiscovery: return None + def get_mqtt_config(self, device: Dict) -> Optional[Dict]: + """ + Holt die MQTT-Einstellung eines Gen2-Geräts. + + Nur Gen2 kann MQTT; die alten Geräte werden weiterhin abgefragt. Wer + sendet, muss nicht gepollt werden - deshalb interessiert hier nicht + nur, ob MQTT eingeschaltet ist, sondern auch das Präfix: daraus ergibt + sich das Topic, unter dem die Werte ankommen. + + Returns: + Die Antwort von Mqtt.GetConfig oder None (Gen1, altes Firmware, + Gerät nicht erreichbar). + """ + if device.get('generation') != 2: + return None + try: + response = requests.get( + f"http://{device['ip']}/rpc/Mqtt.GetConfig", + timeout=2 + ) + if response.status_code == 200: + return response.json() + except Exception as e: + logger.debug(f"Keine MQTT-Einstellung von {device['ip']}: {e}") + return None + def discover_devices(self) -> List[Dict]: """ Entdeckt alle Shelly-Geräte im Netzwerk @@ -185,6 +211,7 @@ class ShellyDiscovery: for idx, device in enumerate(self.devices): status = self.get_device_status(device) self.devices[idx]['status'] = status + self.devices[idx]['mqtt'] = self.get_mqtt_config(device) logger.info(f"Insgesamt {len(self.devices)} Shelly-Geräte entdeckt") return self.devices @@ -205,6 +232,52 @@ class ShellyModule(BaseModule): """Prüft ob Shelly aktiviert ist""" return self.config.shelly_enable + @staticmethod + def _mqtt_praefix(device: Dict) -> Optional[str]: + """ + Das Topic-Präfix, unter dem ein Gerät seine Zustände sendet - oder + None, wenn es das nicht tut. + + Verlangt beides: MQTT eingeschaltet UND die Statusmeldungen + eingeschaltet. Ohne status_ntf hält das Gerät die Verbindung, schickt + aber nichts von selbst - dann bliebe ein Messwert auf dem Broker ewig + aus, und Abfragen ist die richtige Antwort. + """ + mqtt = device.get('mqtt') or {} + if mqtt.get('enable') and mqtt.get('status_ntf') and mqtt.get('topic_prefix'): + return mqtt['topic_prefix'] + return None + + @staticmethod + def _adressiere(states: List[Dict], praefix: Optional[str], + komponente: str, index: int) -> List[Dict]: + """ + Die Messwerte einer Komponente adressieren. + + Zwei Wege, und der Unterschied ist nicht Geschmack, sondern Technik: + + ohne MQTT Das Gerät wird abgefragt. Die Adresse steht am Gerät + (actors.url = der RPC-Aufruf), der Messwert nennt nur + das Feld der Antwort: url = 'total_act_power'. + + mit MQTT Es gibt nichts abzufragen, die Nachricht kommt von + selbst. Dann muss die Adresse am Messwert stehen: + url = 'Power_EG/status/em:0', und das Feld in der + JSON-Nachricht wandert nach value_path - genau so, wie + es das MQTT-Modul für alle anderen Geräte hält. + + Damit fällt für die neuen Geräte das Abfragen im Automatik-Runner weg; + die alten Shellys ohne MQTT bleiben unverändert beim HTTP-Weg. + """ + if not praefix: + return states + topic = f"{praefix}/status/{komponente}:{index}" + for state in states: + # Was bisher die Adresse war, ist jetzt der Schlüssel darin. + state['value_path'] = state.get('url') + state['url'] = topic + return states + def discover(self) -> Tuple[List[Dict], List[Dict]]: """ Führt Shelly Discovery durch @@ -256,6 +329,11 @@ class ShellyModule(BaseModule): device_name = info.get('name', f"Shelly_{info.get('id', ip)}") device_model = info.get('model', 'Unknown') + # Sendet das Geraet seine Zustaende selbst? Dann bekommen die + # Messwerte Topics statt Feldnamen - siehe _adressiere(). + praefix = self._mqtt_praefix(device) + if praefix: + logger.info(f" {device_name}: Zustaende ueber MQTT ({praefix}/status/...)") # Switches als Aktoren switch_count = sum(1 for key in status.keys() if key.startswith('switch:')) @@ -271,14 +349,14 @@ class ShellyModule(BaseModule): {'command': 'turn_off', 'url': 'on=false', 'parameters': []}, {'command': 'toggle', 'parameters': []} ], - 'states': [ + 'states': self._adressiere([ { 'name': 'output', 'type': 'boolean', 'url': 'output', 'current_value': switch_data.get('output', False) } - ] + ], praefix, 'switch', i) }) # Temperatursensoren @@ -290,7 +368,7 @@ class ShellyModule(BaseModule): 'type': 'Temperatur', 'name': f"{device_name}_Temp_{i}", 'url': f"http://{ip}/rpc/Temperature.GetStatus?id={i}", - 'states': [ + 'states': self._adressiere([ { 'name': 'temperature', 'type': 'number', @@ -298,7 +376,7 @@ class ShellyModule(BaseModule): 'url': f"tC", 'unit': '°C' } - ] + ], praefix, 'temperature', i) }) # Energy-Meter em_count = sum(1 for key in status.keys() if key.startswith('em:')) @@ -422,6 +500,8 @@ class ShellyModule(BaseModule): 'url': 'total_act_power', 'current_value': em_data.get('total_act_power'), 'unit': 'W'}) + # Erst hier, weil die Messwerte oben einzeln angehaengt werden. + self._adressiere(sensors[len(sensors)-1]['states'], praefix, 'em', i) return actors, sensors