Replace the standalone Python script + config.cfg with a full web application featuring: - FastAPI backend with REST API - SQLite database (config, state, activity log separated) - Bootstrap 5 web interface for configuration - Pages: Dashboard, Users, Devices, Assignments, Color Rules, Settings, Log - APScheduler background polling for Teams presence + AIDA status - systemd service file for Linux deployment - Migration script to import existing config.cfg data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
149 lines
5.4 KiB
Python
149 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Migrate old config.cfg to SQLite database."""
|
|
|
|
import re
|
|
import sys
|
|
from configparser import ConfigParser
|
|
from database import init_db, get_db, set_setting
|
|
|
|
|
|
def parse_color(value):
|
|
"""Parse 'R=255&G=0&B=0' or '/win&A=255&R=200&G=200&B=200' into (r, g, b, effect)."""
|
|
effect = ""
|
|
# Extract effect prefix (e.g., /win&A=255)
|
|
if value.startswith("/"):
|
|
parts = value.split("&R=")
|
|
if len(parts) == 2:
|
|
effect = parts[0]
|
|
value = "R=" + parts[1]
|
|
|
|
r = g = b = 0
|
|
for part in value.replace("&", " ").split():
|
|
if part.startswith("R="):
|
|
r = int(part[2:])
|
|
elif part.startswith("G="):
|
|
g = int(part[2:])
|
|
elif part.startswith("B="):
|
|
b = int(part[2:])
|
|
return r, g, b, effect
|
|
|
|
|
|
def migrate(config_path="config.cfg"):
|
|
"""Import old config.cfg into the SQLite database."""
|
|
config = ConfigParser()
|
|
config.read(config_path, encoding="utf-8")
|
|
|
|
init_db()
|
|
conn = get_db()
|
|
|
|
# Azure settings
|
|
if config.has_section("azure"):
|
|
az = config["azure"]
|
|
set_setting("azure_client_id", az.get("clientid", ""))
|
|
set_setting("azure_client_secret", az.get("clientsecret", ""))
|
|
set_setting("azure_tenant_id", az.get("tenantid", ""))
|
|
set_setting("azure_auth_tenant", az.get("authtenant", "common"))
|
|
set_setting("azure_scopes", az.get("graphuserscopes", ""))
|
|
|
|
# Auth record
|
|
if config.has_option("cred", "value"):
|
|
set_setting("azure_auth_record", config["cred"]["value"])
|
|
|
|
# Config settings
|
|
if config.has_section("config"):
|
|
cfg = config["config"]
|
|
set_setting("poll_interval", cfg.get("intervall", "15"))
|
|
set_setting("aida_csv_path", cfg.get("anwesenheit_csv", ""))
|
|
set_setting("aida_status_anwesend", cfg.get("anwesenheit_anwesend", "~~1~~"))
|
|
set_setting("aida_status_abwesend", cfg.get("anwesenheit_abwesend", "~~0~~"))
|
|
set_setting("aida_status_homeoffice", cfg.get("anwesenheit_homeoffice", "~~2~~"))
|
|
|
|
# WLED color rules
|
|
if config.has_section("wled"):
|
|
for status, value in config["wled"].items():
|
|
r, g, b, effect = parse_color(value)
|
|
conn.execute(
|
|
"""INSERT OR REPLACE INTO color_rules (status, r, g, b, effect)
|
|
VALUES (?, ?, ?, ?, ?)""",
|
|
(status, r, g, b, effect),
|
|
)
|
|
|
|
# Users and WLED devices
|
|
devices = {} # hostname -> device_id
|
|
if config.has_section("user"):
|
|
for azure_id, entry in config["user"].items():
|
|
parts = entry.split(" - ", 1)
|
|
hostname = parts[0].strip()
|
|
display_name = parts[1].strip() if len(parts) > 1 else ""
|
|
|
|
# Get personal number from anwesenheit section
|
|
personal_nr = ""
|
|
if config.has_option("anwesenheit", azure_id):
|
|
anw_val = config["anwesenheit"][azure_id].split()[0]
|
|
personal_nr = anw_val
|
|
|
|
# Determine if user is active (has a known hostname)
|
|
is_active = 0 if hostname == "unbekannt" else 1
|
|
|
|
conn.execute(
|
|
"""INSERT OR REPLACE INTO users (azure_id, display_name, personal_nr, active)
|
|
VALUES (?, ?, ?, ?)""",
|
|
(azure_id, display_name, personal_nr, is_active),
|
|
)
|
|
|
|
# Create WLED device if hostname is known
|
|
if hostname != "unbekannt" and hostname not in devices:
|
|
conn.execute(
|
|
"INSERT OR IGNORE INTO wled_devices (hostname, name, segment_count) VALUES (?, ?, 2)",
|
|
(hostname, hostname),
|
|
)
|
|
row = conn.execute(
|
|
"SELECT id FROM wled_devices WHERE hostname = ?", (hostname,)
|
|
).fetchone()
|
|
if row:
|
|
devices[hostname] = row["id"]
|
|
|
|
# Segment assignments
|
|
if config.has_section("segment"):
|
|
for azure_id, entry in config["segment"].items():
|
|
parts = entry.split()
|
|
seg_str = parts[0] if parts else "-"
|
|
if seg_str == "-":
|
|
continue
|
|
|
|
try:
|
|
segment = int(seg_str)
|
|
except ValueError:
|
|
continue
|
|
|
|
# Find the user's hostname
|
|
if config.has_option("user", azure_id):
|
|
user_entry = config["user"][azure_id]
|
|
hostname = user_entry.split(" - ", 1)[0].strip()
|
|
if hostname in devices:
|
|
device_id = devices[hostname]
|
|
conn.execute(
|
|
"""INSERT OR REPLACE INTO assignments
|
|
(user_azure_id, wled_device_id, segment)
|
|
VALUES (?, ?, ?)""",
|
|
(azure_id, device_id, segment),
|
|
)
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
print(f"Migration abgeschlossen.")
|
|
print(f" - {len(devices)} WLED-Geraete importiert")
|
|
|
|
conn = get_db()
|
|
user_count = conn.execute("SELECT COUNT(*) as c FROM users").fetchone()["c"]
|
|
active_count = conn.execute("SELECT COUNT(*) as c FROM users WHERE active=1").fetchone()["c"]
|
|
assign_count = conn.execute("SELECT COUNT(*) as c FROM assignments").fetchone()["c"]
|
|
conn.close()
|
|
print(f" - {user_count} User importiert ({active_count} aktiv)")
|
|
print(f" - {assign_count} Segment-Zuordnungen importiert")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
path = sys.argv[1] if len(sys.argv) > 1 else "config.cfg"
|
|
migrate(path)
|