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
|
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]:
|
def discover_devices(self) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Entdeckt alle Shelly-Geräte im Netzwerk
|
Entdeckt alle Shelly-Geräte im Netzwerk
|
||||||
@@ -185,6 +211,7 @@ class ShellyDiscovery:
|
|||||||
for idx, device in enumerate(self.devices):
|
for idx, device in enumerate(self.devices):
|
||||||
status = self.get_device_status(device)
|
status = self.get_device_status(device)
|
||||||
self.devices[idx]['status'] = status
|
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")
|
logger.info(f"Insgesamt {len(self.devices)} Shelly-Geräte entdeckt")
|
||||||
return self.devices
|
return self.devices
|
||||||
@@ -205,6 +232,52 @@ class ShellyModule(BaseModule):
|
|||||||
"""Prüft ob Shelly aktiviert ist"""
|
"""Prüft ob Shelly aktiviert ist"""
|
||||||
return self.config.shelly_enable
|
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]]:
|
def discover(self) -> Tuple[List[Dict], List[Dict]]:
|
||||||
"""
|
"""
|
||||||
Führt Shelly Discovery durch
|
Führt Shelly Discovery durch
|
||||||
@@ -256,6 +329,11 @@ class ShellyModule(BaseModule):
|
|||||||
|
|
||||||
device_name = info.get('name', f"Shelly_{info.get('id', ip)}")
|
device_name = info.get('name', f"Shelly_{info.get('id', ip)}")
|
||||||
device_model = info.get('model', 'Unknown')
|
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
|
# Switches als Aktoren
|
||||||
switch_count = sum(1 for key in status.keys() if key.startswith('switch:'))
|
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': 'turn_off', 'url': 'on=false', 'parameters': []},
|
||||||
{'command': 'toggle', 'parameters': []}
|
{'command': 'toggle', 'parameters': []}
|
||||||
],
|
],
|
||||||
'states': [
|
'states': self._adressiere([
|
||||||
{
|
{
|
||||||
'name': 'output',
|
'name': 'output',
|
||||||
'type': 'boolean',
|
'type': 'boolean',
|
||||||
'url': 'output',
|
'url': 'output',
|
||||||
'current_value': switch_data.get('output', False)
|
'current_value': switch_data.get('output', False)
|
||||||
}
|
}
|
||||||
]
|
], praefix, 'switch', i)
|
||||||
})
|
})
|
||||||
|
|
||||||
# Temperatursensoren
|
# Temperatursensoren
|
||||||
@@ -290,7 +368,7 @@ class ShellyModule(BaseModule):
|
|||||||
'type': 'Temperatur',
|
'type': 'Temperatur',
|
||||||
'name': f"{device_name}_Temp_{i}",
|
'name': f"{device_name}_Temp_{i}",
|
||||||
'url': f"http://{ip}/rpc/Temperature.GetStatus?id={i}",
|
'url': f"http://{ip}/rpc/Temperature.GetStatus?id={i}",
|
||||||
'states': [
|
'states': self._adressiere([
|
||||||
{
|
{
|
||||||
'name': 'temperature',
|
'name': 'temperature',
|
||||||
'type': 'number',
|
'type': 'number',
|
||||||
@@ -298,7 +376,7 @@ class ShellyModule(BaseModule):
|
|||||||
'url': f"tC",
|
'url': f"tC",
|
||||||
'unit': '°C'
|
'unit': '°C'
|
||||||
}
|
}
|
||||||
]
|
], praefix, 'temperature', i)
|
||||||
})
|
})
|
||||||
# Energy-Meter
|
# Energy-Meter
|
||||||
em_count = sum(1 for key in status.keys() if key.startswith('em:'))
|
em_count = sum(1 for key in status.keys() if key.startswith('em:'))
|
||||||
@@ -422,6 +500,8 @@ class ShellyModule(BaseModule):
|
|||||||
'url': 'total_act_power',
|
'url': 'total_act_power',
|
||||||
'current_value': em_data.get('total_act_power'),
|
'current_value': em_data.get('total_act_power'),
|
||||||
'unit': 'W'})
|
'unit': 'W'})
|
||||||
|
# Erst hier, weil die Messwerte oben einzeln angehaengt werden.
|
||||||
|
self._adressiere(sensors[len(sensors)-1]['states'], praefix, 'em', i)
|
||||||
|
|
||||||
return actors, sensors
|
return actors, sensors
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user