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>
157 lines
5 KiB
Python
157 lines
5 KiB
Python
"""BLF-Panel - Besetzt-Lampenfeld für Extension-Status-Überwachung."""
|
|
|
|
import pjsua2 as pj
|
|
|
|
from PySide6.QtWidgets import (
|
|
QWidget, QVBoxLayout, QGridLayout, QPushButton, QLabel,
|
|
)
|
|
from PySide6.QtCore import Signal, Qt
|
|
from PySide6.QtGui import QColor
|
|
|
|
|
|
class BlfBuddy(pj.Buddy):
|
|
"""PJSUA2-Buddy für Presence-Monitoring einer Extension."""
|
|
|
|
def __init__(self, extension, callback=None):
|
|
pj.Buddy.__init__(self)
|
|
self.extension = extension
|
|
self._callback = callback
|
|
|
|
def onBuddyState(self):
|
|
"""Wird aufgerufen wenn sich der Presence-Status ändert."""
|
|
bi = self.getInfo()
|
|
if self._callback:
|
|
self._callback(self.extension, {
|
|
"status": bi.presMonitorEnabled,
|
|
"activity": bi.presStatus.activity,
|
|
"statusText": bi.presStatus.statusText,
|
|
"online": bi.presStatus.status == pj.PJSUA_BUDDY_STATUS_ONLINE,
|
|
})
|
|
|
|
|
|
class BlfPanel(QWidget):
|
|
"""Panel mit Status-LEDs für konfigurierte Extensions."""
|
|
|
|
# Signal: Extension zum Anrufen angeklickt
|
|
extension_geklickt = Signal(str)
|
|
|
|
# Status-Farben
|
|
FARBEN = {
|
|
"frei": "#4CAF50", # Grün
|
|
"besetzt": "#F44336", # Rot
|
|
"klingelt": "#FF9800", # Orange
|
|
"offline": "#9E9E9E", # Grau
|
|
}
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self._buddies = {} # extension → BlfBuddy
|
|
self._buttons = {} # extension → QPushButton
|
|
self._account = None
|
|
self._server = ""
|
|
self._ui_aufbauen()
|
|
|
|
def _ui_aufbauen(self):
|
|
self._layout = QVBoxLayout(self)
|
|
self._layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
titel = QLabel("BLF-Status")
|
|
titel.setStyleSheet("font-weight: bold; margin-bottom: 4px;")
|
|
self._layout.addWidget(titel)
|
|
|
|
self._grid = QGridLayout()
|
|
self._grid.setSpacing(4)
|
|
self._layout.addLayout(self._grid)
|
|
self._layout.addStretch()
|
|
|
|
def konfigurieren(self, account, server, extensions):
|
|
"""BLF-Panel mit Extensions konfigurieren.
|
|
|
|
Args:
|
|
account: MeinAccount-Instanz (PJSUA2-Account)
|
|
server: FreePBX Server-IP
|
|
extensions: Liste von Extension-Nummern (z.B. ["200", "201"])
|
|
"""
|
|
self._account = account
|
|
self._server = server
|
|
|
|
# Bestehende Buddies aufräumen
|
|
self._buddies_aufraumen()
|
|
|
|
# Bestehende Buttons entfernen
|
|
for btn in self._buttons.values():
|
|
self._grid.removeWidget(btn)
|
|
btn.deleteLater()
|
|
self._buttons.clear()
|
|
|
|
# Neue Buttons und Buddies erstellen
|
|
for i, ext in enumerate(extensions):
|
|
zeile = i // 4 # 4 Buttons pro Zeile
|
|
spalte = i % 4
|
|
|
|
btn = QPushButton(ext)
|
|
btn.setMinimumSize(60, 40)
|
|
btn.setStyleSheet(
|
|
f"background-color: {self.FARBEN['offline']}; "
|
|
"color: white; font-weight: bold; border-radius: 4px;"
|
|
)
|
|
btn.clicked.connect(
|
|
lambda checked, e=ext: self.extension_geklickt.emit(e)
|
|
)
|
|
self._grid.addWidget(btn, zeile, spalte)
|
|
self._buttons[ext] = btn
|
|
|
|
# Buddy für Presence-Monitoring erstellen
|
|
if account:
|
|
self._buddy_erstellen(ext)
|
|
|
|
def _buddy_erstellen(self, extension):
|
|
"""PJSUA2-Buddy für eine Extension erstellen."""
|
|
try:
|
|
buddy = BlfBuddy(extension, callback=self._status_update)
|
|
buddy_cfg = pj.BuddyConfig()
|
|
buddy_cfg.uri = f"sip:{extension}@{self._server}"
|
|
buddy_cfg.subscribe = True
|
|
buddy.create(self._account, buddy_cfg)
|
|
self._buddies[extension] = buddy
|
|
except pj.Error:
|
|
pass # Extension nicht erreichbar
|
|
|
|
def _status_update(self, extension, status):
|
|
"""Callback: Status einer Extension hat sich geändert."""
|
|
if extension not in self._buttons:
|
|
return
|
|
|
|
btn = self._buttons[extension]
|
|
|
|
if status.get("online"):
|
|
# Online → prüfe Activity
|
|
activity = status.get("activity", 0)
|
|
if activity == pj.PJRPID_ACTIVITY_BUSY:
|
|
farbe = self.FARBEN["besetzt"]
|
|
else:
|
|
farbe = self.FARBEN["frei"]
|
|
else:
|
|
farbe = self.FARBEN["offline"]
|
|
|
|
btn.setStyleSheet(
|
|
f"background-color: {farbe}; "
|
|
"color: white; font-weight: bold; border-radius: 4px;"
|
|
)
|
|
|
|
tooltip = status.get("statusText", "")
|
|
if tooltip:
|
|
btn.setToolTip(f"{extension}: {tooltip}")
|
|
|
|
def _buddies_aufraumen(self):
|
|
"""Alle Buddies sauber herunterfahren."""
|
|
for buddy in self._buddies.values():
|
|
try:
|
|
buddy.subscribePresence(False)
|
|
except pj.Error:
|
|
pass
|
|
self._buddies.clear()
|
|
|
|
def aufraumen(self):
|
|
"""Ressourcen freigeben."""
|
|
self._buddies_aufraumen()
|