531 lines
18 KiB
Python
531 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Device Discovery - Refactored with Unified Architecture
|
|
=======================================================
|
|
Zentrale Datenbank-Logik im Hauptscript
|
|
Einheitliche Schnittstelle für alle Module
|
|
"""
|
|
|
|
import pymysql
|
|
from pymysql import Error
|
|
import json
|
|
import logging
|
|
import configparser
|
|
import os
|
|
from typing import Optional, List, Dict
|
|
|
|
# Import Module
|
|
from modules.tahoma_module import TahomaModule
|
|
from modules.wled_module import WLEDModule
|
|
from modules.mqtt_module import MQTTModule
|
|
from modules.shelly_module import ShellyModule
|
|
|
|
# Logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(levelname)s - %(message)s'
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ============================================================================
|
|
# CONFIG CLASS (Original)
|
|
# ============================================================================
|
|
|
|
class Config:
|
|
"""Lädt und verwaltet die Konfiguration"""
|
|
|
|
def __init__(self, config_file: str = 'config.ini'):
|
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
config_path = os.path.join(script_dir, config_file)
|
|
|
|
if not os.path.exists(config_path):
|
|
raise FileNotFoundError(
|
|
f"Konfigurationsdatei '{config_file}' nicht gefunden in {script_dir}"
|
|
)
|
|
|
|
self.config = configparser.ConfigParser()
|
|
self.config.read(config_path, encoding='utf-8')
|
|
logger.info(f"Konfiguration geladen von: {config_path}")
|
|
self._validate()
|
|
|
|
def _validate(self):
|
|
required_sections = ['database', 'options']
|
|
for section in required_sections:
|
|
if not self.config.has_section(section):
|
|
raise ValueError(f"Erforderliche Sektion '[{section}]' fehlt")
|
|
|
|
def _get_bool(self, section: str, key: str, fallback: bool = False) -> bool:
|
|
value = self.config.get(section, key, fallback=str(fallback)).strip().lower()
|
|
return value in ('true', '1', 'yes', 'on')
|
|
|
|
def _get_int(self, section: str, key: str, fallback: int = 0) -> int:
|
|
try:
|
|
return self.config.getint(section, key, fallback=fallback)
|
|
except ValueError:
|
|
return fallback
|
|
|
|
def _get_list(self, section: str, key: str) -> List[str]:
|
|
value = self.config.get(section, key, fallback='').strip()
|
|
if not value:
|
|
return []
|
|
return [item.strip() for item in value.split(',') if item.strip()]
|
|
|
|
# Database
|
|
@property
|
|
def db_host(self) -> str:
|
|
return self.config.get('database', 'host').strip()
|
|
|
|
@property
|
|
def db_port(self) -> int:
|
|
return self._get_int('database', 'port', 3306)
|
|
|
|
@property
|
|
def db_name(self) -> str:
|
|
return self.config.get('database', 'database').strip()
|
|
|
|
@property
|
|
def db_user(self) -> str:
|
|
return self.config.get('database', 'user').strip()
|
|
|
|
@property
|
|
def db_password(self) -> str:
|
|
return self.config.get('database', 'password').strip()
|
|
|
|
# Tahoma
|
|
@property
|
|
def tahoma_enable(self) -> bool:
|
|
if not self.config.has_section('tahoma'):
|
|
return False
|
|
return self._get_bool('tahoma', 'enable', False)
|
|
|
|
@property
|
|
def tahoma_ip(self) -> str:
|
|
if not self.config.has_section('tahoma'):
|
|
return ''
|
|
return self.config.get('tahoma', 'ip', fallback='').strip()
|
|
|
|
@property
|
|
def tahoma_token(self) -> str:
|
|
if not self.config.has_section('tahoma'):
|
|
return ''
|
|
return self.config.get('tahoma', 'token', fallback='').strip()
|
|
|
|
@property
|
|
def tahoma_timeout(self) -> int:
|
|
if not self.config.has_section('tahoma'):
|
|
return 10
|
|
return self._get_int('tahoma', 'timeout', 10)
|
|
|
|
# WLED
|
|
@property
|
|
def wled_enable(self) -> bool:
|
|
if not self.config.has_section('wled'):
|
|
return False
|
|
return self._get_bool('wled', 'enable', False)
|
|
|
|
@property
|
|
def wled_discovery_timeout(self) -> int:
|
|
if not self.config.has_section('wled'):
|
|
return 5
|
|
return self._get_int('wled', 'discovery_timeout', 5)
|
|
|
|
@property
|
|
def wled_manual_ips(self) -> List[str]:
|
|
if not self.config.has_section('wled'):
|
|
return []
|
|
return self._get_list('wled', 'manual_ips')
|
|
|
|
@property
|
|
def wled_timeout(self) -> int:
|
|
if not self.config.has_section('wled'):
|
|
return 2
|
|
return self._get_int('wled', 'timeout', 2)
|
|
|
|
# MQTT
|
|
@property
|
|
def mqtt_enable(self) -> bool:
|
|
if not self.config.has_section('mqtt'):
|
|
return False
|
|
return self._get_bool('mqtt', 'enable', False)
|
|
|
|
@property
|
|
def mqtt_broker(self) -> str:
|
|
if not self.config.has_section('mqtt'):
|
|
return 'localhost'
|
|
return self.config.get('mqtt', 'broker', fallback='localhost').strip()
|
|
|
|
@property
|
|
def mqtt_port(self) -> int:
|
|
if not self.config.has_section('mqtt'):
|
|
return 1883
|
|
return self._get_int('mqtt', 'port', 1883)
|
|
|
|
@property
|
|
def mqtt_username(self) -> Optional[str]:
|
|
if not self.config.has_section('mqtt'):
|
|
return None
|
|
value = self.config.get('mqtt', 'username', fallback='').strip()
|
|
return value if value else None
|
|
|
|
@property
|
|
def mqtt_password(self) -> Optional[str]:
|
|
if not self.config.has_section('mqtt'):
|
|
return None
|
|
value = self.config.get('mqtt', 'password', fallback='').strip()
|
|
return value if value else None
|
|
|
|
@property
|
|
def mqtt_discovery_prefix(self) -> str:
|
|
if not self.config.has_section('mqtt'):
|
|
return 'homeassistant'
|
|
return self.config.get('mqtt', 'discovery_prefix', fallback='homeassistant').strip()
|
|
|
|
@property
|
|
def mqtt_discovery_timeout(self) -> int:
|
|
if not self.config.has_section('mqtt'):
|
|
return 10
|
|
return self._get_int('mqtt', 'discovery_timeout', 10)
|
|
|
|
# Shelly
|
|
@property
|
|
def shelly_enable(self) -> bool:
|
|
if not self.config.has_section('shelly'):
|
|
return False
|
|
return self._get_bool('shelly', 'enable', False)
|
|
|
|
@property
|
|
def shelly_network_range(self) -> str:
|
|
if not self.config.has_section('shelly'):
|
|
return '192.168.1'
|
|
return self.config.get('shelly', 'network_range', fallback='192.168.1').strip()
|
|
|
|
@property
|
|
def shelly_start_ip(self) -> int:
|
|
if not self.config.has_section('shelly'):
|
|
return 1
|
|
return self._get_int('shelly', 'start_ip', 1)
|
|
|
|
@property
|
|
def shelly_end_ip(self) -> int:
|
|
if not self.config.has_section('shelly'):
|
|
return 254
|
|
return self._get_int('shelly', 'end_ip', 254)
|
|
|
|
# Options
|
|
@property
|
|
def clear_tables(self) -> bool:
|
|
return self._get_bool('options', 'clear_tables', True)
|
|
|
|
@property
|
|
def log_level(self) -> str:
|
|
level = self.config.get('options', 'log_level', fallback='INFO').strip().upper()
|
|
valid_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
|
|
return level if level in valid_levels else 'INFO'
|
|
|
|
@property
|
|
def log_file(self) -> Optional[str]:
|
|
value = self.config.get('options', 'log_file', fallback='').strip()
|
|
return value if value else None
|
|
|
|
|
|
# ============================================================================
|
|
# DATABASE MANAGER (Zentral im Hauptscript)
|
|
# ============================================================================
|
|
|
|
class DatabaseManager:
|
|
"""Zentrale Datenbank-Verwaltung - ALLE DB-Operationen hier"""
|
|
|
|
def __init__(self, host: str, database: str, user: str, password: str, port: int = 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"""
|
|
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 MySQL-Datenbank verbunden")
|
|
return True
|
|
except Error as e:
|
|
logger.error(f"✗ Datenbankverbindung fehlgeschlagen: {e}")
|
|
return False
|
|
|
|
def disconnect(self):
|
|
"""Schließt 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()
|
|
cursor.execute("SET FOREIGN_KEY_CHECKS=0")
|
|
|
|
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")
|
|
|
|
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 in die Datenbank ein
|
|
|
|
Args:
|
|
device_type: Typ des Geräts
|
|
name: Name des Geräts
|
|
url: Eindeutige URL/ID
|
|
commands: Liste von Command-Dicts
|
|
states: Liste von State-Dicts
|
|
"""
|
|
try:
|
|
cursor = self.connection.cursor()
|
|
|
|
# Actor 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
|
|
|
|
# Commands einfügen
|
|
for cmd in commands:
|
|
command_name = cmd.get('command', '')
|
|
|
|
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 einfügen
|
|
for param in cmd.get('parameters', []):
|
|
param_query = """
|
|
INSERT INTO command_parameters
|
|
(command_id, parameter_name, parameter_type, min_value, max_value, possible_values, url)
|
|
VALUES (%s, %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
|
|
param_url = param.get('url')
|
|
|
|
cursor.execute(param_query,
|
|
(command_id, param_name, param_type, min_val, max_val, possible_vals, param_url))
|
|
|
|
# States einfügen
|
|
for state in states:
|
|
state_query = """
|
|
INSERT INTO actor_states
|
|
(actor_id, state_name, state_type, current_value, unit, url)
|
|
VALUES (%s, %s, %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
|
|
unit = state.get('unit')
|
|
state_url = state.get('url')
|
|
|
|
cursor.execute(state_query, (actor_id, state_name, state_type, current_value, unit, state_url))
|
|
|
|
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 in die Datenbank ein
|
|
|
|
Args:
|
|
device_type: Typ des Sensors
|
|
name: Name des Sensors
|
|
url: Eindeutige URL/ID
|
|
states: Liste von State-Dicts
|
|
"""
|
|
try:
|
|
cursor = self.connection.cursor()
|
|
|
|
# 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
|
|
|
|
# States einfügen
|
|
for state in states:
|
|
state_query = """
|
|
INSERT INTO sensor_states
|
|
(sensor_id, state_name, state_type, current_value, unit, url)
|
|
VALUES (%s, %s, %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
|
|
unit = state.get('unit')
|
|
state_url = state.get('url')
|
|
|
|
cursor.execute(state_query, (sensor_id, state_name, state_type, current_value, unit, state_url))
|
|
|
|
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
|
|
|
|
|
|
# ============================================================================
|
|
# MAIN FUNCTION
|
|
# ============================================================================
|
|
|
|
def main():
|
|
"""Hauptfunktion - orchestriert alle Module mit einheitlicher Schnittstelle"""
|
|
|
|
# Konfiguration laden
|
|
try:
|
|
config = Config('config.ini')
|
|
logger.info("✓ Konfiguration erfolgreich geladen")
|
|
except FileNotFoundError as e:
|
|
print(f"FEHLER: {e}")
|
|
return
|
|
except Exception as e:
|
|
print(f"FEHLER: {e}")
|
|
return
|
|
|
|
# Logging anpassen
|
|
log_level = getattr(logging, config.log_level)
|
|
logger.setLevel(log_level)
|
|
|
|
if config.log_file:
|
|
file_handler = logging.FileHandler(config.log_file, encoding='utf-8')
|
|
file_handler.setLevel(log_level)
|
|
file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
|
|
logger.addHandler(file_handler)
|
|
logger.info(f"Logging in Datei: {config.log_file}")
|
|
|
|
# Datenbank initialisieren
|
|
logger.info("Verbinde mit MySQL-Datenbank...")
|
|
db = DatabaseManager(
|
|
config.db_host,
|
|
config.db_name,
|
|
config.db_user,
|
|
config.db_password,
|
|
config.db_port
|
|
)
|
|
|
|
if not db.connect():
|
|
logger.error("Datenbankverbindung fehlgeschlagen. Abbruch.")
|
|
return
|
|
|
|
try:
|
|
# Tabellen leeren
|
|
if config.clear_tables:
|
|
db.clear_tables()
|
|
|
|
total_actors = 0
|
|
total_sensors = 0
|
|
|
|
# Module initialisieren
|
|
modules = [
|
|
TahomaModule(config),
|
|
WLEDModule(config),
|
|
MQTTModule(config),
|
|
ShellyModule(config)
|
|
]
|
|
|
|
# Jedes Modul durchlaufen
|
|
for module in modules:
|
|
if not module.is_enabled():
|
|
logger.info(f"Modul {module.get_name()} ist deaktiviert")
|
|
continue
|
|
|
|
try:
|
|
# Discovery durchführen (einheitliche Schnittstelle!)
|
|
actors, sensors = module.discover()
|
|
|
|
# Actors in DB speichern
|
|
for actor in actors:
|
|
if db.insert_actor(
|
|
actor['type'],
|
|
actor['name'],
|
|
actor['url'],
|
|
actor.get('commands', []),
|
|
actor.get('states', [])
|
|
):
|
|
total_actors += 1
|
|
logger.info(f" ✓ Actor: {actor['name']} ({actor['type']})")
|
|
|
|
# Sensors in DB speichern
|
|
for sensor in sensors:
|
|
if db.insert_sensor(
|
|
sensor['type'],
|
|
sensor['name'],
|
|
sensor['url'],
|
|
sensor.get('states', [])
|
|
):
|
|
total_sensors += 1
|
|
logger.info(f" ✓ Sensor: {sensor['name']} ({sensor['type']})")
|
|
|
|
except Exception as e:
|
|
logger.error(f"✗ Fehler bei Modul {module.get_name()}: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
# Zusammenfassung
|
|
logger.info("\n" + "=" * 60)
|
|
logger.info("ZUSAMMENFASSUNG")
|
|
logger.info("=" * 60)
|
|
logger.info(f"Aktoren gespeichert: {total_actors}")
|
|
logger.info(f"Sensoren gespeichert: {total_sensors}")
|
|
logger.info("=" * 60)
|
|
|
|
logger.info("\n✓ Import erfolgreich abgeschlossen!")
|
|
|
|
except Exception as e:
|
|
logger.error(f"✗ Fehler: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
finally:
|
|
db.disconnect()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|