diff --git a/README.md b/README.md index 72ed2ac..a72db91 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,245 @@ -# Smart-Dashboard +# Normalisierte Datenbankstruktur für Somfy Tahoma +## Übersicht + +Die Datenbank wurde von einer denormalisierten Struktur (mit JSON in `parameters`) +in eine vollständig normalisierte relationale Struktur überführt. + +## Datenbankschema + +### Haupttabellen + +#### `actors` +Speichert alle Aktoren (Geräte mit Steuerungsfunktion) + +| Spalte | Typ | Beschreibung | +|--------|-----|--------------| +| id | INT (PK, AUTO_INCREMENT) | Eindeutige ID | +| type | VARCHAR(50) | Gerätetyp (z.B. RollerShutter) | +| name | VARCHAR(70) | Name des Geräts | +| parameters | TEXT (nullable) | Optionale Meta-Informationen | +| url | VARCHAR(100) UNIQUE | Tahoma Device URL | + +#### `sensors` +Speichert alle Sensoren (Geräte die Werte melden) + +| Spalte | Typ | Beschreibung | +|--------|-----|--------------| +| id | INT (PK, AUTO_INCREMENT) | Eindeutige ID | +| type | VARCHAR(50) | Sensortyp (z.B. TemperatureSensor) | +| name | VARCHAR(70) | Name des Sensors | +| parameters | TEXT (nullable) | Optionale Meta-Informationen | +| url | VARCHAR(100) UNIQUE | Tahoma Device URL | + +### Aktor-Detailtabellen + +#### `actor_commands` +Speichert alle verfügbaren Commands für jeden Aktor + +| Spalte | Typ | Beschreibung | +|--------|-----|--------------| +| id | INT (PK, AUTO_INCREMENT) | Eindeutige ID | +| actor_id | INT (FK → actors.id) | Referenz zum Aktor | +| command_name | VARCHAR(100) | Name des Commands (z.B. setPosition, open) | + +**Beispieldaten:** +``` +actor_id | command_name +---------|------------- +1 | open +1 | close +1 | setPosition +2 | on +2 | off +``` + +#### `command_parameters` +Speichert die Parameter für jeden Command + +| Spalte | Typ | Beschreibung | +|--------|-----|--------------| +| id | INT (PK, AUTO_INCREMENT) | Eindeutige ID | +| command_id | INT (FK → actor_commands.id) | Referenz zum Command | +| parameter_name | VARCHAR(100) | Name des Parameters (z.B. position) | +| parameter_type | VARCHAR(50) | Datentyp (z.B. integer, string) | +| min_value | DECIMAL(10,2) | Minimaler Wert (nullable) | +| max_value | DECIMAL(10,2) | Maximaler Wert (nullable) | +| possible_values | TEXT | JSON Array mit möglichen Werten (nullable) | + +**Beispieldaten:** +``` +command_id | parameter_name | parameter_type | min_value | max_value +-----------|----------------|----------------|-----------|---------- +3 | position | integer | 0 | 100 +``` + +#### `actor_states` +Speichert die aktuellen States von Aktoren + +| Spalte | Typ | Beschreibung | +|--------|-----|--------------| +| id | INT (PK, AUTO_INCREMENT) | Eindeutige ID | +| actor_id | INT (FK → actors.id) | Referenz zum Aktor | +| state_name | VARCHAR(100) | Name des State (z.B. core:ClosureState) | +| state_type | INT | State-Typ Code aus Tahoma API | +| current_value | VARCHAR(255) | Aktueller Wert | +| unit | VARCHAR(20) | Einheit (nullable) | +| last_updated | TIMESTAMP | Zeitpunkt der letzten Aktualisierung | + +### Sensor-Detailtabellen + +#### `sensor_states` +Speichert alle verfügbaren States für jeden Sensor + +| Spalte | Typ | Beschreibung | +|--------|-----|--------------| +| id | INT (PK, AUTO_INCREMENT) | Eindeutige ID | +| sensor_id | INT (FK → sensors.id) | Referenz zum Sensor | +| state_name | VARCHAR(100) | Name des State (z.B. core:TemperatureState) | +| state_type | INT | State-Typ Code aus Tahoma API | +| current_value | VARCHAR(255) | Aktueller Wert | +| unit | VARCHAR(20) | Einheit (z.B. °C, %) (nullable) | +| last_updated | TIMESTAMP | Zeitpunkt der letzten Aktualisierung | + +**Beispieldaten:** +``` +sensor_id | state_name | state_type | current_value | unit +----------|-------------------------|------------|---------------|------ +1 | core:TemperatureState | 1 | 21.5 | °C +2 | core:LuminanceState | 1 | 350 | lux +``` + +## Beziehungen (Foreign Keys) + +``` +actors (1) ──< (N) actor_commands + └──< (N) command_parameters + +actors (1) ──< (N) actor_states + +sensors (1) ──< (N) sensor_states +``` + +Alle Foreign Keys mit `ON DELETE CASCADE` → Wenn ein Aktor/Sensor gelöscht wird, +werden automatisch alle zugehörigen Commands, Parameter und States gelöscht. + +## Hilfreiche Views + +### `view_actors_with_commands` +Zeigt alle Aktoren mit ihren Commands und Parametern in einer flachen Ansicht + +```sql +SELECT * FROM view_actors_with_commands WHERE actor_name = 'Wohnzimmer Rollo'; +``` + +### `view_sensors_with_states` +Zeigt alle Sensoren mit ihren aktuellen States + +```sql +SELECT * FROM view_sensors_with_states WHERE sensor_type = 'TemperatureSensor'; +``` + +### `view_all_devices` +Zeigt eine Übersicht aller Geräte (Aktoren und Sensoren) + +```sql +SELECT * FROM view_all_devices ORDER BY name; +``` + +## Beispiel-Queries + +### Alle Commands eines bestimmten Aktors anzeigen +```sql +SELECT + a.name as aktor_name, + ac.command_name, + cp.parameter_name, + cp.min_value, + cp.max_value +FROM actors a +JOIN actor_commands ac ON a.id = ac.actor_id +LEFT JOIN command_parameters cp ON ac.id = cp.command_id +WHERE a.name = 'Wohnzimmer Rollo'; +``` + +### Alle Temperatursensoren mit aktuellem Wert +```sql +SELECT + s.name as sensor_name, + ss.current_value as temperatur, + ss.unit, + ss.last_updated +FROM sensors s +JOIN sensor_states ss ON s.id = ss.sensor_id +WHERE s.type = 'TemperatureSensor' + AND ss.state_name LIKE '%Temperature%'; +``` + +### Alle Aktoren eines bestimmten Typs +```sql +SELECT + name, + type, + COUNT(DISTINCT ac.id) as anzahl_commands +FROM actors a +LEFT JOIN actor_commands ac ON a.id = ac.actor_id +WHERE a.type = 'RollerShutter' +GROUP BY a.id, a.name, a.type; +``` + +### Commands ohne Parameter finden +```sql +SELECT + a.name as aktor, + ac.command_name +FROM actors a +JOIN actor_commands ac ON a.id = ac.actor_id +LEFT JOIN command_parameters cp ON ac.id = cp.command_id +WHERE cp.id IS NULL; +``` + +## Vorteile der normalisierten Struktur + +1. **Keine Datenduplizierung**: Jeder Command und Parameter wird nur einmal gespeichert +2. **Einfache Queries**: SQL-Joins statt JSON-Parsing +3. **Flexible Erweiterung**: Neue Spalten können einfach hinzugefügt werden +4. **Referentielle Integrität**: Foreign Keys garantieren Konsistenz +5. **Performance**: Indizes auf relevanten Spalten für schnelle Suchen +6. **Typsicherheit**: Min/Max als DECIMAL statt String + +## Migration von alter zu neuer Struktur + +Falls Sie bereits Daten in der alten Struktur haben: + +```sql +-- Backup erstellen +CREATE TABLE actors_old AS SELECT * FROM actors; +CREATE TABLE sensors_old AS SELECT * FROM sensors; + +-- Alte Tabellen löschen +DROP TABLE actors; +DROP TABLE sensors; + +-- Neue Struktur erstellen (database_schema.sql ausführen) +SOURCE database_schema.sql; + +-- Python-Script ausführen um Daten neu zu importieren +``` + +## Wartung + +### Regelmäßige Aktualisierung der States +Das Script kann regelmäßig ausgeführt werden. Bei `CLEAR_TABLES = True` werden +alle Daten neu importiert. Bei `CLEAR_TABLES = False` können Updates implementiert werden. + +### Veraltete Geräte entfernen +```sql +-- Geräte finden die nicht mehr in der Tahoma Box vorhanden sind +-- (nach erneutem Import) +``` + +### Index-Optimierung prüfen +```sql +SHOW INDEX FROM actors; +SHOW INDEX FROM actor_commands; +``` diff --git a/database_schema.sql b/database_schema.sql new file mode 100644 index 0000000..cb8315e --- /dev/null +++ b/database_schema.sql @@ -0,0 +1,181 @@ +-- ============================================================================ +-- Somfy Tahoma Datenbank Schema +-- Normalisierte Struktur für Aktoren, Sensoren und ihre Parameter +-- ============================================================================ + +-- Datenbank erstellen (falls noch nicht vorhanden) +-- CREATE DATABASE IF NOT EXISTS EnergyFlow CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +-- USE EnergyFlow; + +-- ============================================================================ +-- HAUPTTABELLEN +-- ============================================================================ + +-- Tabelle: actors +-- Speichert alle Aktoren (Geräte mit Steuerungsfunktion) +CREATE TABLE IF NOT EXISTS `actors` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `type` varchar(50) NOT NULL COMMENT 'Gerätetyp z.B. RollerShutter', + `name` varchar(70) NOT NULL COMMENT 'Name des Geräts', + `parameters` text DEFAULT NULL COMMENT 'Zusätzliche Meta-Informationen als JSON', + `url` varchar(100) NOT NULL COMMENT 'Tahoma Device URL', + PRIMARY KEY (`id`), + UNIQUE KEY `url` (`url`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- Tabelle: sensors +-- Speichert alle Sensoren (Geräte die Werte melden) +CREATE TABLE IF NOT EXISTS `sensors` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `type` varchar(50) NOT NULL COMMENT 'Sensortyp z.B. TemperatureSensor', + `name` varchar(70) NOT NULL COMMENT 'Name des Sensors', + `parameters` text DEFAULT NULL COMMENT 'Zusätzliche Meta-Informationen als JSON', + `url` varchar(100) NOT NULL COMMENT 'Tahoma Device URL', + PRIMARY KEY (`id`), + UNIQUE KEY `url` (`url`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ============================================================================ +-- AKTOR-BEZOGENE TABELLEN +-- ============================================================================ + +-- Tabelle: actor_commands +-- Speichert alle verfügbaren Commands für jeden Aktor +CREATE TABLE IF NOT EXISTS `actor_commands` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `actor_id` int(11) NOT NULL COMMENT 'Referenz zum Aktor', + `command_name` varchar(100) NOT NULL COMMENT 'Name des Commands z.B. setPosition, open, close', + PRIMARY KEY (`id`), + KEY `actor_id` (`actor_id`), + CONSTRAINT `fk_actor_commands_actor` + FOREIGN KEY (`actor_id`) REFERENCES `actors`(`id`) + ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- Tabelle: command_parameters +-- Speichert die Parameter für jeden Command +CREATE TABLE IF NOT EXISTS `command_parameters` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `command_id` int(11) NOT NULL COMMENT 'Referenz zum Command', + `parameter_name` varchar(100) NOT NULL COMMENT 'Name des Parameters z.B. position', + `parameter_type` varchar(50) DEFAULT NULL COMMENT 'Datentyp z.B. integer, string', + `min_value` decimal(10,2) DEFAULT NULL COMMENT 'Minimaler Wert (falls numerisch)', + `max_value` decimal(10,2) DEFAULT NULL COMMENT 'Maximaler Wert (falls numerisch)', + `possible_values` text DEFAULT NULL COMMENT 'JSON Array mit möglichen Werten (für Enums)', + PRIMARY KEY (`id`), + KEY `command_id` (`command_id`), + CONSTRAINT `fk_command_parameters_command` + FOREIGN KEY (`command_id`) REFERENCES `actor_commands`(`id`) + ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ============================================================================ +-- SENSOR-BEZOGENE TABELLEN +-- ============================================================================ + +-- Tabelle: sensor_states +-- Speichert alle verfügbaren States für jeden Sensor +CREATE TABLE IF NOT EXISTS `sensor_states` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `sensor_id` int(11) NOT NULL COMMENT 'Referenz zum Sensor', + `state_name` varchar(100) NOT NULL COMMENT 'Name des State z.B. core:TemperatureState', + `state_type` int(11) DEFAULT NULL COMMENT 'State-Typ Code aus Tahoma API', + `current_value` varchar(255) DEFAULT NULL COMMENT 'Aktueller Wert des State', + `unit` varchar(20) DEFAULT NULL COMMENT 'Einheit z.B. °C, %, lux', + `last_updated` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `sensor_id` (`sensor_id`), + CONSTRAINT `fk_sensor_states_sensor` + FOREIGN KEY (`sensor_id`) REFERENCES `sensors`(`id`) + ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ============================================================================ +-- GEMEINSAME TABELLE FÜR ACTOR STATES (optional) +-- ============================================================================ + +-- Tabelle: actor_states +-- Speichert die aktuellen States von Aktoren (z.B. aktuelle Position) +CREATE TABLE IF NOT EXISTS `actor_states` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `actor_id` int(11) NOT NULL COMMENT 'Referenz zum Aktor', + `state_name` varchar(100) NOT NULL COMMENT 'Name des State z.B. core:ClosureState', + `state_type` int(11) DEFAULT NULL COMMENT 'State-Typ Code aus Tahoma API', + `current_value` varchar(255) DEFAULT NULL COMMENT 'Aktueller Wert des State', + `unit` varchar(20) DEFAULT NULL COMMENT 'Einheit falls vorhanden', + `last_updated` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `actor_id` (`actor_id`), + CONSTRAINT `fk_actor_states_actor` + FOREIGN KEY (`actor_id`) REFERENCES `actors`(`id`) + ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ============================================================================ +-- INDIZES FÜR PERFORMANCE +-- ============================================================================ + +-- Zusätzliche Indizes für häufige Queries +CREATE INDEX idx_actors_type ON actors(type); +CREATE INDEX idx_sensors_type ON sensors(type); +CREATE INDEX idx_actor_commands_name ON actor_commands(command_name); +CREATE INDEX idx_sensor_states_name ON sensor_states(state_name); +CREATE INDEX idx_actor_states_name ON actor_states(state_name); + +-- ============================================================================ +-- VIEWS (optional - für einfachere Queries) +-- ============================================================================ + +-- View: Alle Aktoren mit ihren Commands +CREATE OR REPLACE VIEW view_actors_with_commands AS +SELECT + a.id as actor_id, + a.name as actor_name, + a.type as actor_type, + a.url as actor_url, + ac.id as command_id, + ac.command_name, + cp.parameter_name, + cp.parameter_type, + cp.min_value, + cp.max_value, + cp.possible_values +FROM actors a +LEFT JOIN actor_commands ac ON a.id = ac.actor_id +LEFT JOIN command_parameters cp ON ac.id = cp.command_id +ORDER BY a.id, ac.id, cp.id; + +-- View: Alle Sensoren mit ihren States +CREATE OR REPLACE VIEW view_sensors_with_states AS +SELECT + s.id as sensor_id, + s.name as sensor_name, + s.type as sensor_type, + s.url as sensor_url, + ss.state_name, + ss.state_type, + ss.current_value, + ss.unit, + ss.last_updated +FROM sensors s +LEFT JOIN sensor_states ss ON s.id = ss.sensor_id +ORDER BY s.id, ss.id; + +-- View: Übersicht aller Geräte +CREATE OR REPLACE VIEW view_all_devices AS +SELECT + 'actor' as device_category, + id, + type, + name, + url +FROM actors +UNION ALL +SELECT + 'sensor' as device_category, + id, + type, + name, + url +FROM sensors +ORDER BY device_category, name; diff --git a/tahoma_to_mysql.py b/tahoma_to_mysql.py new file mode 100644 index 0000000..ceea07d --- /dev/null +++ b/tahoma_to_mysql.py @@ -0,0 +1,598 @@ +#!/usr/bin/env python3 +""" +Somfy Tahoma Local API to MySQL Database Script +Liest alle Aktoren und Sensoren aus der Tahoma Box und speichert sie in MySQL +""" + +import requests +import pymysql +from pymysql import Error +import json +import logging +from typing import List, Dict, Optional +import urllib3 + +# SSL-Warnungen deaktivieren (Tahoma verwendet selbst-signierte Zertifikate) +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# Logging konfigurieren +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +class TahomaAPI: + """Klasse für die Kommunikation mit der Tahoma Local API""" + + def __init__(self, gateway_ip: str, api_token: str): + """ + Initialisiert die Tahoma API Verbindung + + Args: + gateway_ip: IP-Adresse der Tahoma Box + api_token: API Token (Bearer Token) + """ + 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]: + """ + Ruft die komplette Setup-Konfiguration ab + + Returns: + Dictionary mit allen Geräten oder None bei Fehler + """ + 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]: + """ + Extrahiert alle Geräte aus dem Setup + + Returns: + Liste aller Geräte + """ + setup = self.get_setup() + if not setup: + return [] + + devices = setup.get('devices', []) + logger.info(f"{len(devices)} Geräte gefunden") + return devices + + def get_device_definition(self, device_url: str) -> Optional[Dict]: + """ + Ruft die detaillierte Definition eines Geräts ab + + Args: + device_url: URL des Geräts + + Returns: + Dictionary mit Gerätedefinition oder None bei Fehler + """ + try: + # Device URL encodieren + from urllib.parse import quote + encoded_url = quote(device_url, safe='') + url = f"{self.base_url}/setup/devices/{encoded_url}" + 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.debug(f"Fehler beim Abrufen der Device-Definition für {device_url}: {e}") + return None + + def get_device_states(self, device_url: str) -> List[Dict]: + """ + Ruft die aktuellen States eines Geräts ab + + Args: + device_url: URL des Geräts + + Returns: + Liste der States + """ + try: + from urllib.parse import quote + encoded_url = quote(device_url, safe='') + url = f"{self.base_url}/setup/devices/{encoded_url}/states" + 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.debug(f"Fehler beim Abrufen der Device-States für {device_url}: {e}") + return [] + + +class DatabaseManager: + """Klasse für die MySQL-Datenbankoperationen""" + + def __init__(self, host: str, database: str, user: str, password: str, port: int = 3306): + """ + Initialisiert die Datenbankverbindung + + Args: + host: MySQL Host + database: Datenbankname + user: Benutzername + password: Passwort + port: Port (Standard: 3306) + """ + self.host = host + self.database = database + self.user = user + self.password = password + self.port = port + self.connection = None + + def connect(self) -> bool: + """ + Stellt Verbindung zur Datenbank her + + Returns: + True bei Erfolg, False bei Fehler + """ + try: + self.connection = pymysql.connect( + host=self.host, + database=self.database, + user=self.user, + password=self.password, + port=self.port, + charset='utf8mb4' + ) + logger.info("Erfolgreich mit MariaDB/MySQL-Datenbank verbunden") + return True + except Error as e: + logger.error(f"Fehler bei der Datenbankverbindung: {e}") + return False + + def disconnect(self): + """Schließt die Datenbankverbindung""" + if self.connection: + self.connection.close() + logger.info("Datenbankverbindung geschlossen") + + def clear_tables(self): + """Löscht alle Einträge aus allen Tabellen""" + try: + cursor = self.connection.cursor() + # Foreign Key Constraints temporär deaktivieren + cursor.execute("SET FOREIGN_KEY_CHECKS=0") + + # Alle Tabellen leeren + cursor.execute("DELETE FROM command_parameters") + cursor.execute("DELETE FROM actor_commands") + cursor.execute("DELETE FROM actor_states") + cursor.execute("DELETE FROM actors") + + cursor.execute("DELETE FROM sensor_states") + cursor.execute("DELETE FROM sensors") + + # Foreign Key Constraints wieder aktivieren + cursor.execute("SET FOREIGN_KEY_CHECKS=1") + + self.connection.commit() + logger.info("Alle Tabellen geleert") + cursor.close() + except Error as e: + logger.error(f"Fehler beim Leeren der Tabellen: {e}") + self.connection.rollback() + + def insert_actor(self, device_type: str, name: str, url: str, + commands: list, states: list) -> bool: + """ + Fügt einen Aktor mit Commands und States in die Datenbank ein + + Args: + device_type: Gerätetyp (z.B. RollerShutter) + name: Gerätename + url: URL zum Gerät + commands: Liste der Commands mit Parametern + states: Liste der States + + Returns: + True bei Erfolg, False bei Fehler + """ + try: + cursor = self.connection.cursor() + + # 1. Aktor einfügen + query = """ + INSERT INTO actors (type, name, parameters, url) + VALUES (%s, %s, NULL, %s) + """ + cursor.execute(query, (device_type, name, url)) + actor_id = cursor.lastrowid + + # 2. Commands einfügen + for cmd in commands: + command_name = cmd.get('command', '') + + # Command einfügen + cmd_query = """ + INSERT INTO actor_commands (actor_id, command_name) + VALUES (%s, %s) + """ + cursor.execute(cmd_query, (actor_id, command_name)) + command_id = cursor.lastrowid + + # Parameter des Commands einfügen + cmd_params = cmd.get('parameters', []) + for param in cmd_params: + param_query = """ + INSERT INTO command_parameters + (command_id, parameter_name, parameter_type, min_value, max_value, possible_values) + VALUES (%s, %s, %s, %s, %s, %s) + """ + param_name = param.get('name', '') + param_type = param.get('type', '') + min_val = param.get('min') + max_val = param.get('max') + possible_vals = json.dumps(param.get('values')) if 'values' in param else None + + cursor.execute(param_query, + (command_id, param_name, param_type, min_val, max_val, possible_vals)) + + # 3. States einfügen + for state in states: + state_query = """ + INSERT INTO actor_states + (actor_id, state_name, state_type, current_value) + VALUES (%s, %s, %s, %s) + """ + state_name = state.get('name', '') + state_type = state.get('type', 0) + current_value = str(state.get('current_value', '')) if 'current_value' in state else None + + cursor.execute(state_query, (actor_id, state_name, state_type, current_value)) + + self.connection.commit() + cursor.close() + return True + + except Error as e: + logger.error(f"Fehler beim Einfügen des Aktors {name}: {e}") + self.connection.rollback() + return False + + def insert_sensor(self, device_type: str, name: str, url: str, + states: list) -> bool: + """ + Fügt einen Sensor mit States in die Datenbank ein + + Args: + device_type: Gerätetyp (z.B. TemperatureSensor) + name: Gerätename + url: URL zum Gerät + states: Liste der States + + Returns: + True bei Erfolg, False bei Fehler + """ + try: + cursor = self.connection.cursor() + + # 1. Sensor einfügen + query = """ + INSERT INTO sensors (type, name, parameters, url) + VALUES (%s, %s, NULL, %s) + """ + cursor.execute(query, (device_type, name, url)) + sensor_id = cursor.lastrowid + + # 2. States einfügen + for state in states: + state_query = """ + INSERT INTO sensor_states + (sensor_id, state_name, state_type, current_value) + VALUES (%s, %s, %s, %s) + """ + state_name = state.get('name', '') + state_type = state.get('type', 0) + current_value = str(state.get('current_value', '')) if 'current_value' in state else None + + cursor.execute(state_query, (sensor_id, state_name, state_type, current_value)) + + self.connection.commit() + cursor.close() + return True + + except Error as e: + logger.error(f"Fehler beim Einfügen des Sensors {name}: {e}") + self.connection.rollback() + return False + + +class DeviceClassifier: + """Klassifiziert Geräte als Aktoren oder Sensoren""" + + # Bekannte Aktor-Typen (können erweitert werden) + ACTOR_TYPES = { + 'RollerShutter', 'ExteriorScreen', 'Awning', 'Blind', + 'GarageDoor', 'Window', 'Light', 'OnOff', 'DimmableLight', + 'HeatingSystem', 'Valve', 'Switch', 'Door', 'Curtain', + 'VenetianBlind', 'PergolaScreen' + } + + # Bekannte Sensor-Typen (können erweitert werden) + SENSOR_TYPES = { + 'TemperatureSensor', 'LightSensor', 'HumiditySensor', + 'ContactSensor', 'OccupancySensor', 'SmokeSensor', + 'WaterDetectionSensor', 'WindowHandle', 'MotionSensor', + 'SunSensor', 'WindSensor', 'RainSensor', 'ConsumptionSensor' + } + + @classmethod + def is_actor(cls, device: Dict) -> bool: + """ + Prüft, ob ein Gerät ein Aktor ist + + Args: + device: Geräte-Dictionary + + Returns: + True wenn Aktor, False sonst + """ + device_type = device.get('uiClass', '') + + # Prüfung nach bekannten Typen + if device_type in cls.ACTOR_TYPES: + return True + + # Prüfung nach Commandos (Aktoren haben typischerweise Commands) + commands = device.get('definition', {}).get('commands', []) + if commands and len(commands) > 0: + # Wenn Commands wie open, close, on, off existieren + 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 ein Gerät ein Sensor ist + + Args: + device: Geräte-Dictionary + + Returns: + True wenn Sensor, False sonst + """ + device_type = device.get('uiClass', '') + + # Prüfung nach bekannten Typen + if device_type in cls.SENSOR_TYPES: + return True + + # Prüfung nach States (Sensoren haben typischerweise nur States, keine Commands) + states = device.get('states', []) + commands = device.get('definition', {}).get('commands', []) + + # Sensor hat States aber keine oder nur wenige Commands + if states and len(states) > 0 and len(commands) <= 1: + return True + + return False + + +def extract_actor_data(device: Dict) -> tuple: + """ + Extrahiert Commands und States aus einem Aktor + + Args: + device: Geräte-Dictionary von der Tahoma API + + Returns: + Tuple (commands_list, states_list) + """ + commands = [] + states = [] + + # Commands aus der Definition extrahieren + cmd_definitions = device.get('definition', {}).get('commands', []) + + for cmd in cmd_definitions: + command_name = cmd.get('commandName', '') + + command_entry = { + 'command': command_name, + 'parameters': [] + } + + # Alle Parameter des Commands durchgehen + cmd_params = cmd.get('parameters', []) + for cmd_param in cmd_params: + param_detail = { + 'name': cmd_param.get('name', 'value') + } + + # Datentyp + param_type = cmd_param.get('type') + if param_type: + param_detail['type'] = param_type + + # Min/Max Werte für numerische Parameter + if 'min' in cmd_param: + param_detail['min'] = cmd_param['min'] + if 'max' in cmd_param: + param_detail['max'] = cmd_param['max'] + + # Mögliche Werte (enum) + if 'values' in cmd_param: + param_detail['values'] = cmd_param['values'] + + 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 + + +def extract_sensor_data(device: Dict) -> list: + """ + Extrahiert States aus einem Sensor + + Args: + device: Geräte-Dictionary von der Tahoma API + + Returns: + Liste der States + """ + states = [] + + # 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) + } + + # Aktueller Wert falls vorhanden + if 'value' in state: + state_entry['current_value'] = state['value'] + + states.append(state_entry) + + return states + + +def process_devices(tahoma: TahomaAPI, db: DatabaseManager, clear_before_insert: bool = True): + """ + Verarbeitet alle Geräte und speichert sie in der Datenbank + + Args: + tahoma: TahomaAPI Instanz + db: DatabaseManager Instanz + clear_before_insert: Tabellen vor dem Einfügen leeren (Standard: True) + """ + # Geräte von der API abrufen + devices = tahoma.get_devices() + + if not devices: + logger.warning("Keine Geräte gefunden") + return + + # Optional: Tabellen leeren + if clear_before_insert: + db.clear_tables() + + actor_count = 0 + sensor_count = 0 + unknown_count = 0 + + for device in devices: + device_url = device.get('deviceURL', '') + device_name = device.get('label', 'Unbekannt') + device_type = device.get('uiClass', 'Unknown') + + # Gerät klassifizieren + is_actor = DeviceClassifier.is_actor(device) + is_sensor = DeviceClassifier.is_sensor(device) + + if is_actor: + # Daten extrahieren + commands, states = extract_actor_data(device) + + # In Datenbank speichern + if db.insert_actor(device_type, device_name, device_url, commands, states): + actor_count += 1 + logger.info(f"Aktor hinzugefügt: {device_name} ({device_type}) - " + f"{len(commands)} Commands, {len(states)} States") + + elif is_sensor: + # Daten extrahieren + states = extract_sensor_data(device) + + # In Datenbank speichern + if db.insert_sensor(device_type, device_name, device_url, states): + sensor_count += 1 + logger.info(f"Sensor hinzugefügt: {device_name} ({device_type}) - " + f"{len(states)} States") + + else: + unknown_count += 1 + logger.warning(f"Unbekanntes Gerät: {device_name} ({device_type})") + + logger.info(f"\nZusammenfassung:") + logger.info(f"Aktoren gespeichert: {actor_count}") + logger.info(f"Sensoren gespeichert: {sensor_count}") + logger.info(f"Unbekannte Geräte: {unknown_count}") + + +def main(): + """Hauptfunktion""" + + # ===== KONFIGURATION ===== + # Tahoma Box Einstellungen + TAHOMA_IP = "192.168.1.XXX" # IP-Adresse Ihrer Tahoma Box + TAHOMA_TOKEN = "YOUR_API_TOKEN_HERE" # Ihr API Token + + # MySQL Datenbank Einstellungen + DB_HOST = "localhost" + DB_NAME = "EnergyFlow" + DB_USER = "your_username" + DB_PASSWORD = "your_password" + DB_PORT = 3306 + + # Optionen + CLEAR_TABLES = True # Tabellen vor dem Import leeren + # ========================= + + # Tahoma API initialisieren + logger.info("Verbinde mit Tahoma Box...") + tahoma = TahomaAPI(TAHOMA_IP, TAHOMA_TOKEN) + + # Datenbank initialisieren + logger.info("Verbinde mit MySQL-Datenbank...") + db = DatabaseManager(DB_HOST, DB_NAME, DB_USER, DB_PASSWORD, DB_PORT) + + if not db.connect(): + logger.error("Datenbankverbindung fehlgeschlagen. Abbruch.") + return + + try: + # Geräte verarbeiten und in Datenbank speichern + process_devices(tahoma, db, clear_before_insert=CLEAR_TABLES) + logger.info("Import erfolgreich abgeschlossen!") + + except Exception as e: + logger.error(f"Fehler während der Verarbeitung: {e}") + + finally: + # Datenbankverbindung schließen + db.disconnect() + + +if __name__ == "__main__": + main()