Files
tajniak81 d6956395c8 Rebrand CommGate to gsmnode and adopt new design system
Rename the whole project from CommGate to gsmnode and re-skin every
surface onto the new signal-green + ink design system (Space Grotesk /
IBM Plex Sans / JetBrains Mono, flat surfaces, two-arrow routing mark,
lowercase gsm+node wordmark).

- Web App + API panel: rewrite token layer, gn-* utilities, data-gsm-theme,
  new logo/favicon assets; both builds verified.
- Phone App (Flutter): green theme, new mark/wordmark widgets, adaptive
  launcher icons; rename Android applicationId/package to app.gsmnode.phone;
  bump AGP to 8.6.0 and google_fonts to 8.1.0; debug APK build verified.
- API Server (Go): panel routes, User-Agent, health service id, branding.
- Home Assistant Plugin: rename integration domain sms_gateway to gsmnode
  (folder, DOMAIN, services, entity ids, classes); py_compile verified.
- Update all READMEs; add .gitignore (excludes Design/, build artifacts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 08:53:14 +02:00

106 lines
3.8 KiB
Python

"""Async client for the gsmnode API Server (shared by the UI integration)."""
from __future__ import annotations
import aiohttp
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
class GsmNodeAuthError(Exception):
"""Raised when the API Server rejects the credentials."""
class GsmNodeConnectionError(Exception):
"""Raised when the API Server can't be reached or returns an error."""
class GsmNodeClient:
"""Talks to the API Server: login, send SMS, place calls, health."""
def __init__(
self,
hass: HomeAssistant,
api_base: str,
email: str,
password: str,
device_id: str | None = None,
) -> None:
self._session = async_get_clientsession(hass)
self._api_base = api_base.rstrip("/")
self._email = email
self._password = password
self.device_id = device_id
self._token: str | None = None
async def login(self) -> None:
"""Authenticate and cache the JWT."""
try:
async with self._session.post(
f"{self._api_base}/api/auth/login",
json={"email": self._email, "password": self._password},
) as resp:
if resp.status == 401:
raise GsmNodeAuthError("invalid credentials")
if resp.status != 200:
raise GsmNodeConnectionError(f"HTTP {resp.status}")
data = await resp.json()
self._token = data.get("access_token")
if not self._token:
raise GsmNodeAuthError("no token in response")
except aiohttp.ClientError as err:
raise GsmNodeConnectionError(str(err)) from err
async def _post(self, path: str, payload: dict) -> int:
headers = {"Authorization": f"Bearer {self._token}"} if self._token else {}
async with self._session.post(
f"{self._api_base}{path}", json=payload, headers=headers
) as resp:
return resp.status
async def _send(self, path: str, payload: dict) -> None:
"""POST with auth, re-logging in once on a 401."""
try:
if not self._token:
await self.login()
status = await self._post(path, payload)
if status == 401:
await self.login()
status = await self._post(path, payload)
except aiohttp.ClientError as err:
raise GsmNodeConnectionError(str(err)) from err
if status not in (200, 201, 202):
raise GsmNodeConnectionError(f"{path} -> HTTP {status}")
async def send_sms(
self,
phone_numbers: list[str],
message: str,
device_id: str | None = None,
sim_number: int | None = None,
) -> None:
"""Queue an outbound SMS."""
payload: dict = {"phone_numbers": phone_numbers, "text_message": message}
dev = device_id or self.device_id
if dev:
payload["device_id"] = dev
if sim_number is not None:
payload["sim_number"] = sim_number
await self._send("/api/messages", payload)
async def place_call(self, phone_number: str, device_id: str | None = None) -> None:
"""Queue an outbound phone call."""
payload: dict = {"phone_number": phone_number}
dev = device_id or self.device_id
if dev:
payload["device_id"] = dev
await self._send("/api/calls", payload)
async def health(self) -> bool:
"""Return True if the API Server's health endpoint responds OK."""
try:
async with self._session.get(f"{self._api_base}/api/health") as resp:
return resp.status == 200
except aiohttp.ClientError:
return False