Files
MCM/channels/sms_channel.py
itdrui.de 7f3b4768c3 Initial MCM project: FastAPI + Textual TUI unified messenger
MultiCustomerMessenger supporting Telegram (python-telegram-bot),
WhatsApp (Green API) and SMS (python-gsmmodem-new). REST API with
Bearer-token auth, SQLAlchemy models for MariaDB, APScheduler for
background polling, and Textual TUI running in same asyncio event-loop.
2026-03-03 14:43:19 +01:00

98 lines
3.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)