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>
This commit is contained in:
2026-08-21 08:12:42 +00:00
co-authored by Claude Opus 5
parent 65314f2338
commit 086b21dc2b
6 changed files with 554 additions and 3 deletions
+61
View File
@@ -72,6 +72,67 @@ def get_color_for_status(status, conn):
return {"r": 200, "g": 200, "b": 200, "effect": ""}
def current_color_for_user(azure_id, conn):
"""Farbe fuer den zuletzt gespeicherten Status eines Users.
Prioritaet wie im Polling und im Telefon-Webhook: Telefon > Teams > Zeiterfassung.
Wer laut Zeiterfassung nicht anwesend/homeoffice ist, bekommt die abwesend-Farbe.
"""
row = conn.execute("""
SELECT last_attendance_status, last_teams_status,
COALESCE(phone_active, 0) as phone_active
FROM state WHERE user_azure_id = ?
""", (azure_id,)).fetchone()
attendance = (row["last_attendance_status"] or "") if row else ""
teams = (row["last_teams_status"] or "") if row else ""
phone_active = row["phone_active"] if row else 0
if attendance not in ("anwesend", "homeoffice"):
return get_color_for_status("abwesend", conn)
if phone_active:
return get_color_for_status("phone_active", conn)
if attendance == "homeoffice":
return get_color_for_status("homeoffice", conn)
if teams:
return get_color_for_status(teams, conn)
return get_color_for_status("available", conn)
def push_user_light(azure_id, display_name, hostname, segment, conn, reason="zentrale"):
"""Aktuelle Farbe eines Users sofort an WLED senden.
Wird beim Platzwechsel gebraucht: _process_user schickt nur bei einer
Statusaenderung etwas ans Geraet, nach einem Wechsel ist der Status aber
unveraendert. Ohne diesen Push wuerde das Segment die Farbe des Vorgaengers
behalten, bis sich zufaellig etwas aendert.
"""
color = current_color_for_user(azure_id, conn)
url = build_wled_url(
hostname, color["r"], color["g"], color["b"],
segment=segment, effect=color.get("effect", ""),
)
success, result_text = call_wled(url)
log_activity(azure_id, display_name, reason, "-", url, result_text)
if not success:
logger.warning("WLED Fehler beim Platzwechsel fuer %s: %s", display_name, result_text)
return success, result_text
def clear_segment(hostname, segment, conn, reason="zentrale_frei"):
"""Segment auf die abwesend-Farbe setzen (Standard ist 0,0,0 = aus)."""
color = get_color_for_status("abwesend", conn)
url = build_wled_url(
hostname, color["r"], color["g"], color["b"],
segment=segment, effect=color.get("effect", ""),
)
success, result_text = call_wled(url)
log_activity(None, "-", reason, "-", url, result_text)
if not success:
logger.warning("WLED Fehler beim Freigeben von %s Segment %s: %s", hostname, segment, result_text)
return success, result_text
def poll_status():
"""Main polling function - called periodically by APScheduler."""
global graph