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:
@@ -52,6 +52,7 @@ class BaseModule(ABC):
|
||||
'type': str,
|
||||
'min': float (optional),
|
||||
'max': float (optional),
|
||||
'url': str, # Eindeutige ID/URL
|
||||
'values': list (optional)
|
||||
}
|
||||
]
|
||||
@@ -77,6 +78,7 @@ class BaseModule(ABC):
|
||||
'name': str,
|
||||
'type': int/str,
|
||||
'current_value': any,
|
||||
'url': str, # Eindeutige ID/URL
|
||||
'unit': str (optional)
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
WLED Module
|
||||
Enthält NUR Logik-spezifische Geräte-Discovery Logik
|
||||
KEINE Datenbank-Operationen!
|
||||
"""
|
||||
|
||||
import requests
|
||||
import socket
|
||||
import concurrent.futures
|
||||
import logging
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
from modules.base_module import BaseModule
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LogicModule(BaseModule):
|
||||
"""
|
||||
Logic Modul - Implementiert BaseModule Interface
|
||||
Gibt nur Actors zurück, KEINE DB-Operationen
|
||||
"""
|
||||
|
||||
def is_enabled(self) -> bool:
|
||||
"""Prüft ob Logic aktiviert ist"""
|
||||
return self.config.logic_enable
|
||||
|
||||
def discover(self) -> Tuple[List[Dict], List[Dict]]:
|
||||
"""
|
||||
Erzeugt Einträge für Sensorunabhängige Funktionen (Timer, Sonnenauf/untergang, Uhrzeiten, etc.)
|
||||
|
||||
Returns:
|
||||
Tuple (actors, sensors) - WLED sind immer Actors
|
||||
"""
|
||||
logger.info("\n" + "=" * 60)
|
||||
logger.info("LOGIC-GERÄTE WERDEN ERZEUGT")
|
||||
logger.info("=" * 60)
|
||||
|
||||
actors = []
|
||||
sensors = []
|
||||
|
||||
states = []
|
||||
states.append({
|
||||
'name': 'Uhrzeit',
|
||||
'url': 'time',
|
||||
'type': 'time',
|
||||
'current_value': '13:45'
|
||||
})
|
||||
states.append({
|
||||
'name': 'Datum',
|
||||
'url': 'date',
|
||||
'type': 'date',
|
||||
'current_value': '20.03.2026'
|
||||
})
|
||||
states.append({
|
||||
'name': 'Sonnenaufgang',
|
||||
'url': 'sunrise',
|
||||
'type': 'deltatime',
|
||||
'current_value': '00:00'
|
||||
})
|
||||
states.append({
|
||||
'name': 'Sonnenuntergang',
|
||||
'url': 'sunset',
|
||||
'type': 'deltatime',
|
||||
'current_value': '00:00'
|
||||
})
|
||||
|
||||
sensors.append({'type': 'LOGIC',
|
||||
'name': "Zeitpunkt",
|
||||
'url': f"Logic",
|
||||
'commands': [],
|
||||
'states': states})
|
||||
|
||||
return actors, sensors
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ KEINE Datenbank-Operationen!
|
||||
import json
|
||||
import requests
|
||||
import socket
|
||||
import concurrent.futures
|
||||
import logging
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
from modules.base_module import BaseModule
|
||||
@@ -28,22 +29,47 @@ class ShellyDiscovery:
|
||||
SHELLY_MDNS_SERVICE = "_http._tcp.local."
|
||||
COMMON_PORTS = [80]
|
||||
|
||||
def __init__(self, network_range: str = "192.168.1"):
|
||||
self.network_range = network_range
|
||||
def __init__(self):
|
||||
self.devices = []
|
||||
|
||||
def scan_network(self, start_ip: int = 2, end_ip: int = 254, timeout: float = 0.3) -> List[str]:
|
||||
"""
|
||||
Scannt das Netzwerk nach aktiven Hosts
|
||||
|
||||
def scan_network(self, network: str = None, max_threads: int = 50) -> List[str]:
|
||||
"""Scannt das Netzwerk nach Shelly-Geräten"""
|
||||
if network is None:
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.connect(("8.8.8.8", 80))
|
||||
local_ip = s.getsockname()[0]
|
||||
s.close()
|
||||
network_prefix = '.'.join(local_ip.split('.')[:-1])
|
||||
except:
|
||||
logger.warning("Konnte lokale IP nicht ermitteln, verwende 192.168.1.x")
|
||||
network_prefix = "192.168.1"
|
||||
else:
|
||||
network_prefix = '.'.join(network.split('.')[:3])
|
||||
|
||||
Args:
|
||||
start_ip: Start IP (letztes Oktett)
|
||||
end_ip: End IP (letztes Oktett)
|
||||
timeout: Timeout für Socket-Verbindung
|
||||
logger.info(f"Scanne Netzwerk {network_prefix}.0/24 nach Shelly-Geräten...")
|
||||
|
||||
def check_ip(ip):
|
||||
device = ShellyDiscovery.is_shelly_device(ip)
|
||||
if device:
|
||||
return device
|
||||
return None
|
||||
|
||||
shelly_devices = []
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=max_threads) as executor:
|
||||
futures = [executor.submit(check_ip, f"{network_prefix}.{i}")
|
||||
for i in range(1, 255)]
|
||||
|
||||
Returns:
|
||||
Liste von erreichbaren IP-Adressen
|
||||
"""
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
result = future.result()
|
||||
if result:
|
||||
shelly_devices.append(result)
|
||||
#logger.info(f"Shelly-Gerät gefunden: {result}")
|
||||
|
||||
return shelly_devices
|
||||
"""def scan_network(self, start_ip: int = 2, end_ip: int = 254, timeout: float = 0.3) -> List[str]:
|
||||
|
||||
active_hosts = []
|
||||
logger.info(f"Scanne Netzwerk {self.network_range}.{start_ip}-{end_ip}...")
|
||||
|
||||
@@ -64,8 +90,8 @@ class ShellyDiscovery:
|
||||
|
||||
logger.info(f"{len(active_hosts)} aktive Hosts gefunden")
|
||||
return active_hosts
|
||||
|
||||
def is_shelly_device(self, ip: str) -> Optional[Dict]:
|
||||
"""
|
||||
def is_shelly_device(ip: str) -> Optional[Dict]:
|
||||
"""
|
||||
Prüft ob ein Host ein Shelly-Gerät ist
|
||||
|
||||
@@ -75,7 +101,7 @@ class ShellyDiscovery:
|
||||
Returns:
|
||||
Device Info Dict wenn Shelly, sonst None
|
||||
"""
|
||||
logger.info(f"Suche Shelly Getät unter: {ip}")
|
||||
#logger.info(f"Suche Shelly Getät unter: {ip}")
|
||||
try:
|
||||
# Versuche Gen2 API (neuere Shelly-Geräte)
|
||||
response = requests.get(
|
||||
@@ -96,13 +122,13 @@ class ShellyDiscovery:
|
||||
try:
|
||||
# Versuche Gen1 API (ältere Shelly-Geräte)
|
||||
response = requests.get(
|
||||
f"http://{ip}/shelly",
|
||||
f"http://{ip}/settings",
|
||||
timeout=2
|
||||
)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
if 'type' in data and (data['type'].startswith('SHELLY') or data['type'].startswith('SHSW')):
|
||||
logger.info(f"Shelly Gen1 Gerät gefunden: {ip} - {data.get('type', 'Unknown')}")
|
||||
if 'device' in data and (data['device']['type'].startswith('SHELLY') or data['device']['type'].startswith('SHSW')):
|
||||
logger.info(f"Shelly Gen1 Gerät gefunden: {ip} - {data['device'].get('type', 'Unknown')}")
|
||||
return {
|
||||
'ip': ip,
|
||||
'generation': 1,
|
||||
@@ -146,22 +172,19 @@ class ShellyDiscovery:
|
||||
logger.error(f"Fehler beim Abrufen des Status von {ip}: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def discover_devices(self, start_ip: int = 1, end_ip: int = 254) -> List[Dict]:
|
||||
|
||||
def discover_devices(self) -> List[Dict]:
|
||||
"""
|
||||
Entdeckt alle Shelly-Geräte im Netzwerk
|
||||
|
||||
Returns:
|
||||
Liste von Shelly-Geräten mit Status
|
||||
"""
|
||||
active_hosts = self.scan_network(start_ip, end_ip)
|
||||
self.devices = self.scan_network()
|
||||
|
||||
for ip in active_hosts:
|
||||
device = self.is_shelly_device(ip)
|
||||
if device:
|
||||
status = self.get_device_status(device)
|
||||
device['status'] = status
|
||||
self.devices.append(device)
|
||||
for idx, device in enumerate(self.devices):
|
||||
status = self.get_device_status(device)
|
||||
self.devices[idx]['status'] = status
|
||||
|
||||
logger.info(f"Insgesamt {len(self.devices)} Shelly-Geräte entdeckt")
|
||||
return self.devices
|
||||
@@ -197,11 +220,8 @@ class ShellyModule(BaseModule):
|
||||
sensors = []
|
||||
|
||||
try:
|
||||
discovery = ShellyDiscovery(network_range=self.config.shelly_network_range)
|
||||
devices = discovery.discover_devices(
|
||||
start_ip=self.config.shelly_start_ip,
|
||||
end_ip=self.config.shelly_end_ip
|
||||
)
|
||||
discovery = ShellyDiscovery()
|
||||
devices = discovery.discover_devices()
|
||||
|
||||
if not devices:
|
||||
logger.info("Keine Shelly-Geräte gefunden")
|
||||
@@ -266,7 +286,7 @@ class ShellyModule(BaseModule):
|
||||
temp_data = status.get(f'temperature:{i}', {})
|
||||
|
||||
sensors.append({
|
||||
'type': 'ShellyTemperatureSensor',
|
||||
'type': 'Temperatur',
|
||||
'name': f"{device_name}_Temp_{i}",
|
||||
'url': f"http://{ip}/rpc/Temperature.GetStatus?id={i}",
|
||||
'states': [
|
||||
@@ -274,6 +294,7 @@ class ShellyModule(BaseModule):
|
||||
'name': 'temperature',
|
||||
'type': 'number',
|
||||
'current_value': temp_data.get('tC'),
|
||||
'url': f"tC",
|
||||
'unit': '°C'
|
||||
}
|
||||
]
|
||||
@@ -283,7 +304,7 @@ class ShellyModule(BaseModule):
|
||||
for i in range(em_count):
|
||||
em_data = status.get(f'em:{i}', {})
|
||||
sensors.append({
|
||||
'type': 'ShellyEnergyMeter',
|
||||
'type': 'Stromzähler',
|
||||
'name': f"{device_name}_EM_{i}",
|
||||
'url': f"http://{ip}/rpc/em.GetStatus?id={i}",
|
||||
'states': []
|
||||
@@ -413,7 +434,7 @@ class ShellyModule(BaseModule):
|
||||
ip = device['ip']
|
||||
|
||||
device_name = info.get('name', f"Shelly_{info.get('type', ip)}")
|
||||
device_type = info.get('type', 'Unknown')
|
||||
device_type = info['device'].get('type', 'Unknown')
|
||||
|
||||
# Relays als Aktoren
|
||||
relays = status.get('relays', [])
|
||||
@@ -440,7 +461,7 @@ class ShellyModule(BaseModule):
|
||||
temp_data = status.get('tmp', {})
|
||||
if temp_data and 'tC' in temp_data:
|
||||
sensors.append({
|
||||
'type': 'ShellyTemperatureSensor',
|
||||
'type': 'Temperatur',
|
||||
'name': f"{device_name}_Temp",
|
||||
'url': f"http://{ip}/status",
|
||||
'states': [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -106,7 +106,7 @@ class WLEDDiscovery:
|
||||
return wled_devices
|
||||
|
||||
@staticmethod
|
||||
def is_wled_device(ip: str, timeout: float = 1.0) -> bool:
|
||||
def is_wled_device(ip: str, timeout: float = 2.0) -> bool:
|
||||
"""Prüft ob IP ein WLED-Gerät ist"""
|
||||
try:
|
||||
response = requests.get(
|
||||
@@ -190,16 +190,16 @@ class WLEDAPI:
|
||||
eff_values = self.get_effects()
|
||||
|
||||
commands = [
|
||||
{'command': 'on', 'parameters': []},
|
||||
{'command': 'off', 'parameters': []},
|
||||
{'command': 'An', 'url': '{"on":true}', 'parameters': []},
|
||||
{'command': 'Aus', 'url': '{"on":false}', 'parameters': []},
|
||||
{
|
||||
'command': 'setBrightness',
|
||||
'command': 'Helligkeit', 'url': '{"bri":%brightness%}',
|
||||
'parameters': [
|
||||
{'name': 'brightness', 'type': 'integer', 'min': 0, 'max': 255}
|
||||
]
|
||||
},
|
||||
{
|
||||
'command': 'setColor',
|
||||
'command': 'Farbe', 'url': '{"seg":[{"col":[[%red%,%green%,%blue%]]}]}',
|
||||
'parameters': [
|
||||
{'name': 'red', 'type': 'integer', 'min': 0, 'max': 255},
|
||||
{'name': 'green', 'type': 'integer', 'min': 0, 'max': 255},
|
||||
@@ -207,13 +207,13 @@ class WLEDAPI:
|
||||
]
|
||||
},
|
||||
{
|
||||
'command': 'setEffect',
|
||||
'command': 'Effekt', 'url': '{"seg":[{"fx":%effect%}]}',
|
||||
'parameters': [
|
||||
{'name': 'effect', 'type': 'integer', 'min': 0, 'max': 255, 'values': eff_values}
|
||||
]
|
||||
},
|
||||
{
|
||||
'command': 'setPreset',
|
||||
'command': 'Preset', 'url': '{"ps":%preset%}',
|
||||
'parameters': [
|
||||
{'name': 'preset', 'type': 'integer', 'min': 1, 'max': 250, 'values': preset_values}
|
||||
]
|
||||
@@ -223,12 +223,14 @@ class WLEDAPI:
|
||||
states = []
|
||||
if state:
|
||||
states.append({
|
||||
'name': 'power',
|
||||
'name': 'An',
|
||||
'url': 'on',
|
||||
'type': 'boolean',
|
||||
'current_value': state.get('on', False)
|
||||
})
|
||||
states.append({
|
||||
'name': 'brightness',
|
||||
'name': 'Helligkeit',
|
||||
'url': 'bri',
|
||||
'type': 'integer',
|
||||
'current_value': state.get('bri', 0)
|
||||
})
|
||||
@@ -238,7 +240,8 @@ class WLEDAPI:
|
||||
colors = segments[0].get('col', [[0,0,0]])
|
||||
if colors and len(colors) > 0:
|
||||
states.append({
|
||||
'name': 'color_rgb',
|
||||
'name': 'Farbe',
|
||||
'url': 'seg[0].col[0]',
|
||||
'type': 'array',
|
||||
'current_value': colors[0]
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user