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

141 lines
4 KiB
Python

#!/usr/bin/env python3
"""SIP Softphone - Desktop-Anwendung für FreePBX/Asterisk.
Basiert auf PJSUA2 (SIP-Stack) und PySide6 (Qt-GUI).
Verbindet sich über SIP/UDP direkt mit der FreePBX-Anlage.
Voraussetzungen:
- Python 3.10+
- PySide6: pip install PySide6
- pjsua2: yay -S python-pjproject (AUR, Manjaro/Arch)
Starten:
python3 main.py
Click-to-Call (tel: URI):
python3 main.py tel:+49123456789
python3 main.py sip:200@server
"""
import re
import sys
import signal
from PySide6.QtWidgets import QApplication
from PySide6.QtCore import Qt
# D-Bus Single-Instance
DBUS_SERVICE = "com.alleswattlaeuft.sipwebapp"
DBUS_PATH = "/com/alleswattlaeuft/sipwebapp"
try:
import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop
DBUS_VERFUEGBAR = True
except ImportError:
DBUS_VERFUEGBAR = False
def _uri_zu_nummer(uri):
"""tel: oder sip: URI in Rufnummer umwandeln."""
if not uri:
return ""
# tel:+49123456789 → +49123456789
nummer = re.sub(r"^(tel:|sip:)", "", uri, flags=re.IGNORECASE)
# sip:200@server → 200
nummer = re.sub(r"@.*$", "", nummer)
# Leerzeichen und Sonderzeichen entfernen
nummer = re.sub(r"[^\d+*#]", "", nummer)
return nummer
def _uri_aus_args():
"""tel:/sip: URI aus Kommandozeilenargumenten extrahieren."""
for arg in sys.argv[1:]:
if arg.lower().startswith(("tel:", "sip:")):
return arg
return ""
class _DBusService(dbus.service.Object):
"""D-Bus Service für Single-Instance und URI-Weiterleitung."""
def __init__(self, bus_name, fenster):
super().__init__(bus_name, DBUS_PATH)
self._fenster = fenster
@dbus.service.method(DBUS_SERVICE, in_signature="s")
def anruf_von_uri(self, uri):
"""Von zweiter Instanz aufgerufen: URI → Anruf starten."""
nummer = _uri_zu_nummer(uri)
if nummer and self._fenster:
self._fenster.show()
self._fenster.activateWindow()
self._fenster.raise_()
# Nummer ins Wählfeld setzen und anrufen
self._fenster.waehlfeld.nummer_eingabe.setText(nummer)
self._fenster._anruf_mit_nummer(nummer)
def main():
# Ctrl+C im Terminal erlauben
signal.signal(signal.SIGINT, signal.SIG_DFL)
uri = _uri_aus_args()
# D-Bus Mainloop MUSS vor allem anderen initialisiert werden
if DBUS_VERFUEGBAR:
DBusGMainLoop(set_as_default=True)
# D-Bus Single-Instance: Prüfen ob bereits eine Instanz läuft
if DBUS_VERFUEGBAR and uri:
try:
bus = dbus.SessionBus()
proxy = bus.get_object(DBUS_SERVICE, DBUS_PATH)
iface = dbus.Interface(proxy, DBUS_SERVICE)
iface.anruf_von_uri(uri)
# Erfolgreich an laufende Instanz weitergeleitet
sys.exit(0)
except dbus.DBusException:
pass # Keine laufende Instanz → normal starten
app = QApplication(sys.argv)
app.setApplicationName("SIP Softphone")
app.setOrganizationName("AllesWattLaeuft")
app.setApplicationVersion("1.0.0")
# High-DPI-Unterstützung
app.setHighDpiScaleFactorRoundingPolicy(
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
)
from ui.hauptfenster import HauptFenster
fenster = HauptFenster()
# D-Bus Service registrieren (für Click-to-Call von Browser)
dbus_service = None
if DBUS_VERFUEGBAR:
try:
bus = dbus.SessionBus()
bus_name = dbus.service.BusName(DBUS_SERVICE, bus=bus)
dbus_service = _DBusService(bus_name, fenster)
except dbus.DBusException:
pass
fenster.show()
fenster.starten()
# Wenn mit URI gestartet, Anruf auslösen
if uri:
nummer = _uri_zu_nummer(uri)
if nummer:
# Kurz warten bis Registrierung durch ist
from PySide6.QtCore import QTimer
QTimer.singleShot(2000, lambda: fenster._anruf_mit_nummer(nummer))
sys.exit(app.exec())
if __name__ == "__main__":
main()