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
(<praefix>/status/<komponente>:<index>) 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 <noreply@anthropic.com>
This commit is contained in:
2026-09-08 22:45:06 +02:00
co-authored by Claude Opus 5
parent cca452806f
commit 1b58579c22
@@ -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