#!/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 "" def _minimiert_starten(config=None): """Prüft ob minimiert gestartet werden soll (Config oder Kommandozeile).""" # Kommandozeile hat Vorrang (für manuelles Testen) if "--minimiert" in sys.argv or "-m" in sys.argv: return True # Sonst aus Config lesen if config: return config.allgemein.get("minimiert_starten", False) return False 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 from utils.config_manager import ConfigManager # Config laden für minimiert_starten Einstellung config = ConfigManager() 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 # Minimiert starten? (aus Config oder --minimiert Flag) minimiert = _minimiert_starten(config) if minimiert: # Direkt in Tray starten, Fenster nicht anzeigen fenster.hide() else: 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()