Files
busylight_modular/database.py
T
christianandClaude Opus 5 086b21dc2b feat: Zentrale per Durchwahl selbst besetzen
Das Busylight im Empfangsbereich wird wechselnd besetzt. Mitarbeiter
koennen sich jetzt selbst darauf setzen, ohne Admin-Passwort.

- Neue Seite /zentrale (Durchwahl eingeben) und /zentrale/{durchwahl}
  als Direktlink fuer QR-Codes. Der Direktlink zeigt nur an und aendert
  nichts, damit Link-Vorschau und Prefetch niemanden versetzen.
- Neue Tabelle home_assignments merkt das eigene Tuerschild, solange der
  Mitarbeiter in der Zentrale sitzt. Beim Verlassen wird es
  wiederhergestellt, das Zentral-Segment wird freigegeben.
- Der Vorgaenger bleibt ohne Zuordnung - er ist nicht da. Sein gemerkter
  Heimatplatz bleibt erhalten, er kann jederzeit zurueckkehren.
- Ist der Heimatplatz inzwischen belegt, wird die Rueckkehr abgelehnt
  und nichts geaendert.
- Eine ausdrueckliche Admin-Zuordnung verwirft den gemerkten Heimatplatz.
  Ein reines Loeschen der Zuordnung nicht, sonst kaeme niemand mehr von
  der Zentrale zurueck.
- Farbe wird sofort ans Geraet geschickt (scheduler.push_user_light).
  Das Polling allein reicht nicht: _process_user sendet nur bei einer
  Statusaenderung, und beim Platzwechsel bleibt der Status gleich.
- Neue Settings selfservice_device_id und selfservice_segment,
  einstellbar unter Einstellungen.
- Jeder Wechsel landet im Protokoll (zentrale / zentrale_frei).

Die Durchwahl ist eine Kennung, kein Geheimnis - im README dokumentiert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 08:12:42 +00:00

227 lines
7.8 KiB
Python

