diff --git a/app.py b/app.py index 6daed2a..07f65c5 100644 --- a/app.py +++ b/app.py @@ -78,6 +78,7 @@ async def dashboard(request: Request): row = conn.execute(""" SELECT u.display_name, u.azure_id, s.last_aida_status, s.last_teams_status, + COALESCE(s.phone_active, 0) as phone_active, cr_aida.r as ar, cr_aida.g as ag, cr_aida.b as ab, cr_teams.r as tr, cr_teams.g as tg, cr_teams.b as tb FROM assignments a @@ -89,8 +90,12 @@ async def dashboard(request: Request): """, (dev["id"], seg_idx)).fetchone() if row: - # Use teams color if anwesend, otherwise aida color - if row["last_aida_status"] == "anwesend" and row["tr"] is not None: + phone = row["phone_active"] + if phone: + # Phone active: show phone color + pc = conn.execute("SELECT r,g,b FROM color_rules WHERE status='phone_active'").fetchone() + r, g, b = (pc["r"], pc["g"], pc["b"]) if pc else (255, 0, 0) + elif row["last_aida_status"] == "anwesend" and row["tr"] is not None: r, g, b = row["tr"], row["tg"], row["tb"] elif row["ar"] is not None: r, g, b = row["ar"], row["ag"], row["ab"] @@ -101,6 +106,7 @@ async def dashboard(request: Request): "user": row["display_name"], "aida": row["last_aida_status"] or "-", "teams": row["last_teams_status"] or "-", + "phone": phone, "r": r, "g": g, "b": b, }) else: @@ -167,12 +173,13 @@ async def sync_users(): async def update_user( azure_id: str, personal_nr: str = Form(""), + phone_extension: str = Form(""), active: str = Form("0"), ): conn = database.get_db() conn.execute( - "UPDATE users SET personal_nr=?, active=? WHERE azure_id=?", - (personal_nr, 1 if active == "1" else 0, azure_id), + "UPDATE users SET personal_nr=?, phone_extension=?, active=? WHERE azure_id=?", + (personal_nr, phone_extension, 1 if active == "1" else 0, azure_id), ) conn.commit() conn.close() @@ -471,8 +478,20 @@ def cli_auth(): if __name__ == "__main__": import uvicorn + import threading if len(sys.argv) > 1 and sys.argv[1] == "--auth": cli_auth() else: + # Start webhook server on port 8081 in background thread + from webhook import webhook_app + + def run_webhook(): + uvicorn.run(webhook_app, host="0.0.0.0", port=8081, log_level="info") + + webhook_thread = threading.Thread(target=run_webhook, daemon=True) + webhook_thread.start() + logger.info("Webhook-Server gestartet auf Port 8081") + + # Main web UI on port 8080 uvicorn.run(app, host="0.0.0.0", port=8080) diff --git a/database.py b/database.py index e814b61..a54f2ed 100644 --- a/database.py +++ b/database.py @@ -25,6 +25,7 @@ def init_db(): display_name TEXT NOT NULL DEFAULT '', email TEXT DEFAULT '', personal_nr TEXT DEFAULT '', + phone_extension TEXT DEFAULT '', active INTEGER NOT NULL DEFAULT 0 ); @@ -60,6 +61,7 @@ def init_db(): user_azure_id TEXT PRIMARY KEY REFERENCES users(azure_id) ON DELETE CASCADE, last_aida_status TEXT DEFAULT '', last_teams_status TEXT DEFAULT '', + phone_active INTEGER DEFAULT 0, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); @@ -92,6 +94,14 @@ def init_db(): conn.execute("ALTER TABLE wled_devices ADD COLUMN last_check TIMESTAMP") 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 defaults = { "azure_client_id": "", @@ -129,6 +139,7 @@ def init_db(): ("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( diff --git a/scheduler.py b/scheduler.py index 422eceb..48d5c76 100644 --- a/scheduler.py +++ b/scheduler.py @@ -90,7 +90,8 @@ def poll_status(): users_with_assignments = conn.execute(""" SELECT u.azure_id, u.display_name, u.personal_nr, a.segment, d.hostname, - s.last_aida_status, s.last_teams_status + s.last_aida_status, s.last_teams_status, + COALESCE(s.phone_active, 0) as phone_active FROM users u JOIN assignments a ON a.user_azure_id = u.azure_id JOIN wled_devices d ON d.id = a.wled_device_id @@ -130,6 +131,7 @@ def _process_user(user, aida_lines, settings, conn): segment = user["segment"] last_aida = user["last_aida_status"] or "" last_teams = user["last_teams_status"] or "" + phone_active = user["phone_active"] # Immer Teams-Status abfragen (fuer Dashboard-Anzeige) teams_status = "" @@ -144,8 +146,8 @@ def _process_user(user, aida_lines, settings, conn): if aida_status == last_aida and teams_status == last_teams: return # No change - # --- WLED nur steuern wenn AIDA-Status bekannt --- - wled_changed = aida_status is not None and ( + # --- WLED nur steuern wenn AIDA-Status bekannt und kein aktives Telefonat --- + wled_changed = aida_status is not None and not phone_active and ( aida_status != last_aida or (aida_status == "anwesend" and teams_status != last_teams) ) diff --git a/templates/dashboard.html b/templates/dashboard.html index 8f5e244..bd82efb 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -81,6 +81,10 @@ {{ seg.teams }} {% endif %} {% endif %} + + {% if seg.phone %} + Telefoniert + {% endif %} {% endif %} {% else %} diff --git a/templates/users.html b/templates/users.html index 6581b10..0518e70 100644 --- a/templates/users.html +++ b/templates/users.html @@ -35,6 +35,7 @@ Name E-Mail Personal-Nr (AIDA) + Durchwahl Zuordnung Aktionen @@ -57,6 +58,19 @@ class="form-control form-control-sm" style="width: 120px;" placeholder="~~00000~~"> + + + + + +
+ + + diff --git a/webhook.py b/webhook.py new file mode 100644 index 0000000..e150784 --- /dev/null +++ b/webhook.py @@ -0,0 +1,126 @@ +"""Webhook server for phone system integration (runs on port 8081).""" + +import logging +from datetime import datetime +from fastapi import FastAPI +from fastapi.responses import PlainTextResponse + +from database import get_db, log_activity +from wled_client import build_wled_url, call_wled + +logger = logging.getLogger(__name__) + +webhook_app = FastAPI(title="Busylight Webhook") + + +def _get_user_by_extension(extension, conn): + """Find user and their assignment by phone extension.""" + return conn.execute(""" + SELECT u.azure_id, u.display_name, + a.segment, d.hostname, + s.last_aida_status, s.last_teams_status, s.phone_active + FROM users u + JOIN assignments a ON a.user_azure_id = u.azure_id + JOIN wled_devices d ON d.id = a.wled_device_id + LEFT JOIN state s ON s.user_azure_id = u.azure_id + WHERE u.phone_extension = ? AND u.active = 1 + """, (extension,)).fetchone() + + +def _get_color(status, conn): + """Get color for a status.""" + row = conn.execute( + "SELECT r, g, b, effect FROM color_rules WHERE status = ?", (status,) + ).fetchone() + if row: + return dict(row) + return {"r": 200, "g": 200, "b": 200, "effect": ""} + + +@webhook_app.get("/{extension}/callstart", response_class=PlainTextResponse) +@webhook_app.post("/{extension}/callstart", response_class=PlainTextResponse) +async def call_start(extension: str): + conn = get_db() + try: + user = _get_user_by_extension(extension, conn) + if not user: + logger.warning("Webhook callstart: Durchwahl %s nicht gefunden", extension) + return f"Unknown extension: {extension}" + + aida = user["last_aida_status"] or "" + # Nur wenn anwesend oder homeoffice + if aida not in ("anwesend", "homeoffice"): + logger.info("Webhook callstart: %s ignoriert (AIDA=%s)", user["display_name"], aida) + return f"Ignored: {user['display_name']} not in office (aida={aida})" + + # WLED auf Telefon-Farbe setzen + color = _get_color("phone_active", conn) + url = build_wled_url( + user["hostname"], color["r"], color["g"], color["b"], + segment=user["segment"], effect=color.get("effect", ""), + ) + success, result_text = call_wled(url) + + # phone_active Flag setzen + conn.execute(""" + UPDATE state SET phone_active=1, updated_at=? WHERE user_azure_id=? + """, (datetime.now(), user["azure_id"])) + conn.commit() + + log_activity( + user["azure_id"], user["display_name"], + aida, "phone_active", url, result_text, + ) + + logger.info("Webhook callstart: %s -> %s", user["display_name"], result_text) + return f"OK: {user['display_name']} call started" + + finally: + conn.close() + + +@webhook_app.get("/{extension}/callend", response_class=PlainTextResponse) +@webhook_app.post("/{extension}/callend", response_class=PlainTextResponse) +async def call_end(extension: str): + conn = get_db() + try: + user = _get_user_by_extension(extension, conn) + if not user: + logger.warning("Webhook callend: Durchwahl %s nicht gefunden", extension) + return f"Unknown extension: {extension}" + + aida = user["last_aida_status"] or "" + if aida not in ("anwesend", "homeoffice"): + return f"Ignored: {user['display_name']} not in office" + + # Farbe zuruecksetzen basierend auf aktuellem Status + teams = user["last_teams_status"] or "" + if aida == "homeoffice": + color = _get_color("homeoffice", conn) + elif teams: + color = _get_color(teams, conn) + else: + color = _get_color("available", conn) + + url = build_wled_url( + user["hostname"], color["r"], color["g"], color["b"], + segment=user["segment"], effect=color.get("effect", ""), + ) + success, result_text = call_wled(url) + + # phone_active Flag zuruecksetzen + conn.execute(""" + UPDATE state SET phone_active=0, updated_at=? WHERE user_azure_id=? + """, (datetime.now(), user["azure_id"])) + conn.commit() + + log_activity( + user["azure_id"], user["display_name"], + aida, f"phone_end -> {teams}", url, result_text, + ) + + logger.info("Webhook callend: %s -> %s (%s)", user["display_name"], result_text, teams) + return f"OK: {user['display_name']} call ended" + + finally: + conn.close()