Regenbasierte automatische Gartenbewässerung + homeMesh-Anbindung für AutoAction-Regeleditor

- Neues stündliches Python-Script (restricted/gartenbewaesserung/auto_watering.py): holt Niederschlags-
  und Sonnenuntergangsdaten von Open-Meteo, wertet je Zone (Hochbeete/Tröge/Garten vorn) individuelle
  Regen-Schwellenwerte und Mindest-Bewässerungsdauern aus und startet bei Bedarf per MQTT den passenden
  Automatik-Modus der Ventilsteuerung im Zeitraum vor Sonnenuntergang; veröffentlicht den Regen-Status
  zusätzlich stündlich als retained MQTT-Nachricht je Zone.
- Web-UI: neue Bewässerungs-Steuerung/-Anzeige (ajax/watering.php, js/solar/solarMQTT.js,
  assets/img/realtime.svg) inkl. Live-Status-Icons (aus/geplant/pausiert/aktiv per Farbe und Symbol).
- AutoAction-Regeleditor (ajax/actorDetails.php, sensorDetails.php, fillActorDD.php, fillSensorDD.php,
  tahoma.php, AutoAction.php) liest Actor-/Sensor-Parameter jetzt live aus der homeMesh-Datenbank statt
  aus stark vereinfachten Altfeldern, inkl. zugehöriger Anpassungen am Device-Discovery-Tooling
  (restricted/deviceDiscovery/*, neues logic_module.py).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 20:19:27 +02:00
co-authored by Claude Sonnet 5
parent 4de95c1a29
commit 29f752eafe
22 changed files with 1693 additions and 234 deletions
@@ -255,10 +255,13 @@ class MQTTDeviceConverter:
device_info = device_data.get('device_info')
# Gerätename vom ersten Entity oder aus device_info
device_name = device_info.get('name', device_id)
if not device_name or device_name == device_id:
device_mf = device_info.get('model', device_info.get('mdl', device_info.get('mf','MQTT')))
if(device_info.get('mf','MQTT') == "OpenDTU"):
device_mf = "Wechselrichter"
#if not device_name or device_name == device_id:
# Fallback: Name vom ersten Entity
if entities:
device_name = entities[0]['config'].get('name', device_id)
# if entities:
# device_name = entities[0]['config'].get('name', device_id)
# Device URL
device_url = f"mqtt://{device_id}"
@@ -279,11 +282,13 @@ class MQTTDeviceConverter:
component = entity['component']
object_id = entity.get('object_id', 'unknown')
config = entity['config']
if(device_mf == "MQTT"):
device_mf = config.get('device', device_info.get('dev',{'model': 'MQTT'})).get('model', device_info.get('mdl', device_info.get('mf','MQTT')))
# Command aus der Entity erstellen
command_entry = MQTTDeviceConverter._entity_to_command(component, object_id, config)
command_entry = MQTTDeviceConverter._entity_to_commands(component, object_id, config)
if command_entry:
commands.append(command_entry)
commands.extend(command_entry)
# States aus der Entity extrahieren
entity_states = MQTTDeviceConverter._entity_to_states(component, object_id, config)
@@ -292,7 +297,7 @@ class MQTTDeviceConverter:
if commands: # Nur Actor erstellen wenn Commands vorhanden
actor_result = {
'name': device_name,
'type': f"mqtt_device", # Allgemeiner Typ für Multi-Entity-Geräte
'type': device_mf, # Allgemeiner Typ für Multi-Entity-Geräte
'url': device_url,
'commands': commands,
'states': states
@@ -306,7 +311,8 @@ class MQTTDeviceConverter:
component = entity['component']
object_id = entity.get('object_id', 'unknown')
config = entity['config']
if(device_mf == "MQTT"):
device_mf = device_info.get('model', device_info.get('mdl', device_info.get('mf','MQTT')))
# States aus der Entity extrahieren
entity_states = MQTTDeviceConverter._entity_to_states(component, object_id, config)
states.extend(entity_states)
@@ -314,7 +320,7 @@ class MQTTDeviceConverter:
if states: # Nur Sensor erstellen wenn States vorhanden
sensor_result = {
'name': device_name,
'type': f"mqtt_device",
'type': device_mf,
'url': device_url,
'states': states
}
@@ -322,7 +328,7 @@ class MQTTDeviceConverter:
return actor_result, sensor_result
@staticmethod
def _entity_to_command(component: str, object_id: str, config: Dict) -> Optional[Dict]:
def _entity_to_commands(component: str, object_id: str, config: Dict) -> Optional[Dict]:
"""
Konvertiert eine MQTT Entity in ein Command
@@ -336,19 +342,21 @@ class MQTTDeviceConverter:
"""
# Command Topic - verschiedene mögliche Feldnamen
command_topic = config.get('command_topic') or config.get('cmd_t') or config.get('temperature_command_topic')
base_topic = config.get('~','')
command_topic = command_topic.replace("~",base_topic)
if not command_topic:
return None
# Command-Name aus object_id ableiten
command_name = object_id.replace('_', ' ').title().replace(' ', '')
#command_name =
# Oder aus dem Namen
entity_name = config.get('name', object_id)
command_name = config.get('name', object_id.replace('_', ' ').title().replace(' ', ''))
command_entry = {
command_entry = [{
'command': command_name,
'parameters': []
}
'parameters': [],
'url': command_topic
}]
# Parameter basierend auf Component-Typ
if component == 'number':
@@ -375,7 +383,7 @@ class MQTTDeviceConverter:
if unit:
param['unit'] = unit
command_entry['parameters'].append(param)
command_entry[0]['parameters'].append(param)
elif component == 'select':
# Select hat Optionen
@@ -389,7 +397,7 @@ class MQTTDeviceConverter:
if options:
param['values'] = options
command_entry['parameters'].append(param)
command_entry[0]['parameters'].append(param)
elif component in ['switch', 'light']:
# Switch/Light haben on/off
@@ -402,7 +410,7 @@ class MQTTDeviceConverter:
config.get('payload_off', config.get('pl_off', 'OFF'))
]
}
command_entry['parameters'].append(param)
command_entry[0]['parameters'].append(param)
# Brightness für Light
brightness_cmd_topic = (
@@ -410,7 +418,7 @@ class MQTTDeviceConverter:
config.get('bri_cmd_t')
)
if component == 'light' and brightness_cmd_topic:
command_entry['parameters'].append({
command_entry[0]['parameters'].append({
'name': 'brightness',
'type': 'integer',
'min': 0,
@@ -432,7 +440,7 @@ class MQTTDeviceConverter:
'max': 100,
'url': set_pos_topic
}
command_entry['parameters'].append(param)
command_entry[0]['parameters'].append(param)
else:
# Nur open/close/stop
param = {
@@ -441,7 +449,7 @@ class MQTTDeviceConverter:
'url': command_topic,
'values': ['OPEN', 'CLOSE', 'STOP']
}
command_entry['parameters'].append(param)
command_entry[0]['parameters'].append(param)
elif component == 'button':
# Button hat normalerweise keinen Parameter, nur das Topic
@@ -450,7 +458,7 @@ class MQTTDeviceConverter:
'type': 'trigger',
'url': command_topic
}
command_entry['parameters'].append(param)
command_entry[0]['parameters'].append(param)
elif component == 'climate':
# Climate hat Temperatur-Setpoint
@@ -469,8 +477,9 @@ class MQTTDeviceConverter:
param['min'] = config['min_temp']
if 'max_temp' in config:
param['max'] = config['max_temp']
command_entry['parameters'].append(param)
command_entry[0]['command'] = "Solltemperatur"
command_entry[0]['parameters'].append(param)
mode_cmd_topic = (
config.get('mode_command_topic') or
config.get('mode_cmd_t')
@@ -482,7 +491,10 @@ class MQTTDeviceConverter:
'url': mode_cmd_topic,
'values': config.get('modes', [])
}
command_entry['parameters'].append(param)
command_entry.append({
'command': "Modus",
'parameters': [param]
})
else:
# Generischer Command mit dem Topic
param = {
@@ -512,8 +524,7 @@ class MQTTDeviceConverter:
# State Topic - verschiedene mögliche Feldnamen prüfen
state_topic = (
config.get('state_topic') or
config.get('stat_t') or # Abkürzung
config.get('~') and config.get('stat_t') # Mit Base Topic
config.get('stat_t')
)
# Bei number/select: oft kein separates state_topic, dann command_topic verwenden
@@ -523,6 +534,8 @@ class MQTTDeviceConverter:
state_topic = config.get('command_topic') or config.get('cmd_t')
if state_topic:
base_topic = config.get('~','')
state_topic = state_topic.replace("~",base_topic)
state_entry = {
'name': object_id,
'type': 'string',
@@ -654,7 +667,6 @@ class MQTTModule(BaseModule):
actor_data, sensor_data = MQTTDeviceConverter.convert_device_to_actors_and_sensors(
device_id, device_data
)
if actor_data:
actors.append(actor_data)