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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user