Der SolarManager legt gut 130 Topics auf den Broker, aber er meldet sich nicht per Home-Assistant-Discovery. Das MQTT-Modul findet ihn deshalb nicht, und eine Automatik konnte weder auf den Netzbezug noch auf den Ladestand der Batterie oder die Temperatur im Puffer reagieren. Vier feste Geraete mit neun Messwerten, ausgewaehlt nach dem, was das Realtime-SVG zeigt - von der PV nur die Summe. Alles anzulegen haette die Auswahl im Editor unbrauchbar gemacht: jeder Wechselrichter einzeln, jeder Phasenstrom, jede Vorlauftemperatur. Netzanschluss Bezug, Einspeisung Stromzaehler Obergeschoss Leistung Solaranlage PV-Leistung, Batterie-Ladestand Pufferspeicher drei Temperaturen, Heizstab-Leistung Die beiden Modbus-Zaehler am Gen24 (meter0 am Hausanschluss, meter1 fuer das Obergeschoss) gibt es nur ueber diesen Weg - sie sprechen sonst mit niemandem. Die beiden Shelly EM3 bleiben dagegen aussen vor: die stehen ueber die Home-Assistant-Discovery laengst als Power_EG_EM_0 und Power_UG_EM_0 in der Tabelle. solarManager/eg waere ohnehin nicht dasselbe, dort ist die Wallbox im Carport eingerechnet. Beim Netzanschluss stehen Bezug und Einspeisung statt P_Grid: eine Zahl mit Vorzeichen liest sich in einer Bedingung falsch herum. Beim Obergeschoss geht das nicht, der Zaehler meldet Verbrauch negativ - das steht als Warnung im Modul. Alles nur zum Lesen. Geschaltet wird am SolarManager nichts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
654 lines
23 KiB
Python
654 lines
23 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
|
|
from modules.gartenwasser_module import GartenwasserModule
|
|
from modules.solarmanager_module import SolarManagerModule
|
|
from modules.logic_module import LogicModule #nicht Vergessen neue Module auch unten in die Liste "modules" einzufügen!!
|
|
|
|
# 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)
|
|
|
|
# LOGIC
|
|
@property
|
|
def logic_enable(self) -> bool:
|
|
if not self.config.has_section('logic'):
|
|
return False
|
|
return self._get_bool('logic', 'enable', False)
|
|
|
|
# GARTENWASSER
|
|
@property
|
|
def gartenwasser_enable(self) -> bool:
|
|
if not self.config.has_section('gartenwasser'):
|
|
return False
|
|
return self._get_bool('gartenwasser', 'enable', False)
|
|
|
|
# SOLARMANAGER
|
|
@property
|
|
def solarmanager_enable(self) -> bool:
|
|
if not self.config.has_section('solarmanager'):
|
|
return False
|
|
return self._get_bool('solarmanager', 'enable', False)
|
|
|
|
|
|
# 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"""
|
|
|
|
PARAMETER_TYPES = {
|
|
0: "bool",
|
|
1: "integer", # Integer value
|
|
2: "float", # Floating point number
|
|
3: "string", # Text string
|
|
4: "undefined",
|
|
5: "time",
|
|
6: "array",
|
|
7: "deltatime",
|
|
8: "date",
|
|
9: "datetime",
|
|
}
|
|
|
|
PARAMETER_TYPES_MAPPING = {
|
|
"bool": 0,
|
|
"boolean" : 0,
|
|
"integer" : 1, # Integer value
|
|
"float" : 2, # Floating point number
|
|
"number" : 2, # Floating point number
|
|
"string" : 3, # Text string
|
|
"time" : 5,
|
|
"array": 6,
|
|
"deltatime" : 7,
|
|
"date" : 8,
|
|
"datetime" : 9,
|
|
}
|
|
|
|
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("TRUNCATE state_types")
|
|
cursor.execute("TRUNCATE command_parameters")
|
|
cursor.execute("TRUNCATE actor_commands")
|
|
cursor.execute("TRUNCATE actor_states")
|
|
cursor.execute("TRUNCATE actors")
|
|
cursor.execute("TRUNCATE sensor_states")
|
|
cursor.execute("TRUNCATE 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, url)
|
|
VALUES (%s, %s, %s)
|
|
ON DUPLICATE KEY UPDATE
|
|
type = VALUES(type),
|
|
name = VALUES(name)
|
|
"""
|
|
cursor.execute(query, (device_type, name, url))
|
|
query = """
|
|
SELECT ID FROM actors where url = %s
|
|
"""
|
|
cursor.execute(query, (url))
|
|
actor_id = cursor.fetchone()[0]
|
|
|
|
# Commands einfügen
|
|
for cmd in commands:
|
|
command_name = cmd.get('command', '')
|
|
command_url = cmd.get('url', '')
|
|
cmd_query = """
|
|
INSERT INTO actor_commands (actor_id, command_name, command_url)
|
|
VALUES (%s, %s, %s)
|
|
ON DUPLICATE KEY UPDATE
|
|
command_url = VALUES(command_url)
|
|
"""
|
|
cursor.execute(cmd_query, (actor_id, command_name, command_url))
|
|
query = """
|
|
SELECT ID FROM actor_commands where actor_id = %s and command_name = %s
|
|
"""
|
|
cursor.execute(query, (actor_id, command_name))
|
|
command_id = cursor.fetchone()[0]
|
|
|
|
# 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)
|
|
ON DUPLICATE KEY UPDATE
|
|
parameter_type = VALUES(parameter_type),
|
|
min_value = VALUES(min_value),
|
|
max_value = VALUES(max_value),
|
|
possible_values = VALUES(possible_values),
|
|
url = VALUES(url)
|
|
"""
|
|
param_name = param.get('name', '')
|
|
param_type = self.PARAMETER_TYPES_MAPPING.get(param.get('type', ''), 4)
|
|
min_val = param.get('min')
|
|
max_val = param.get('max')
|
|
possible_vals = json.dumps(param.get('values')) if 'values' in param else ""
|
|
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, possible_values, value_path)
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
|
ON DUPLICATE KEY UPDATE
|
|
state_type = VALUES(state_type),
|
|
-- COALESCE und nicht VALUES() allein: ein Modul, das
|
|
-- keinen Startwert kennt, liefert hier NULL, und das
|
|
-- loeschte bisher den laufenden Messwert. Der Runner
|
|
-- schrieb ihn auch nicht zurueck - er merkt sich, was er
|
|
-- zuletzt in die Datenbank geschrieben hat, und der
|
|
-- naechste gleiche Wert galt damit als unveraendert. Im
|
|
-- Editor stand danach "Regen heute (= )", bis sich der
|
|
-- Wert zufaellig einmal aenderte.
|
|
current_value = COALESCE(VALUES(current_value), current_value),
|
|
possible_values = VALUES(possible_values),
|
|
unit = VALUES(unit),
|
|
url = VALUES(url),
|
|
value_path = VALUES(value_path)
|
|
"""
|
|
state_name = state.get('name', '')
|
|
state_type = self.PARAMETER_TYPES_MAPPING.get(state.get('type', 0), 4)
|
|
current_value = str(state.get('current_value', '')) if 'current_value' in state else None
|
|
unit = state.get('unit')
|
|
state_url = state.get('url')
|
|
value_path = state.get('value_path')
|
|
possible_vals = json.dumps(state.get('values')) if 'values' in state else ""
|
|
cursor.execute(state_query, (actor_id, state_name, state_type, current_value, unit, state_url, possible_vals, value_path))
|
|
|
|
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 fillVariableTypes(self):
|
|
cursor = self.connection.cursor()
|
|
query = """
|
|
INSERT INTO state_types (id, type)
|
|
VALUES (%s, %s)
|
|
ON DUPLICATE KEY UPDATE
|
|
type = VALUES(type)
|
|
"""
|
|
for id, parameter_type in self.PARAMETER_TYPES.items():
|
|
cursor.execute(query, (id, parameter_type))
|
|
|
|
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 actors (type, name, url)
|
|
VALUES (%s, %s, %s)
|
|
ON DUPLICATE KEY UPDATE
|
|
type = VALUES(type),
|
|
name = VALUES(name)
|
|
"""
|
|
cursor.execute(query, (device_type, name, url))
|
|
sensor_id = cursor.lastrowid
|
|
query = """
|
|
SELECT ID FROM actors where url = %s
|
|
"""
|
|
cursor.execute(query, (url))
|
|
sensor_id = cursor.fetchone()[0]
|
|
|
|
# States einfügen
|
|
for state in states:
|
|
state_query = """
|
|
INSERT INTO actor_states
|
|
(actor_id, state_name, state_type, current_value, unit, url, value_path, possible_values)
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
|
ON DUPLICATE KEY UPDATE
|
|
state_type = VALUES(state_type),
|
|
-- Siehe insert_actor: NULL heisst "kein Startwert",
|
|
-- nicht "Messwert loeschen".
|
|
current_value = COALESCE(VALUES(current_value), current_value),
|
|
unit = VALUES(unit),
|
|
url = VALUES(url),
|
|
value_path = VALUES(value_path),
|
|
possible_values = VALUES(possible_values)
|
|
"""
|
|
state_name = state.get('name', '')
|
|
state_type = self.PARAMETER_TYPES_MAPPING.get(state.get('type', 0), 4)
|
|
current_value = str(state.get('current_value', '')) if 'current_value' in state else None
|
|
unit = state.get('unit')
|
|
state_url = state.get('url')
|
|
value_path = state.get('value_path')
|
|
possible_vals = json.dumps(state.get('values')) if 'values' in state else ""
|
|
|
|
cursor.execute(state_query, (sensor_id, state_name, state_type, current_value, unit, state_url, value_path, possible_vals))
|
|
|
|
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
|
|
|
|
db.fillVariableTypes()
|
|
# Module initialisieren
|
|
modules = [
|
|
TahomaModule(config),
|
|
WLEDModule(config),
|
|
MQTTModule(config),
|
|
ShellyModule(config),
|
|
LogicModule(config),
|
|
GartenwasserModule(config),
|
|
SolarManagerModule(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()
|