from __future__ import annotations import asyncio import logging from typing import Any, Callable, Awaitable from channels.base import BaseChannel from config import settings logger = logging.getLogger(__name__) class SmsChannel(BaseChannel): """SMS-Kanal via python-gsmmodem-new (USB-Modem).""" def __init__(self) -> None: self._modem: Any = None # gsmmodem.modem.GsmModem self._inbound_callback: Callable[[dict[str, Any]], Awaitable[None]] | None = None self._loop: asyncio.AbstractEventLoop | None = None def set_inbound_callback(self, cb: Callable[[dict[str, Any]], Awaitable[None]]) -> None: self._inbound_callback = cb # ── BaseChannel interface ────────────────────────────────────────────────── async def send_message( self, recipient: str, text: str, reply_to_id: str | None = None, ) -> dict[str, Any]: if not settings.sms_enabled or not self._modem: return {"success": False, "channel_message_id": None, "error": "SMS not available"} try: loop = asyncio.get_event_loop() await loop.run_in_executor(None, self._modem.sendSms, recipient, text) return {"success": True, "channel_message_id": None, "error": None} except Exception as exc: logger.error("SMS send error: %s", exc) return {"success": False, "channel_message_id": None, "error": str(exc)} async def check_connection(self) -> tuple[bool, str]: if not settings.sms_enabled: return False, "SMS disabled in config" if not self._modem: return False, "Modem not initialized" try: loop = asyncio.get_event_loop() signal = await loop.run_in_executor(None, self._modem.signalStrength) return True, f"Signal: {signal}%" except Exception as exc: return False, str(exc) async def start(self) -> None: if not settings.sms_enabled: logger.info("SMS disabled (sms_enabled=false in config)") return self._loop = asyncio.get_event_loop() try: from gsmmodem.modem import GsmModem # type: ignore modem = GsmModem( settings.sms_port, settings.sms_baud_rate, incomingSmsCallbackFunc=self._sms_received_sync, ) await self._loop.run_in_executor(None, modem.connect) self._modem = modem logger.info("SMS channel started on %s", settings.sms_port) except Exception as exc: logger.error("SMS channel start error: %s", exc) async def stop(self) -> None: if self._modem: try: loop = asyncio.get_event_loop() await loop.run_in_executor(None, self._modem.close) except Exception: pass logger.info("SMS channel stopped") # ── Interner Callback (sync → async bridge) ──────────────────────────────── def _sms_received_sync(self, sms: Any) -> None: """Wird vom Modem-Thread aufgerufen – leitet an async weiter.""" if not self._inbound_callback or not self._loop: return payload: dict[str, Any] = { "channel": "sms", "channel_message_id": None, "sender_phone": sms.number, "sender_name": sms.number, "chat_id": sms.number, "text": sms.text, "reply_to_id": None, } asyncio.run_coroutine_threadsafe(self._inbound_callback(payload), self._loop)