feat: rewrite busylight as FastAPI web application with SQLite
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>
This commit is contained in:
+148
@@ -0,0 +1,148 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user