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. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
97
channels/sms_channel.py
Normal file
97
channels/sms_channel.py
Normal file
@@ -0,0 +1,97 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user