import sqlite3
import os
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "busylight.db")
def get_db():
conn = sqlite3.connect(DB_PATH, timeout=10)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA foreign_keys=ON")
return conn
def init_db():
conn = get_db()
conn.executescript("""
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS users (
azure_id TEXT PRIMARY KEY,
display_name TEXT NOT NULL DEFAULT '',
email TEXT DEFAULT '',
personal_nr TEXT DEFAULT '',
phone_extension TEXT DEFAULT '',
active INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS wled_devices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hostname TEXT NOT NULL UNIQUE,
name TEXT DEFAULT '',
segment_count INTEGER NOT NULL DEFAULT 2,
online INTEGER DEFAULT 0,
leds INTEGER DEFAULT 0,
version TEXT DEFAULT '',
last_check TIMESTAMP
);
CREATE TABLE IF NOT EXISTS assignments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_azure_id TEXT NOT NULL REFERENCES users(azure_id) ON DELETE CASCADE,
wled_device_id INTEGER NOT NULL REFERENCES wled_devices(id) ON DELETE CASCADE,
segment INTEGER NOT NULL DEFAULT 0,
UNIQUE(wled_device_id, segment),
UNIQUE(user_azure_id)
);
-- Heimat-Tuerschild: gemerkte Zuordnung, waehrend der Nutzer in der Zentrale sitzt
CREATE TABLE IF NOT EXISTS home_assignments (
user_azure_id TEXT PRIMARY KEY REFERENCES users(azure_id) ON DELETE CASCADE,
wled_device_id INTEGER NOT NULL REFERENCES wled_devices(id) ON DELETE CASCADE,
segment INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS color_rules (
status TEXT PRIMARY KEY,
r INTEGER NOT NULL DEFAULT 0,
g INTEGER NOT NULL DEFAULT 0,
b INTEGER NOT NULL DEFAULT 0,
effect TEXT DEFAULT ''
);
CREATE TABLE IF NOT EXISTS state (
user_azure_id TEXT PRIMARY KEY REFERENCES users(azure_id) ON DELETE CASCADE,
last_attendance_status TEXT DEFAULT '',
last_teams_status TEXT DEFAULT '',
phone_active INTEGER DEFAULT 0,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS activity_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
user_azure_id TEXT,
user_name TEXT,
attendance_status TEXT,
teams_status TEXT,
wled_url TEXT,
result TEXT
);
""")
# Migrate: add columns if missing (for existing databases)
try:
conn.execute("ALTER TABLE wled_devices ADD COLUMN online INTEGER DEFAULT 0")
except Exception:
pass
try:
conn.execute("ALTER TABLE wled_devices ADD COLUMN leds INTEGER DEFAULT 0")
except Exception:
pass
try:
conn.execute("ALTER TABLE wled_devices ADD COLUMN version TEXT DEFAULT ''")
except Exception:
pass
try:
conn.execute("ALTER TABLE wled_devices ADD COLUMN last_check TIMESTAMP")
except Exception:
pass
try:
conn.execute("ALTER TABLE wled_devices ADD COLUMN mac TEXT DEFAULT ''")
except Exception:
pass
try:
conn.execute("ALTER TABLE users ADD COLUMN phone_extension TEXT DEFAULT ''")
except Exception:
pass
try:
conn.execute("ALTER TABLE state ADD COLUMN phone_active INTEGER DEFAULT 0")
except Exception:
pass
# Migrate: rename aida columns to attendance
try:
conn.execute("ALTER TABLE state RENAME COLUMN last_aida_status TO last_attendance_status")
except Exception:
pass
try:
conn.execute("ALTER TABLE activity_log RENAME COLUMN aida_status TO attendance_status")
except Exception:
pass
# Migrate: rename aida settings keys
for old_key, new_key in [
("aida_csv_path", "attendance_csv_path"),
("aida_status_anwesend", "attendance_status_anwesend"),
("aida_status_abwesend", "attendance_status_abwesend"),
("aida_status_homeoffice", "attendance_status_homeoffice"),
]:
old_val = conn.execute("SELECT value FROM settings WHERE key=?", (old_key,)).fetchone()
if old_val:
conn.execute("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", (new_key, old_val["value"]))
conn.execute("DELETE FROM settings WHERE key=?", (old_key,))
defaults = {
"azure_client_id": "",
"azure_client_secret": "",
"azure_tenant_id": "",
"azure_auth_tenant": "common",
"azure_scopes": "User.Read User.Read.All Presence.Read Presence.Read.All",
"azure_auth_record": "",
"poll_interval": "15",
"attendance_csv_path": "",
"attendance_status_anwesend": "~~1~~",
"attendance_status_abwesend": "~~0~~",
"attendance_status_homeoffice": "~~2~~",
"selfservice_device_id": "",
"selfservice_segment": "0",
}
for key, value in defaults.items():
conn.execute(
"INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)",
(key, value),
)
default_colors = [
("abwesend", 0, 0, 0, ""),
("homeoffice", 255, 255, 0, ""),
("available", 0, 255, 0, ""),
("teams_available", 0, 255, 0, ""),
("teams_away", 0, 255, 0, ""),
("teams_busy", 255, 0, 0, ""),
("teams_donotdisturb", 255, 0, 255, ""),
("teams_inacall", 255, 0, 255, ""),
("teams_inameeting", 255, 0, 0, ""),
("teams_presenting", 200, 200, 200, ""),
("teams_inaconferencecall", 200, 200, 200, ""),
("teams_berightback", 0, 255, 0, ""),
("teams_offline", 0, 255, 0, ""),
("teams_inactive", 0, 255, 0, ""),
("teams_offwork", 0, 255, 0, ""),
("teams_outofoffice", 0, 255, 0, ""),
("phone_active", 255, 0, 0, ""),
]
for status, r, g, b, effect in default_colors:
conn.execute(
"INSERT OR IGNORE INTO color_rules (status, r, g, b, effect) VALUES (?, ?, ?, ?, ?)",
(status, r, g, b, effect),
)
conn.commit()
conn.close()
def get_setting(key, default=""):
conn = get_db()
row = conn.execute("SELECT value FROM settings WHERE key = ?", (key,)).fetchone()
conn.close()
return row["value"] if row else default
def set_setting(key, value):
conn = get_db()
conn.execute(
"INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)",
(key, str(value)),
)
conn.commit()
conn.close()
def get_all_settings():
conn = get_db()
rows = conn.execute("SELECT key, value FROM settings").fetchall()
conn.close()
return {row["key"]: row["value"] for row in rows}
def log_activity(user_azure_id, user_name, attendance_status, teams_status, wled_url, result):
from datetime import datetime
conn = get_db()
conn.execute(
"""INSERT INTO activity_log
(timestamp, user_azure_id, user_name, attendance_status, teams_status, wled_url, result)
VALUES (?, ?, ?, ?, ?, ?, ?)""",
(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), user_azure_id, user_name, attendance_status, teams_status, wled_url, result),
)
conn.execute(
"DELETE FROM activity_log WHERE id NOT IN (SELECT id FROM activity_log ORDER BY id DESC LIMIT 500)"
)
conn.commit()
conn.close()