linux.sipsoftphone/utils/config_manager.py
data 48ddb4b4af Initiales Release: SIP Softphone für FreePBX/Asterisk
PySide6/PJSUA2-basiertes Desktop-Softphone mit:
- SIP-Telefonie (Anrufe, DTMF, Halten, Transfer, Konferenz)
- CardDAV-Kontaktsync mit Write-Back (Multi-Account)
- Kontaktverwaltung mit Suche und Bearbeiten-Dialog
- Anrufliste mit Namensauflösung
- Favoriten-Panel (manuell + häufig angerufen)
- BLF-Panel (SIP Presence Monitoring)
- Responsives Layout (kompakt/erweitert)
- Click-to-Call (tel:/sip: URI via D-Bus)
- KDE-Integration (Tray, Benachrichtigungen)
- PipeWire/PulseAudio Audio-Routing
- AppImage Build-Support (PyInstaller + appimagetool)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 13:18:54 +01:00

204 lines
6.5 KiB
Python

"""Konfigurationsverwaltung - Lädt und speichert Einstellungen in JSON."""
import json
import os
from pathlib import Path
# XDG-konformer Konfigurations-Pfad
KONFIG_VERZEICHNIS = Path(
os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")
) / "sipwebapp"
KONFIG_DATEI = KONFIG_VERZEICHNIS / "config.json"
ANRUFLISTE_DATEI = KONFIG_VERZEICHNIS / "anrufliste.json"
KONTAKTE_DATEI = KONFIG_VERZEICHNIS / "kontakte.json"
# Standard-Konfiguration
STANDARD_KONFIG = {
"sip": {
"server": "192.168.154.242",
"port": 55444,
"extension": "",
"passwort": "",
"transport": "udp",
},
"audio": {
"aufnahme_geraet": "", # PipeWire-Name (leer = KDE-Standard)
"wiedergabe_geraet": "", # PipeWire-Name (leer = KDE-Standard)
"klingelton_geraet": "", # PipeWire-Name fürs Klingeln (leer = Standard)
"mikrofon_lautstaerke": 1.0,
"lautsprecher_lautstaerke": 1.0,
},
"allgemein": {
"minimieren_in_tray": True,
"autostart": False,
"klingelton": "", # Pfad zu WAV-Datei
},
"carddav": {
"accounts": [], # [{"name": "Privat", "url": "...", "benutzername": "...", "passwort": "..."}]
"auto_sync": True,
"sync_intervall": 3600, # Sekunden
},
"blf": {
"extensions": [], # Liste von Extensions die überwacht werden
},
"favoriten": {
"manuell": [], # [{"name": "...", "nummer": "..."}]
"max_anzeige": 10, # Max angezeigte Favoriten
},
}
class ConfigManager:
"""Verwaltet die Anwendungs-Konfiguration als JSON-Datei."""
def __init__(self):
self._konfig = {}
self.laden()
def laden(self):
"""Konfiguration aus JSON laden oder Standard erstellen."""
if KONFIG_DATEI.exists():
try:
with open(KONFIG_DATEI, "r", encoding="utf-8") as f:
gespeichert = json.load(f)
# Standard-Werte mit gespeicherten zusammenführen
self._konfig = self._zusammenfuehren(STANDARD_KONFIG, gespeichert)
except (json.JSONDecodeError, OSError):
self._konfig = STANDARD_KONFIG.copy()
else:
self._konfig = STANDARD_KONFIG.copy()
# Migration: Alter einzelner CardDAV-Account → accounts-Liste
cdav = self._konfig.get("carddav", {})
if cdav.get("url") and not cdav.get("accounts"):
cdav["accounts"] = [{
"name": "Standard",
"url": cdav.pop("url", ""),
"benutzername": cdav.pop("benutzername", ""),
"passwort": cdav.pop("passwort", ""),
}]
if "accounts" not in cdav:
cdav["accounts"] = []
# Alte Einzelfelder entfernen
for feld in ("url", "benutzername", "passwort"):
cdav.pop(feld, None)
def speichern(self):
"""Konfiguration als JSON speichern."""
KONFIG_VERZEICHNIS.mkdir(parents=True, exist_ok=True)
with open(KONFIG_DATEI, "w", encoding="utf-8") as f:
json.dump(self._konfig, f, indent=2, ensure_ascii=False)
def get(self, *schluessel):
"""Wert aus der Konfiguration lesen.
Beispiel: config.get("sip", "server") → "192.168.154.242"
"""
wert = self._konfig
for s in schluessel:
if isinstance(wert, dict) and s in wert:
wert = wert[s]
else:
return None
return wert
def set(self, *args):
"""Wert in der Konfiguration setzen.
Letztes Argument ist der Wert, davor die Schlüssel-Hierarchie.
Beispiel: config.set("sip", "server", "192.168.1.100")
"""
if len(args) < 2:
return
schluessel = args[:-1]
wert = args[-1]
ziel = self._konfig
for s in schluessel[:-1]:
if s not in ziel:
ziel[s] = {}
ziel = ziel[s]
ziel[schluessel[-1]] = wert
@property
def sip(self):
"""SIP-Konfiguration."""
return self._konfig.get("sip", {})
@property
def audio(self):
"""Audio-Konfiguration."""
return self._konfig.get("audio", {})
@property
def allgemein(self):
"""Allgemeine Konfiguration."""
return self._konfig.get("allgemein", {})
@property
def carddav(self):
"""CardDAV-Konfiguration."""
return self._konfig.get("carddav", {})
@property
def blf(self):
"""BLF-Konfiguration."""
return self._konfig.get("blf", {})
def hat_sip_zugangsdaten(self):
"""Prüft ob SIP-Zugangsdaten konfiguriert sind."""
sip = self.sip
return bool(sip.get("extension") and sip.get("passwort"))
def _zusammenfuehren(self, standard, gespeichert):
"""Zusammenführen: Standard-Werte + gespeicherte Werte."""
ergebnis = standard.copy()
for schluessel, wert in gespeichert.items():
if (schluessel in ergebnis and
isinstance(ergebnis[schluessel], dict) and
isinstance(wert, dict)):
ergebnis[schluessel] = self._zusammenfuehren(
ergebnis[schluessel], wert
)
else:
ergebnis[schluessel] = wert
return ergebnis
# --- Anrufliste ---
def anrufliste_laden(self):
"""Anrufliste aus JSON laden."""
if ANRUFLISTE_DATEI.exists():
try:
with open(ANRUFLISTE_DATEI, "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, OSError):
pass
return []
def anrufliste_speichern(self, anrufe):
"""Anrufliste als JSON speichern."""
KONFIG_VERZEICHNIS.mkdir(parents=True, exist_ok=True)
with open(ANRUFLISTE_DATEI, "w", encoding="utf-8") as f:
json.dump(anrufe, f, indent=2, ensure_ascii=False)
# --- Kontakte ---
def kontakte_laden(self):
"""Kontaktliste aus JSON laden."""
if KONTAKTE_DATEI.exists():
try:
with open(KONTAKTE_DATEI, "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, OSError):
pass
return []
def kontakte_speichern(self, kontakte):
"""Kontaktliste als JSON speichern."""
KONFIG_VERZEICHNIS.mkdir(parents=True, exist_ok=True)
with open(KONTAKTE_DATEI, "w", encoding="utf-8") as f:
json.dump(kontakte, f, indent=2, ensure_ascii=False)