linux.sipsoftphone/ui/blf_panel.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

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()