#!/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', 'ExteriorScreen', 'Awning', 'Blind', 'GarageDoor', 'Window', 'Light', 'OnOff', 'DimmableLight', 'HeatingSystem', 'Valve', 'Switch', 'Door', 'Curtain', 'VenetianBlind', 'PergolaScreen' } SENSOR_TYPES = { 'TemperatureSensor', 'LightSensor', 'HumiditySensor', 'ContactSensor', 'OccupancySensor', 'SmokeSensor', 'WaterDetectionSensor', 'WindowHandle', 'MotionSensor', 'SunSensor', 'WindSensor', 'RainSensor', 'ConsumptionSensor' } # Tahoma Commands mit Parametern TAHOMA_COMMANDS = { "setClosure": [{"name": "position", "type": "integer", "min": 0, "max": 100}], "setClosureAndOrientation": [ {"name": "position", "type": "integer", "min": 0, "max": 100}, {"name": "neigung", "type": "integer", "min": 0, "max": 100} ], "setOrientation": [{"name": "neigung", "type": "integer", "min": 0, "max": 100}], "up": [], "down": [], "my": [], "stop": [], "refresh": [], "wink":[], "setMyPosition": [{"name": "position", "type": "integer", "min": 0, "max": 100}], "on": [], "off": [], "toggle": [], "setIntensity": [{"name": "helligkeit", "type": "integer", "min": 0, "max": 100}], "setColor": [ {"name": "farbton", "type": "integer", "min": 0, "max": 360}, {"name": "sättigung", "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}], "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', '')) 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('controllableName', device.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_name = cmd.get('commandName', '') cmd_params = cls.TAHOMA_COMMANDS.get(command_name, "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, '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 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_name = state.get('name', '') if state_name: state_entry = { 'name': state_name, 'type': state.get('type', 0) } 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_name = state.get('name', '') if state_name: state_entry = { 'name': state_name, 'type': state.get('type', 0) } 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: 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('controllableName', device.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) actor = { 'type': device_type, 'name': device_name, 'url': device_url, 'commands': commands, 'states': states } elif is_sensor: states = DeviceClassifier.extract_sensor_data(device) sensor = { 'type': device_type, 'name': device_name, 'url': device_url, 'states': states } return actor, sensor