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 @@
|
||||
"""The gsmnode integration.
|
||||
|
||||
Sends SMS and places phone calls through the gsmnode API Server. Configured
|
||||
from the UI (Settings → Devices & Services → Add Integration → gsmnode).
|
||||
|
||||
Exposes two services — `gsmnode.send_sms` and `gsmnode.call` — and an
|
||||
"API Server" connectivity binary sensor. A legacy `notify.gsmnode` platform
|
||||
(YAML) is also available for backward compatibility (see notify.py / README).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .client import GsmNodeClient, GsmNodeConnectionError
|
||||
from .const import (
|
||||
CONF_API_BASE,
|
||||
CONF_DEVICE_ID,
|
||||
DOMAIN,
|
||||
SERVICE_CALL,
|
||||
SERVICE_SEND_SMS,
|
||||
)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR]
|
||||
|
||||
SEND_SMS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("phone_numbers"): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Required("message"): cv.string,
|
||||
vol.Optional("device_id"): cv.string,
|
||||
vol.Optional("sim_number"): vol.Coerce(int),
|
||||
}
|
||||
)
|
||||
|
||||
CALL_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("phone_number"): cv.string,
|
||||
vol.Optional("device_id"): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up gsmnode from a config entry."""
|
||||
client = GsmNodeClient(
|
||||
hass,
|
||||
entry.data[CONF_API_BASE],
|
||||
entry.data[CONF_EMAIL],
|
||||
entry.data[CONF_PASSWORD],
|
||||
entry.data.get(CONF_DEVICE_ID),
|
||||
)
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = client
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
_async_register_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
def _async_register_services(hass: HomeAssistant) -> None:
|
||||
"""Register the send_sms / call services (once)."""
|
||||
if hass.services.has_service(DOMAIN, SERVICE_SEND_SMS):
|
||||
return
|
||||
|
||||
def _first_client() -> GsmNodeClient | None:
|
||||
for value in hass.data.get(DOMAIN, {}).values():
|
||||
if isinstance(value, GsmNodeClient):
|
||||
return value
|
||||
return None
|
||||
|
||||
async def handle_send_sms(call: ServiceCall) -> None:
|
||||
client = _first_client()
|
||||
if client is None:
|
||||
raise GsmNodeConnectionError("no gsmnode configured")
|
||||
await client.send_sms(
|
||||
call.data["phone_numbers"],
|
||||
call.data["message"],
|
||||
call.data.get("device_id"),
|
||||
call.data.get("sim_number"),
|
||||
)
|
||||
|
||||
async def handle_call(call: ServiceCall) -> None:
|
||||
client = _first_client()
|
||||
if client is None:
|
||||
raise GsmNodeConnectionError("no gsmnode configured")
|
||||
await client.place_call(call.data["phone_number"], call.data.get("device_id"))
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SEND_SMS, handle_send_sms, schema=SEND_SMS_SCHEMA
|
||||
)
|
||||
hass.services.async_register(DOMAIN, SERVICE_CALL, handle_call, schema=CALL_SCHEMA)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id, None)
|
||||
if not hass.data[DOMAIN]:
|
||||
hass.services.async_remove(DOMAIN, SERVICE_SEND_SMS)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_CALL)
|
||||
return unload_ok
|
||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,66 @@
|
||||
"""API Server connectivity binary sensor."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
|
||||
from .client import GsmNodeClient
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the connectivity sensor for a config entry."""
|
||||
client: GsmNodeClient = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
coordinator: DataUpdateCoordinator[bool] = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="gsmnode_health",
|
||||
update_method=client.health,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
async_add_entities([GsmNodeHealthSensor(coordinator, entry)])
|
||||
|
||||
|
||||
class GsmNodeHealthSensor(CoordinatorEntity[DataUpdateCoordinator[bool]], BinarySensorEntity):
|
||||
"""Reports whether the API Server is reachable."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = "API Server"
|
||||
_attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
|
||||
|
||||
def __init__(self, coordinator: DataUpdateCoordinator[bool], entry: ConfigEntry) -> None:
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = f"{entry.entry_id}_api_health"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, entry.entry_id)},
|
||||
name="gsmnode",
|
||||
manufacturer="gsmnode",
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""True when the API Server responded OK on the last check."""
|
||||
return bool(self.coordinator.data)
|
||||
@@ -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
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Config flow for the gsmnode integration (UI setup)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||
from homeassistant.helpers import selector
|
||||
|
||||
from .client import (
|
||||
GsmNodeAuthError,
|
||||
GsmNodeClient,
|
||||
GsmNodeConnectionError,
|
||||
)
|
||||
from .const import CONF_API_BASE, CONF_DEVICE_ID, DEFAULT_API_BASE, DOMAIN
|
||||
|
||||
STEP_USER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_BASE, default=DEFAULT_API_BASE): str,
|
||||
vol.Required(CONF_EMAIL): str,
|
||||
vol.Required(CONF_PASSWORD): selector.TextSelector(
|
||||
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
|
||||
),
|
||||
vol.Optional(CONF_DEVICE_ID): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class GsmNodeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle the UI configuration flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
client = GsmNodeClient(
|
||||
self.hass,
|
||||
user_input[CONF_API_BASE],
|
||||
user_input[CONF_EMAIL],
|
||||
user_input[CONF_PASSWORD],
|
||||
user_input.get(CONF_DEVICE_ID),
|
||||
)
|
||||
try:
|
||||
await client.login()
|
||||
except GsmNodeAuthError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except GsmNodeConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
api_base = user_input[CONF_API_BASE].rstrip("/")
|
||||
await self.async_set_unique_id(f"{api_base}::{user_input[CONF_EMAIL]}")
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=f"{user_input[CONF_EMAIL]} ({api_base})",
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_SCHEMA, errors=errors
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
"""Constants for the gsmnode integration."""
|
||||
|
||||
DOMAIN = "gsmnode"
|
||||
|
||||
CONF_API_BASE = "api_base"
|
||||
CONF_DEVICE_ID = "device_id"
|
||||
|
||||
DEFAULT_API_BASE = "http://localhost:8080"
|
||||
DEFAULT_NAME = "gsmnode"
|
||||
|
||||
# Service names registered by the integration.
|
||||
SERVICE_SEND_SMS = "send_sms"
|
||||
SERVICE_CALL = "call"
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"domain": "gsmnode",
|
||||
"name": "gsmnode",
|
||||
"version": "1.2.0",
|
||||
"config_flow": true,
|
||||
"documentation": "https://github.com/your-org/sms-gateway",
|
||||
"issue_tracker": "https://github.com/your-org/sms-gateway/issues",
|
||||
"dependencies": [],
|
||||
"codeowners": [],
|
||||
"requirements": [],
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling"
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
"""gsmnode notify platform.
|
||||
|
||||
Sends SMS through the gsmnode API Server's `/api/messages` endpoint. The
|
||||
API Server is the only thing that talks to PocketBase, so this integration only
|
||||
needs the API Server's URL and a user login.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_DATA,
|
||||
ATTR_TARGET,
|
||||
PLATFORM_SCHEMA,
|
||||
BaseNotificationService,
|
||||
)
|
||||
from homeassistant.const import CONF_EMAIL, CONF_NAME, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import (
|
||||
CONF_API_BASE,
|
||||
CONF_DEVICE_ID,
|
||||
DEFAULT_API_BASE,
|
||||
DEFAULT_NAME,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_API_BASE, default=DEFAULT_API_BASE): cv.string,
|
||||
vol.Required(CONF_EMAIL): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_DEVICE_ID): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_get_service(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> "GsmNodeNotificationService":
|
||||
"""Return the gsmnode notification service."""
|
||||
return GsmNodeNotificationService(
|
||||
hass,
|
||||
config[CONF_API_BASE],
|
||||
config[CONF_EMAIL],
|
||||
config[CONF_PASSWORD],
|
||||
config.get(CONF_DEVICE_ID),
|
||||
)
|
||||
|
||||
|
||||
class GsmNodeNotificationService(BaseNotificationService):
|
||||
"""Implement the notification service for gsmnode."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
api_base: str,
|
||||
email: str,
|
||||
password: str,
|
||||
device_id: str | None,
|
||||
) -> None:
|
||||
"""Initialize the service."""
|
||||
self._hass = hass
|
||||
self._api_base = api_base.rstrip("/")
|
||||
self._email = email
|
||||
self._password = password
|
||||
self._device_id = device_id
|
||||
self._token: str | None = None
|
||||
|
||||
@property
|
||||
def _session(self):
|
||||
return async_get_clientsession(self._hass)
|
||||
|
||||
async def _login(self) -> None:
|
||||
"""Authenticate against the API Server and cache the JWT."""
|
||||
url = f"{self._api_base}/api/auth/login"
|
||||
async with self._session.post(
|
||||
url, json={"email": self._email, "password": self._password}
|
||||
) as resp:
|
||||
if resp.status != 200:
|
||||
raise RuntimeError(f"login failed: HTTP {resp.status}")
|
||||
data = await resp.json()
|
||||
self._token = data.get("access_token")
|
||||
if not self._token:
|
||||
raise RuntimeError("login response did not contain a token")
|
||||
|
||||
async def _post(self, path: str, payload: dict[str, Any]) -> int:
|
||||
"""POST to an API path; returns the HTTP status code."""
|
||||
url = f"{self._api_base}{path}"
|
||||
headers = {"Authorization": f"Bearer {self._token}"}
|
||||
async with self._session.post(url, json=payload, headers=headers) as resp:
|
||||
return resp.status
|
||||
|
||||
async def _send(self, path: str, payload: dict[str, Any]) -> None:
|
||||
"""Authenticate (if needed) and POST, retrying once on a 401."""
|
||||
try:
|
||||
if not self._token:
|
||||
await self._login()
|
||||
|
||||
status = await self._post(path, payload)
|
||||
if status == 401: # token expired — re-login once and retry
|
||||
await self._login()
|
||||
status = await self._post(path, payload)
|
||||
|
||||
if status not in (200, 201, 202):
|
||||
_LOGGER.error("gsmnode: %s failed with HTTP %s", path, status)
|
||||
except Exception as err: # noqa: BLE001 - surface any transport error
|
||||
_LOGGER.error("gsmnode: error calling %s: %s", path, err)
|
||||
|
||||
async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send an SMS, or place a phone call when `data.type` is `call`.
|
||||
|
||||
Recipient numbers come from the `target` field. Optional data overrides:
|
||||
`device_id` (which device), `sim_number` (SMS only), and `type: call` to
|
||||
dial the target(s) instead of texting.
|
||||
"""
|
||||
targets = kwargs.get(ATTR_TARGET)
|
||||
if not targets:
|
||||
_LOGGER.error("gsmnode: no target phone number(s) provided")
|
||||
return
|
||||
|
||||
data = kwargs.get(ATTR_DATA) or {}
|
||||
device_id = data.get("device_id", self._device_id)
|
||||
is_call = str(data.get("type", "")).lower() == "call"
|
||||
|
||||
if is_call:
|
||||
# One call per target number (a call has a single recipient).
|
||||
for number in targets:
|
||||
payload: dict[str, Any] = {"phone_number": number}
|
||||
if device_id:
|
||||
payload["device_id"] = device_id
|
||||
await self._send("/api/calls", payload)
|
||||
return
|
||||
|
||||
payload = {"phone_numbers": list(targets), "text_message": message}
|
||||
if device_id:
|
||||
payload["device_id"] = device_id
|
||||
if "sim_number" in data:
|
||||
payload["sim_number"] = data["sim_number"]
|
||||
await self._send("/api/messages", payload)
|
||||
@@ -0,0 +1,53 @@
|
||||
send_sms:
|
||||
name: Send SMS
|
||||
description: Send an SMS through the gateway.
|
||||
fields:
|
||||
phone_numbers:
|
||||
name: Phone numbers
|
||||
description: One or more recipient phone numbers.
|
||||
required: true
|
||||
example: '["+15551234567"]'
|
||||
selector:
|
||||
text:
|
||||
multiple: true
|
||||
message:
|
||||
name: Message
|
||||
description: The text to send.
|
||||
required: true
|
||||
example: Hello from Home Assistant
|
||||
selector:
|
||||
text:
|
||||
multiline: true
|
||||
device_id:
|
||||
name: Device
|
||||
description: Device to send from (defaults to the configured / most recent device).
|
||||
example: my-phone
|
||||
selector:
|
||||
text:
|
||||
sim_number:
|
||||
name: SIM number
|
||||
description: Which SIM to use on a dual-SIM phone.
|
||||
example: 1
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 4
|
||||
mode: box
|
||||
|
||||
call:
|
||||
name: Place call
|
||||
description: Tell a gateway device to place an outbound phone call.
|
||||
fields:
|
||||
phone_number:
|
||||
name: Phone number
|
||||
description: The number to call.
|
||||
required: true
|
||||
example: "+15551234567"
|
||||
selector:
|
||||
text:
|
||||
device_id:
|
||||
name: Device
|
||||
description: Device to call from (defaults to the configured / most recent device).
|
||||
example: my-phone
|
||||
selector:
|
||||
text:
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "gsmnode",
|
||||
"description": "Connect to your gsmnode API Server.",
|
||||
"data": {
|
||||
"api_base": "API Server URL",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"device_id": "Default device ID (optional)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect to the API Server. Check the URL and that it is reachable.",
|
||||
"invalid_auth": "Invalid email or password."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "This gateway is already configured."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"send_sms": {
|
||||
"name": "Send SMS",
|
||||
"description": "Send an SMS through the gateway.",
|
||||
"fields": {
|
||||
"phone_numbers": { "name": "Phone numbers", "description": "One or more recipient phone numbers." },
|
||||
"message": { "name": "Message", "description": "The text to send." },
|
||||
"device_id": { "name": "Device", "description": "Device to send from." },
|
||||
"sim_number": { "name": "SIM number", "description": "Which SIM to use on a dual-SIM phone." }
|
||||
}
|
||||
},
|
||||
"call": {
|
||||
"name": "Place call",
|
||||
"description": "Tell a gateway device to place an outbound phone call.",
|
||||
"fields": {
|
||||
"phone_number": { "name": "Phone number", "description": "The number to call." },
|
||||
"device_id": { "name": "Device", "description": "Device to call from." }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "gsmnode",
|
||||
"description": "Connect to your gsmnode API Server.",
|
||||
"data": {
|
||||
"api_base": "API Server URL",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"device_id": "Default device ID (optional)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect to the API Server. Check the URL and that it is reachable.",
|
||||
"invalid_auth": "Invalid email or password."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "This gateway is already configured."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"send_sms": {
|
||||
"name": "Send SMS",
|
||||
"description": "Send an SMS through the gateway.",
|
||||
"fields": {
|
||||
"phone_numbers": { "name": "Phone numbers", "description": "One or more recipient phone numbers." },
|
||||
"message": { "name": "Message", "description": "The text to send." },
|
||||
"device_id": { "name": "Device", "description": "Device to send from." },
|
||||
"sim_number": { "name": "SIM number", "description": "Which SIM to use on a dual-SIM phone." }
|
||||
}
|
||||
},
|
||||
"call": {
|
||||
"name": "Place call",
|
||||
"description": "Tell a gateway device to place an outbound phone call.",
|
||||
"fields": {
|
||||
"phone_number": { "name": "Phone number", "description": "The number to call." },
|
||||
"device_id": { "name": "Device", "description": "Device to call from." }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user