Zeilenenden vereinheitlichen: LF ueberall ausser im Fremdcode

Bisher entschied jede Arbeitsstation fuer sich, welche Zeilenenden ein
Commit bekommt: Git fuer Windows setzt core.autocrlf=input in seiner
System-Konfiguration, die NAS setzt nichts. Weil der Bestand ausserdem in
sich gemischt war - 34 PHP-Dateien mit LF, 30 mit CRLF -, gab es gar kein
richtiges Zeilenende, an das ein Werkzeug sich haette halten koennen. Jede
Ergaenzung mit LF in einer CRLF-Datei ergab eine gemischte Datei; helper.php
und js/solar/homeMQTT.js waren bereits so entstanden.

.gitattributes macht die Zeilenenden zur Eigenschaft des Repositorys. LF,
weil dieses Verzeichnis der Web-Ordner der NAS ist und von Linux
ausgeliefert wird - und weil Git fuer Windows mit "input" ohnehin schon
dorthin zeigt.

restricted/WebAuthn bleibt ausgenommen und damit byteweise so, wie die
Bibliothek geliefert wurde; darunter liegen 292 Zertifikate.

Von den 47 geaenderten Dateien wurde nachgerechnet keine einzige im Inhalt
angefasst - der Vergleich HEAD-ohne-CR gegen Index ist ueberall gleich.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 23:04:00 +02:00
co-authored by Claude Opus 5
parent abdf6880eb
commit 93fb054b66
48 changed files with 13209 additions and 13192 deletions
@@ -1,445 +1,445 @@
#!/usr/bin/env python3
"""
Tahoma Module
Enthält NUR Tahoma-spezifische Geräte-Discovery Logik
KEINE Datenbank-Operationen!
"""
import requests
import urllib3
import re
import logging
from typing import List, Dict, Optional, Tuple
from modules.base_module import BaseModule
# SSL-Warnungen deaktivieren
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
logger = logging.getLogger(__name__)
class TahomaAPI:
"""Original TahomaAPI Klasse - unverändert"""
def __init__(self, gateway_ip: str, api_token: str):
self.base_url = f"https://{gateway_ip}:8443/enduser-mobile-web/1/enduserAPI"
self.headers = {
"Authorization": f"Bearer {api_token}",
"Content-Type": "application/json"
}
def get_setup(self) -> Optional[Dict]:
try:
url = f"{self.base_url}/setup"
response = requests.get(url, headers=self.headers, verify=False, timeout=10)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
logger.error(f"Fehler beim Abrufen der Setup-Daten: {e}")
return None
def get_devices(self) -> List[Dict]:
setup = self.get_setup()
if not setup:
return []
devices = setup.get('devices', [])
logger.info(f"{len(devices)} Tahoma-Geräte gefunden")
return devices
class DeviceClassifier:
"""Original DeviceClassifier - unverändert"""
ACTOR_TYPES = {
'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': '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'
}
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": '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", "url":"0", "type": "integer", "min": 0, "max": 100},
{"name": "Neigung", "url":"1", "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", "url":"0", "type": "integer", "min": 0, "max": 100}],
"on": [], "off": [], "toggle": [],
"setIntensity": [{"name": "Helligkeit", "url":"0", "type": "integer", "min": 0, "max": 100}],
"setColor": [
{"name": "Farbton", "url":"0", "type": "integer", "min": 0, "max": 360},
{"name": "Sättigung", "url":"1", "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('definition').get('uiClass', '')
if device_type in cls.ACTOR_TYPES:
return True
commands = device.get('definition', {}).get('commands', [])
if commands:
command_names = [cmd.get('commandName', '') for cmd in commands]
actor_commands = {'open', 'close', 'on', 'off', 'up', 'down', 'setPosition', 'dim'}
if any(cmd in actor_commands for cmd in command_names):
return True
return False
@classmethod
def is_sensor(cls, device: Dict) -> bool:
"""Prüft ob Gerät ein Sensor ist"""
device_type = device.get('definition').get('uiClass', '')
if device_type in cls.SENSOR_TYPES:
return True
states = device.get('states', [])
commands = device.get('definition', {}).get('commands', [])
if states and len(states) > 0 and len(commands) <= 1:
return True
return False
@classmethod
def extract_actor_data(cls, device: Dict) -> tuple:
"""Extrahiert Commands und States aus Aktor"""
commands = []
states = []
cmd_definitions = device.get('definition', {}).get('commands', [])
for cmd in cmd_definitions:
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': []
}
for cmd_param in cmd_params:
param_detail = {'name': cmd_param.get('name', '')}
if 'type' in cmd_param:
param_detail['type'] = cmd_param['type']
if 'min' in cmd_param:
param_detail['min'] = cmd_param['min']
if 'max' in cmd_param:
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)
commands.append(command_entry)
# States extrahieren
state_definitions = device.get('states', [])
for state in state_definitions:
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,
'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']
states.append(state_entry)
return commands, states
@classmethod
def extract_sensor_data(cls, device: Dict) -> list:
"""Extrahiert States aus Sensor"""
states = []
state_definitions = device.get('states', [])
for state in state_definitions:
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,
'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']
states.append(state_entry)
return states
class TahomaModule(BaseModule):
"""
Tahoma Modul - Implementiert BaseModule Interface
Gibt nur Actors/Sensors zurück, KEINE DB-Operationen
"""
def is_enabled(self) -> bool:
"""Prüft ob Tahoma aktiviert ist"""
return (self.config.tahoma_enable and
self.config.tahoma_ip and
self.config.tahoma_token)
def discover(self) -> Tuple[List[Dict], List[Dict]]:
"""
Führt Tahoma Discovery durch
Returns:
Tuple (actors, sensors) - Listen von Dicts im vereinheitlichten Format
"""
logger.info("\n" + "=" * 60)
logger.info("TAHOMA-GERÄTE WERDEN ABGERUFEN")
logger.info("=" * 60)
actors = []
sensors = []
# TahomaAPI initialisieren
tahoma = TahomaAPI(self.config.tahoma_ip, self.config.tahoma_token)
devices = tahoma.get_devices()
if not devices:
logger.warning("Keine Tahoma-Geräte gefunden")
return actors, sensors
# Geräte gruppieren (Original-Logik)
device_groups = {}
standalone_devices = []
for device in devices:
device_url = device.get('deviceURL', '')
match = re.match(r'(.+)#(\d+)$', device_url)
if match:
base_url = match.group(1)
if base_url not in device_groups:
device_groups[base_url] = []
device_groups[base_url].append(device)
else:
standalone_devices.append(device)
# Gruppierte Geräte verarbeiten
for base_url, group_devices in device_groups.items():
main_device = None
for dev in group_devices:
if dev.get('deviceURL', '').endswith('#1'):
main_device = dev
break
if not main_device and group_devices:
main_device = group_devices[0]
main_name = main_device.get('label', 'Unbekannt') if main_device else 'Unbekannt'
for device in group_devices:
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:
sensors.append(sensor)
# Standalone Geräte verarbeiten
for device in standalone_devices:
device_name = device.get('label', 'Unbekannt')
actor, sensor = self._process_device(device, device_name)
if actor:
actors.append(actor)
if sensor:
sensors.append(sensor)
logger.info(f"Tahoma: {len(actors)} Aktoren, {len(sensors)} Sensoren gefunden")
return actors, sensors
def _process_device(self, device: Dict, device_name: str) -> Tuple[Optional[Dict], Optional[Dict]]:
"""
Verarbeitet ein einzelnes Gerät
Returns:
Tuple (actor_dict or None, sensor_dict or None)
"""
device_url = device.get('deviceURL', '')
device_type = device.get('definition').get('uiClass', 'Unknown')
is_actor = DeviceClassifier.is_actor(device)
is_sensor = DeviceClassifier.is_sensor(device)
actor = None
sensor = None
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,
'url': device_url,
'commands': commands,
'states': states
}
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,
'url': device_url,
'states': states
}
return actor, sensor
#!/usr/bin/env python3
"""
Tahoma Module
Enthält NUR Tahoma-spezifische Geräte-Discovery Logik
KEINE Datenbank-Operationen!
"""
import requests
import urllib3
import re
import logging
from typing import List, Dict, Optional, Tuple
from modules.base_module import BaseModule
# SSL-Warnungen deaktivieren
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
logger = logging.getLogger(__name__)
class TahomaAPI:
"""Original TahomaAPI Klasse - unverändert"""
def __init__(self, gateway_ip: str, api_token: str):
self.base_url = f"https://{gateway_ip}:8443/enduser-mobile-web/1/enduserAPI"
self.headers = {
"Authorization": f"Bearer {api_token}",
"Content-Type": "application/json"
}
def get_setup(self) -> Optional[Dict]:
try:
url = f"{self.base_url}/setup"
response = requests.get(url, headers=self.headers, verify=False, timeout=10)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
logger.error(f"Fehler beim Abrufen der Setup-Daten: {e}")
return None
def get_devices(self) -> List[Dict]:
setup = self.get_setup()
if not setup:
return []
devices = setup.get('devices', [])
logger.info(f"{len(devices)} Tahoma-Geräte gefunden")
return devices
class DeviceClassifier:
"""Original DeviceClassifier - unverändert"""
ACTOR_TYPES = {
'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': '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'
}
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": '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", "url":"0", "type": "integer", "min": 0, "max": 100},
{"name": "Neigung", "url":"1", "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", "url":"0", "type": "integer", "min": 0, "max": 100}],
"on": [], "off": [], "toggle": [],
"setIntensity": [{"name": "Helligkeit", "url":"0", "type": "integer", "min": 0, "max": 100}],
"setColor": [
{"name": "Farbton", "url":"0", "type": "integer", "min": 0, "max": 360},
{"name": "Sättigung", "url":"1", "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('definition').get('uiClass', '')
if device_type in cls.ACTOR_TYPES:
return True
commands = device.get('definition', {}).get('commands', [])
if commands:
command_names = [cmd.get('commandName', '') for cmd in commands]
actor_commands = {'open', 'close', 'on', 'off', 'up', 'down', 'setPosition', 'dim'}
if any(cmd in actor_commands for cmd in command_names):
return True
return False
@classmethod
def is_sensor(cls, device: Dict) -> bool:
"""Prüft ob Gerät ein Sensor ist"""
device_type = device.get('definition').get('uiClass', '')
if device_type in cls.SENSOR_TYPES:
return True
states = device.get('states', [])
commands = device.get('definition', {}).get('commands', [])
if states and len(states) > 0 and len(commands) <= 1:
return True
return False
@classmethod
def extract_actor_data(cls, device: Dict) -> tuple:
"""Extrahiert Commands und States aus Aktor"""
commands = []
states = []
cmd_definitions = device.get('definition', {}).get('commands', [])
for cmd in cmd_definitions:
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': []
}
for cmd_param in cmd_params:
param_detail = {'name': cmd_param.get('name', '')}
if 'type' in cmd_param:
param_detail['type'] = cmd_param['type']
if 'min' in cmd_param:
param_detail['min'] = cmd_param['min']
if 'max' in cmd_param:
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)
commands.append(command_entry)
# States extrahieren
state_definitions = device.get('states', [])
for state in state_definitions:
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,
'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']
states.append(state_entry)
return commands, states
@classmethod
def extract_sensor_data(cls, device: Dict) -> list:
"""Extrahiert States aus Sensor"""
states = []
state_definitions = device.get('states', [])
for state in state_definitions:
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,
'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']
states.append(state_entry)
return states
class TahomaModule(BaseModule):
"""
Tahoma Modul - Implementiert BaseModule Interface
Gibt nur Actors/Sensors zurück, KEINE DB-Operationen
"""
def is_enabled(self) -> bool:
"""Prüft ob Tahoma aktiviert ist"""
return (self.config.tahoma_enable and
self.config.tahoma_ip and
self.config.tahoma_token)
def discover(self) -> Tuple[List[Dict], List[Dict]]:
"""
Führt Tahoma Discovery durch
Returns:
Tuple (actors, sensors) - Listen von Dicts im vereinheitlichten Format
"""
logger.info("\n" + "=" * 60)
logger.info("TAHOMA-GERÄTE WERDEN ABGERUFEN")
logger.info("=" * 60)
actors = []
sensors = []
# TahomaAPI initialisieren
tahoma = TahomaAPI(self.config.tahoma_ip, self.config.tahoma_token)
devices = tahoma.get_devices()
if not devices:
logger.warning("Keine Tahoma-Geräte gefunden")
return actors, sensors
# Geräte gruppieren (Original-Logik)
device_groups = {}
standalone_devices = []
for device in devices:
device_url = device.get('deviceURL', '')
match = re.match(r'(.+)#(\d+)$', device_url)
if match:
base_url = match.group(1)
if base_url not in device_groups:
device_groups[base_url] = []
device_groups[base_url].append(device)
else:
standalone_devices.append(device)
# Gruppierte Geräte verarbeiten
for base_url, group_devices in device_groups.items():
main_device = None
for dev in group_devices:
if dev.get('deviceURL', '').endswith('#1'):
main_device = dev
break
if not main_device and group_devices:
main_device = group_devices[0]
main_name = main_device.get('label', 'Unbekannt') if main_device else 'Unbekannt'
for device in group_devices:
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:
sensors.append(sensor)
# Standalone Geräte verarbeiten
for device in standalone_devices:
device_name = device.get('label', 'Unbekannt')
actor, sensor = self._process_device(device, device_name)
if actor:
actors.append(actor)
if sensor:
sensors.append(sensor)
logger.info(f"Tahoma: {len(actors)} Aktoren, {len(sensors)} Sensoren gefunden")
return actors, sensors
def _process_device(self, device: Dict, device_name: str) -> Tuple[Optional[Dict], Optional[Dict]]:
"""
Verarbeitet ein einzelnes Gerät
Returns:
Tuple (actor_dict or None, sensor_dict or None)
"""
device_url = device.get('deviceURL', '')
device_type = device.get('definition').get('uiClass', 'Unknown')
is_actor = DeviceClassifier.is_actor(device)
is_sensor = DeviceClassifier.is_sensor(device)
actor = None
sensor = None
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,
'url': device_url,
'commands': commands,
'states': states
}
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,
'url': device_url,
'states': states
}
return actor, sensor