This commit is contained in:
Moirtz Wagner 2026-02-02 19:21:09 +01:00
parent 715f90ca9b
commit fd2d3cee8b
3 changed files with 1023 additions and 1 deletions

245
README.md
View File

@ -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;
```

181
database_schema.sql Normal file
View File

@ -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;

598
tahoma_to_mysql.py Normal file
View File

@ -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()