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:
@@ -51,47 +51,166 @@ class DeviceClassifier:
|
||||
"""Original DeviceClassifier - unverändert"""
|
||||
|
||||
ACTOR_TYPES = {
|
||||
'RollerShutter', 'ExteriorScreen', 'Awning', 'Blind',
|
||||
'GarageDoor', 'Window', 'Light', 'OnOff', 'DimmableLight',
|
||||
'HeatingSystem', 'Valve', 'Switch', 'Door', 'Curtain',
|
||||
'VenetianBlind', 'PergolaScreen'
|
||||
'RollerShutter': 'Rollladen',
|
||||
'ExteriorScreen': 'Außenrollo',
|
||||
'Awning': 'Markise',
|
||||
'ExteriorVenetianBlind':'Außenjalousie',
|
||||
'Blind': 'Jalousie',
|
||||
'GarageDoor': 'Garagentor',
|
||||
'Window': 'Fenster',
|
||||
'Light': 'Beleuchtung',
|
||||
'OnOff': 'Schalter',
|
||||
'DimmableLight': 'Dimmbare Beleuchtung',
|
||||
'HeatingSystem': 'Heizungssystem',
|
||||
'Valve': 'Ventil',
|
||||
'Switch': 'Schalter',
|
||||
'Door': 'Tür',
|
||||
'Curtain': 'Vorhang',
|
||||
'VenetianBlind': 'Jalousie',
|
||||
'PergolaScreen': 'Pergola-Rollo'
|
||||
}
|
||||
|
||||
SENSOR_TYPES = {
|
||||
'TemperatureSensor', 'LightSensor', 'HumiditySensor',
|
||||
'ContactSensor', 'OccupancySensor', 'SmokeSensor',
|
||||
'WaterDetectionSensor', 'WindowHandle', 'MotionSensor',
|
||||
'SunSensor', 'WindSensor', 'RainSensor', 'ConsumptionSensor'
|
||||
'TemperatureSensor': 'Temperatur',
|
||||
'LightSensor': 'Licht',
|
||||
'HumiditySensor': 'Feuchtigkeit',
|
||||
'ContactSensor': 'Kontakt',
|
||||
'OccupancySensor': 'Anwesenheit',
|
||||
'SmokeSensor': 'Rauch',
|
||||
'WaterDetectionSensor': 'Wasser',
|
||||
'WindowHandle': 'Fenstergriff',
|
||||
'MotionSensor': 'Bewegung',
|
||||
'SunSensor': 'Sonne',
|
||||
'WindSensor': 'Wind',
|
||||
'RainSensor': 'Regen',
|
||||
'ConsumptionSensor': 'Verbrauch'
|
||||
}
|
||||
|
||||
STATE_NAMES = {
|
||||
"core:BatteryLevelState": 'Ladestand',
|
||||
"core:BatteryState": 'Batteriezustand',
|
||||
"core:ClosureState": 'Position',
|
||||
"core:CommandLockLevelsState": 'Gesperrt',
|
||||
"core:Memorized1OrientationState": 'Gespeicherte Neigung',
|
||||
"core:Memorized1PositionState": 'Gespeicherte Position',
|
||||
"core:MovingState": 'Fährt gerade',
|
||||
"core:OpenClosedState": 'Geöffnet/Geschlossen',
|
||||
"core:PriorityLockTimerState": 'Verriegelungstimer',
|
||||
"core:DiscreteRSSILevelState": 'RSSI',
|
||||
"core:SlateOrientationState": 'Lamellenausrichtung',
|
||||
"core:StatusState": 'Status',
|
||||
"core:LuminanceState": 'Helligkeit',
|
||||
"core:SmokeState": 'Rauch',
|
||||
"core:SunEnergyState": 'Sonnenenergie',
|
||||
"core:TemperatureState": 'Temperatur',
|
||||
"io:MaintenanceRadioPartBatteryState": 'Ladestand Funkmodul',
|
||||
"io:MaintenanceSensorPartBatteryState": 'Ladestand Sensor',
|
||||
"core:SensorDefectState": 'Sensor defekt'
|
||||
}
|
||||
|
||||
# Tahoma Commands mit Parametern
|
||||
STATE_ENUMS = {
|
||||
"core:BatteryLevelState": ["full","normal","low","verylow"],
|
||||
"core:BatteryState": ["full","normal","low","verylow"],
|
||||
"core:ClosureState": '',
|
||||
"core:CommandLockLevelsState": '',
|
||||
"core:Memorized1OrientationState": '',
|
||||
"core:Memorized1PositionState": '',
|
||||
"core:MovingState": ["true","false"],
|
||||
"core:OpenClosedState": ["open","close"],
|
||||
"core:PriorityLockTimerState": '',
|
||||
"core:DiscreteRSSILevelState": ["good","normal","low"],
|
||||
"core:SlateOrientationState": '',
|
||||
"core:StatusState": ["available","unavailable"],
|
||||
"core:TargetClosureState": '',
|
||||
"core:LuminanceState": 'Helligkeit',
|
||||
"core:SmokeState": ["notDetected","detected"],
|
||||
"core:SunEnergyState": '',
|
||||
"core:TemperatureState": '',
|
||||
"io:MaintenanceRadioPartBatteryState": ["full","normal","low","verylow"],
|
||||
"io:MaintenanceSensorPartBatteryState": ["full","normal","low","verylow"],
|
||||
"core:SensorDefectState": ["true","false"]
|
||||
}
|
||||
|
||||
"""
|
||||
"core:TargetClosureState": 'Zielposition',
|
||||
"io:PriorityLockOriginatorState": 'Sperrendes Gerät',
|
||||
"core:ManufacturerDiagnosticsState": 'Diagnose',
|
||||
"core:OpenClosedUnknownState": 'Statusanzeige wenn unbekannt',
|
||||
"core:ManufacturerSettingsState": 'Einstellungen',
|
||||
"core:RSSILevelState": 'RSSI',
|
||||
"core:NameState": 'Name',
|
||||
"core:SecuredPositionState": 'Sicherer Zustand',
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_sensor_type_translation(cls, sensor):
|
||||
return cls.SENSOR_TYPES.get(sensor, sensor)
|
||||
|
||||
@classmethod
|
||||
def get_actor_type_translation(cls, actor_type):
|
||||
return cls.ACTOR_TYPES.get(actor_type, actor_type)
|
||||
|
||||
PARAMETER_TYPES = {
|
||||
0: "bool",
|
||||
1: "integer", # Integer value
|
||||
2: "float", # Floating point number
|
||||
3: "string", # Text string
|
||||
4: "undefined",
|
||||
6: "bool",
|
||||
11: "array"
|
||||
}
|
||||
|
||||
TAHOMA_COMMANDS = {
|
||||
"setClosure": [{"name": "position", "type": "integer", "min": 0, "max": 100}],
|
||||
"setClosure": 'Position',
|
||||
"setClosureAndOrientation": "Position+Neigung",
|
||||
"setOrientation": "Neigung",
|
||||
"up": "Auf",
|
||||
"down": "Zu",
|
||||
"my": "my-Position anfahren",
|
||||
"stop": "Stop",
|
||||
"refresh": "Aktualisieren",
|
||||
"wink": "Winken",
|
||||
"setMyPosition": "my-Position einstellen",
|
||||
"on": "Anschalten",
|
||||
"off": "Ausschalten",
|
||||
"toggle": "Toggle",
|
||||
"setIntensity": "Intensität",
|
||||
"setColor": "Farbe",
|
||||
"setColorTemperature": "Farbtemperatur",
|
||||
"setTargetTemperature": "Solltemperatur",
|
||||
"setMode": "Modus",
|
||||
"pulse": "Impuls",
|
||||
"setLevel": "Wert",
|
||||
"trigger": "Trigger",
|
||||
}
|
||||
# Tahoma Commands mit Parametern
|
||||
TAHOMA_COMMAND_PARAMS = {
|
||||
"setClosure": [{"name": "Position","url":"0", "type": "integer", "min": 0, "max": 100}],
|
||||
"setClosureAndOrientation": [
|
||||
{"name": "position", "type": "integer", "min": 0, "max": 100},
|
||||
{"name": "neigung", "type": "integer", "min": 0, "max": 100}
|
||||
{"name": "Position", "url":"0", "type": "integer", "min": 0, "max": 100},
|
||||
{"name": "Neigung", "url":"1", "type": "integer", "min": 0, "max": 100}
|
||||
],
|
||||
"setOrientation": [{"name": "neigung", "type": "integer", "min": 0, "max": 100}],
|
||||
"setOrientation": [{"name": "Neigung", "url":"0", "type": "integer", "min": 0, "max": 100}],
|
||||
"up": [], "down": [], "my": [], "stop": [], "refresh": [], "wink":[],
|
||||
"setMyPosition": [{"name": "position", "type": "integer", "min": 0, "max": 100}],
|
||||
"setMyPosition": [{"name": "Position", "url":"0", "type": "integer", "min": 0, "max": 100}],
|
||||
"on": [], "off": [], "toggle": [],
|
||||
"setIntensity": [{"name": "helligkeit", "type": "integer", "min": 0, "max": 100}],
|
||||
"setIntensity": [{"name": "Helligkeit", "url":"0", "type": "integer", "min": 0, "max": 100}],
|
||||
"setColor": [
|
||||
{"name": "farbton", "type": "integer", "min": 0, "max": 360},
|
||||
{"name": "sättigung", "type": "integer", "min": 0, "max": 100}
|
||||
{"name": "Farbton", "url":"0", "type": "integer", "min": 0, "max": 360},
|
||||
{"name": "Sättigung", "url":"1", "type": "integer", "min": 0, "max": 100}
|
||||
],
|
||||
"setColorTemperature": [{"name": "farbtemperatur", "type": "integer", "min": 2000, "max": 6500}],
|
||||
"setTargetTemperature": [{"name": "temperatur", "type": "float", "min": 5.0, "max": 30.0}],
|
||||
"setMode": [{"name": "betriebsart", "type": "string"}],
|
||||
"pulse": [{"name": "impuls_dauer", "type": "integer", "min": 1, "max": 3600}],
|
||||
"setLevel": [{"name": "ausgangs_level", "type": "integer", "min": 0, "max": 100}],
|
||||
"setColorTemperature": [{"name": "Farbtemperatur", "url":"0", "type": "integer", "min": 2000, "max": 6500}],
|
||||
"setTargetTemperature": [{"name": "Temperatur", "url":"0", "type": "float", "min": 5.0, "max": 30.0}],
|
||||
"setMode": [{"name": "Betriebsart", "url":"0", "type": "string"}],
|
||||
"pulse": [{"name": "Impulsdauer", "url":"0", "type": "integer", "min": 1, "max": 3600}],
|
||||
"setLevel": [{"name": "Ausgangslevel", "url":"0", "type": "integer", "min": 0, "max": 100}],
|
||||
"trigger": [],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def is_actor(cls, device: Dict) -> bool:
|
||||
"""Prüft ob Gerät ein Aktor ist"""
|
||||
device_type = device.get('controllableName', device.get('uiClass', ''))
|
||||
device_type = device.get('definition').get('uiClass', '')
|
||||
|
||||
if device_type in cls.ACTOR_TYPES:
|
||||
return True
|
||||
@@ -108,7 +227,7 @@ class DeviceClassifier:
|
||||
@classmethod
|
||||
def is_sensor(cls, device: Dict) -> bool:
|
||||
"""Prüft ob Gerät ein Sensor ist"""
|
||||
device_type = device.get('controllableName', device.get('uiClass', ''))
|
||||
device_type = device.get('definition').get('uiClass', '')
|
||||
|
||||
if device_type in cls.SENSOR_TYPES:
|
||||
return True
|
||||
@@ -130,11 +249,13 @@ class DeviceClassifier:
|
||||
cmd_definitions = device.get('definition', {}).get('commands', [])
|
||||
|
||||
for cmd in cmd_definitions:
|
||||
command_name = cmd.get('commandName', '')
|
||||
cmd_params = cls.TAHOMA_COMMANDS.get(command_name, "Not in List")
|
||||
command_url = cmd.get('commandName', '')
|
||||
command_name = cls.TAHOMA_COMMANDS.get(command_url, command_url)
|
||||
cmd_params = cls.TAHOMA_COMMAND_PARAMS.get(command_url, "Not in List")
|
||||
if(cmd_params != "Not in List"): #only append command, if it is on of the listed commands, to prevent flooding the DB with bullshit.
|
||||
command_entry = {
|
||||
'command': command_name,
|
||||
'url': command_url,
|
||||
'parameters': []
|
||||
}
|
||||
|
||||
@@ -149,7 +270,8 @@ class DeviceClassifier:
|
||||
param_detail['max'] = cmd_param['max']
|
||||
if 'values' in cmd_param:
|
||||
param_detail['values'] = cmd_param['values']
|
||||
|
||||
if 'url' in cmd_param:
|
||||
param_detail['url'] = cmd_param['url']
|
||||
if param_detail['name']:
|
||||
command_entry['parameters'].append(param_detail)
|
||||
|
||||
@@ -158,11 +280,15 @@ class DeviceClassifier:
|
||||
# States extrahieren
|
||||
state_definitions = device.get('states', [])
|
||||
for state in state_definitions:
|
||||
state_name = state.get('name', '')
|
||||
state_url = state.get('name', '')
|
||||
state_name = cls.STATE_NAMES.get(state_url, '')
|
||||
state_enums = cls.STATE_ENUMS.get(state_url,'')
|
||||
if state_name:
|
||||
state_entry = {
|
||||
'name': state_name,
|
||||
'type': state.get('type', 0)
|
||||
'url': state_url,
|
||||
'type': cls.PARAMETER_TYPES.get(state.get('type', 4)),
|
||||
'values': state_enums
|
||||
}
|
||||
if 'value' in state:
|
||||
state_entry['current_value'] = state['value']
|
||||
@@ -177,11 +303,15 @@ class DeviceClassifier:
|
||||
|
||||
state_definitions = device.get('states', [])
|
||||
for state in state_definitions:
|
||||
state_name = state.get('name', '')
|
||||
state_url = state.get('name', '')
|
||||
state_name = cls.STATE_NAMES.get(state_url, '')
|
||||
state_enums = cls.STATE_ENUMS.get(state_url,'')
|
||||
if state_name:
|
||||
state_entry = {
|
||||
'name': state_name,
|
||||
'type': state.get('type', 0)
|
||||
'url': state_url,
|
||||
'type': cls.PARAMETER_TYPES.get(state.get('type', 4)),
|
||||
'values': state_enums
|
||||
}
|
||||
if 'value' in state:
|
||||
state_entry['current_value'] = state['value']
|
||||
@@ -254,7 +384,10 @@ class TahomaModule(BaseModule):
|
||||
main_name = main_device.get('label', 'Unbekannt') if main_device else 'Unbekannt'
|
||||
|
||||
for device in group_devices:
|
||||
actor, sensor = self._process_device(device, main_name)
|
||||
if(device.get('label','').startswith("IO (") == False):
|
||||
actor, sensor = self._process_device(device, device.get('label',main_name))
|
||||
else:
|
||||
actor, sensor = self._process_device(device, main_name)
|
||||
if actor:
|
||||
actors.append(actor)
|
||||
if sensor:
|
||||
@@ -280,7 +413,7 @@ class TahomaModule(BaseModule):
|
||||
Tuple (actor_dict or None, sensor_dict or None)
|
||||
"""
|
||||
device_url = device.get('deviceURL', '')
|
||||
device_type = device.get('controllableName', device.get('uiClass', 'Unknown'))
|
||||
device_type = device.get('definition').get('uiClass', 'Unknown')
|
||||
|
||||
is_actor = DeviceClassifier.is_actor(device)
|
||||
is_sensor = DeviceClassifier.is_sensor(device)
|
||||
@@ -290,6 +423,7 @@ class TahomaModule(BaseModule):
|
||||
|
||||
if is_actor:
|
||||
commands, states = DeviceClassifier.extract_actor_data(device)
|
||||
device_type = DeviceClassifier.get_actor_type_translation(device_type)
|
||||
actor = {
|
||||
'type': device_type,
|
||||
'name': device_name,
|
||||
@@ -300,6 +434,7 @@ class TahomaModule(BaseModule):
|
||||
|
||||
elif is_sensor:
|
||||
states = DeviceClassifier.extract_sensor_data(device)
|
||||
device_type = DeviceClassifier.get_sensor_type_translation(device_type)
|
||||
sensor = {
|
||||
'type': device_type,
|
||||
'name': device_name,
|
||||
|
||||
Reference in New Issue
Block a user