#!/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