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:
tajniak81
2026-07-06 08:53:14 +02:00
commit d6956395c8
166 changed files with 12605 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
# gsmnode — Home Assistant Integration
A custom Home Assistant integration that sends SMS **and places phone calls**
through the gsmnode **API Server**. It can be added and configured entirely
from the Home Assistant UI.
```
Home Assistant ──► API Server (/api/messages, /api/calls) ──► your phone ──► SMS / call
```
Like every other client, it talks **only** to the API Server (never PocketBase).
## What you get
- **UI setup** (config flow) — add it under *Settings → Devices & Services*.
- **Services** — `gsmnode.send_sms` and `gsmnode.call`, with field
pickers in the automation editor and *Developer Tools → Actions*.
- **Sensor** — `binary_sensor` "API Server" (connectivity) so you can see and
automate on the gateway being up/down.
## Install
1. Copy the integration folder into your Home Assistant config directory so it
lands at:
```
<config>/custom_components/gsmnode/
```
(Copy this repo's `custom_components/gsmnode/` next to your
`configuration.yaml`. Use the Samba / File editor / SSH add-on, or the
mapped volume for Docker.)
2. **Restart Home Assistant** (Settings → System → Restart).
## Add it from the UI
1. **Settings → Devices & Services → Add Integration**.
2. Search for **gsmnode**.
3. Fill in the form:
- **API Server URL** — e.g. `http://10.2.1.101:8080` (must be reachable from HA)
- **Email** / **Password** — a gateway user (create one with
`node "API Server/scripts/create-user.mjs" ha@local "pass" "Home Assistant"`)
- **Default device ID** *(optional)* — pin sends/calls to a specific phone
The form validates by logging in; you'll get *Invalid auth* or *Cannot
connect* if something's wrong.
A **gsmnode** device appears with an **API Server** connectivity sensor.
## Use it
### Send an SMS
```yaml
action: gsmnode.send_sms
data:
phone_numbers: ["+15551234567"]
message: "Hello from Home Assistant"
device_id: my-phone # optional
sim_number: 1 # optional (dual-SIM)
```
### Place a call
```yaml
action: gsmnode.call
data:
phone_number: "+15551234567"
device_id: my-phone # optional
```
### Example automation
```yaml
automation:
- alias: "Water leak — text then call"
trigger:
- platform: state
entity_id: binary_sensor.basement_leak
to: "on"
action:
- action: gsmnode.send_sms
data:
phone_numbers: ["+15551234567"]
message: "Water leak detected in the basement!"
- action: gsmnode.call
data:
phone_number: "+15551234567"
```
### React to the gateway going offline
```yaml
automation:
- alias: "Alert if SMS gateway API is down"
trigger:
- platform: state
entity_id: binary_sensor.gsmnode_api_server
to: "off"
for: "00:02:00"
action:
- action: persistent_notification.create
data:
title: "gsmnode"
message: "The API Server is unreachable."
```
## Receiving SMS in Home Assistant
To **receive** incoming texts, register the API Server's `sms:received` webhook
against a HA webhook trigger. A ready-to-use snippet is in
[`configuration.example.yaml`](configuration.example.yaml).
## Legacy YAML `notify` platform (optional)
A `notify.gsmnode` service is still available for those who prefer YAML
(`notify.py`). It's independent of the UI integration — see
[`configuration.example.yaml`](configuration.example.yaml). For new setups, the
UI integration above is recommended.
## How it works
- On first send it logs in (`POST /api/auth/login`) and caches the JWT; on a
`401` it re-logs in once and retries.
- `send_sms` → `POST /api/messages`; `call` → `POST /api/calls`.
- The sensor polls `GET /api/health` every 30s.
- All HTTP uses Home Assistant's shared aiohttp session (fully async).
## Notes / limitations
- The API Server must be reachable from the Home Assistant host.
- No external dependencies (`requirements: []`).
@@ -0,0 +1,49 @@
# RECOMMENDED: add this integration from the UI instead
# (Settings -> Devices & Services -> Add Integration -> gsmnode).
# The blocks below are only for the OPTIONAL legacy `notify.gsmnode`
# YAML service and for receiving incoming SMS via a webhook.
# --- Optional: legacy notify.gsmnode YAML service ---------------------
notify:
- platform: gsmnode
name: gsmnode # service becomes notify.gsmnode
api_base: http://10.2.1.10:8080 # the gsmnode API Server (reachable from HA)
email: !secret gsmnode_email
password: !secret gsmnode_password
device_id: my-phone # optional: pin to a specific device
# secrets.yaml:
# gsmnode_email: you@example.com
# gsmnode_password: your-password
# --- Usage ----------------------------------------------------------------
# Send an SMS:
# service: notify.gsmnode
# data: { message: "Hello", target: ["+15551234567"] }
#
# Place a phone call (gateway phone dials the number; message is ignored):
# service: notify.gsmnode
# data: { message: "", target: ["+15551234567"], data: { type: call } }
# --- Optional: receive incoming SMS as Home Assistant events ---------------
# The API Server can POST the `sms:received` webhook to a HA webhook trigger.
#
# 1. Create an automation with a webhook trigger (gives you a webhook_id):
#
# automation:
# - alias: "gsmnode - incoming SMS"
# trigger:
# - platform: webhook
# webhook_id: gsmnode_incoming
# allowed_methods: [POST]
# local_only: true
# action:
# - service: persistent_notification.create
# data:
# title: "SMS from {{ trigger.json.payload.phone_number }}"
# message: "{{ trigger.json.payload.message }}"
#
# 2. Register the webhook with the API Server (one-time), pointing at:
# http://<home-assistant>:8123/api/webhook/gsmnode_incoming
# e.g. via the Web App "Webhooks" page or:
# POST /api/webhooks {"event":"sms:received","url":"<that url>"}
@@ -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
@@ -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." }
}
}
}
}