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>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user