Bisher entschied jede Arbeitsstation fuer sich, welche Zeilenenden ein Commit bekommt: Git fuer Windows setzt core.autocrlf=input in seiner System-Konfiguration, die NAS setzt nichts. Weil der Bestand ausserdem in sich gemischt war - 34 PHP-Dateien mit LF, 30 mit CRLF -, gab es gar kein richtiges Zeilenende, an das ein Werkzeug sich haette halten koennen. Jede Ergaenzung mit LF in einer CRLF-Datei ergab eine gemischte Datei; helper.php und js/solar/homeMQTT.js waren bereits so entstanden. .gitattributes macht die Zeilenenden zur Eigenschaft des Repositorys. LF, weil dieses Verzeichnis der Web-Ordner der NAS ist und von Linux ausgeliefert wird - und weil Git fuer Windows mit "input" ohnehin schon dorthin zeigt. restricted/WebAuthn bleibt ausgenommen und damit byteweise so, wie die Bibliothek geliefert wurde; darunter liegen 292 Zertifikate. Von den 47 geaenderten Dateien wurde nachgerechnet keine einzige im Inhalt angefasst - der Vergleich HEAD-ohne-CR gegen Index ist ueberall gleich. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
645 lines
23 KiB
Python
645 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.logic_module import LogicModule #nicht Vergessen neue Module auch in Zeile 546 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)
|
|
|
|
|
|
# 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)
|
|
]
|
|
|
|
# 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()
|