Licensed Apache-2.0. Anyone is free to implement clients against this spec — that is its purpose.
This document describes the local HTTP and WebSocket API exposed by SmartGhar Hubs (TankSync hub, and future product variants). The hub broadcasts itself on mDNS and answers requests on the home LAN. No cloud or internet access is required for any operation in this spec.
The TankSync cloud and PWA continue to operate alongside this protocol — they are unrelated, run in parallel, and are not a dependency for any client implementing this spec.
This is v1. Hub firmware reports its supported version in GET /api/v1/info. Clients should:
- Treat unknown fields as forward-compatible (additive changes within v1.x are allowed)
- Treat unknown endpoints as forward-compatible
- Bump to the matching
v2namespace if/when v2 is ever published - Pin minimum version in their manifests/dependencies
Breaking changes to v1 will not happen. v2 (if ever) goes through a 2-week public discussion thread and a deprecation window.
Hubs broadcast on the local network with the following service:
Service: _smartghar._tcp.local.
Port: 80 (HTTP) / 80 (WebSocket upgrade)
TXT records:
version=1 # protocol schema major version
hub_id=<uppercase MAC bytes> # globally unique, never changes
hub_name=<user-set name> # may contain UTF-8
fw=<firmware version> # e.g. "2.8.0"
device_kinds=tank[,power,...] # comma-separated kinds attached
auth=none|token # whether requests need a bearer token
Clients should subscribe to the service type and treat each unique hub_id as one logical hub.
Two modes:
auth=none— default for home networks. Any client on LAN can read state and issue commands.auth=token— opt-in via hub web UI. Clients must includeAuthorization: Bearer <token>on every request, including the WebSocket handshake. Tokens are hub-scoped, generated on the hub, displayed once in the web UI for the user to paste into the client.
There is no per-user permission model in v1. Either you can talk to the hub or you can't.
All endpoints return JSON (Content-Type: application/json). Errors return { "error": "<code>", "detail": "<human-readable>" } with appropriate HTTP status.
Hub identity and runtime info.
{
"schema_version": "1.0",
"hub_id": "A1B2C3D4E5F6",
"hub_name": "Rooftop",
"fw_version": "2.8.0",
"uptime_s": 86412,
"wifi_rssi": -54,
"claimed": true,
"claim_account": "owner@example.com",
"device_kinds": ["tank"],
"ota": {
"current": "2.8.0",
"available": null,
"channel": "stable"
}
}All attached devices (tanks for now; multi-product as ecosystem expands).
{
"devices": [
{
"id": 1,
"kind": "tank",
"name": "Drinking",
"fw": "2.0.11",
"mac": "d83bda12ecb0",
"state": { "level_pct": 67.4, "voltage": 4.02, "rssi": -78, "ts": 1709123456, "sensor_error": false, "sensor_stuck": false },
"config": { "capacity_l": 1000, "low_threshold_pct": 20, "sensor_kind": "ultrasonic" }
}
]
}mac: 12-char lowercase hex of the TX's eFuse MAC. This is the recommended anchor forunique_idin client integrations — it survives re-pairs, hub firmware upgrades, and address reassignment. Empty string ("") means the entry was paired before the MAC was added to the protocol (rx < 2.7.10); clients should fall back toidfor those legacy entries and degrade UX accordingly (rename/delete continuity not guaranteed).id: LoRa address (1-65535). For new pairings on rx-v2.7.10+ this is a small int (1-99). For legacy entries it may be a large random number (e.g. 26960). May change on a re-pair after delete only if the live registry has no MAC match; with MAC match, the original address is preserved.fw: TX firmware version string."unknown"if the TX hasn't reported a wake yet.state.sensor_error(added rx-v2.8.0): true when the TX is alive but its ultrasonic sensor failed to echo on the last read. Thelevel_pctshown is the prior good value. Absent on older firmware — treat absence asfalse.state.sensor_stuck(added rx-v2.8.3): true when variance check over a 20-sample window detected a constant reading regardless of actual water level (defective ultrasonic module). Different fromsensor_error— here the read succeeds but returns a plausible-but-stuck value. Absent on older firmware — treat absence asfalse.
Single device with full state and config.
Device's local 48-slot half-hourly buffer.
{
"samples": [
{ "ts": 1709120000, "level_pct": 65.0, "voltage": 4.05 },
{ "ts": 1709121800, "level_pct": 66.2, "voltage": 4.04 }
]
}Edit device config. Body is a partial update; only present fields change.
{ "name": "Drinking Tank", "low_threshold_pct": 25 }Hub validates, persists to NVS, and propagates the change via the existing PWA-config-sync MQTT path so the cloud and PWA stay in sync.
Blink the LEDs associated with this device for ~5 seconds. No body. 204 on success.
Read/write LED config (brightness, mode, per-tank colors).
Read/write OLED display config (brightness, rotation).
Read/write hub buzzer config. Body shape:
{
"master_enable": true,
"master_profile": 1,
"quiet_start_hour": 22,
"quiet_end_hour": 7,
"critical_overrides_quiet": true,
"alerts": [
{ "event": 0, "name": "Boot complete", "enabled": true, "tier": 1 },
{ "event": 1, "name": "Critical low (<5%)", "enabled": true, "tier": 1 }
]
}master_profile is the global volume: 0=Quiet, 1=Standard, 2=Loud. Quiet hours are local-time integer hours (0-23); set both equal to disable. The alerts array enumerates every buzzer event with its enabled flag and a tier (1=essential, 2=default-on, 3=opt-in). PUT bodies are partial — only present fields change.
PUT validates and persists to NVS. Hub firmware older than rx-v2.8.4 returns 404 for this path; clients should treat that as "buzzer feature unavailable" rather than an error.
Preview a buzzer alert pattern without saving config. Bypasses master_enable and quiet hours — it's a preview, not a real alert.
{ "event": 4, "profile": 1 }event is the buzzer event ID (0-13, matching buzzer_event_t in firmware). profile is optional — when present, overrides the current master_profile for this one preview. 204 on success.
Blink the hub's status LED to help the user find it physically.
Trigger an OTA manifest check against the cloud OTA server (this is the only endpoint in this spec that causes the hub to reach the internet — and only because OTA is fundamentally a cloud-mediated operation).
Clear stored Wi-Fi credentials and re-enter setup AP mode. Destructive — clients should confirm before calling.
Push channel for real-time state changes. Hub sends JSON messages; client doesn't need to send anything except optional pings.
Message shapes:
// Device state changed (level, voltage, RSSI, etc.)
{ "kind": "device_state", "device_id": 1, "state": { "level_pct": 68, "voltage": 4.02, "rssi": -78, "ts": 1709123456 } }
// Fill event detected (RX-side fill detection)
{ "kind": "fill_event", "device_id": 1, "delta_pct": 22.4, "duration_s": 142, "started_at": 1709123210 }
// Hub-level state changed (WiFi, OTA, uptime tick)
{ "kind": "hub_state", "state": { "wifi_rssi": -54, "uptime_s": 86412, "ota_available": false } }
// Config edit happened (HA should refetch config for that scope)
{ "kind": "config_changed", "scope": "device", "id": 1 }
// Future: leak detection, low-threshold crossing, etc.
{ "kind": "alert", "severity": "warning", "code": "low_threshold", "device_id": 1, "data": {} }The hub will send { "kind": "hello", "schema_version": "1.0", "hub_id": "..." } immediately after the WS handshake completes so the client can validate compatibility before subscribing.
This section defines what state and config look like per device kind. Only tank is implemented in v1.0; other kinds are reserved for ecosystem expansion.
State fields: level_pct (0–100), voltage (TX battery, volts), rssi (LoRa signal, dBm), ts (Unix seconds).
Config fields: name, capacity_l, low_threshold_pct, sensor_kind (ultrasonic | pressure), tx_id.
State fields (planned): v_rms, i_rms, p_active, p_reactive, pf, freq, kwh_today.
- New fields may be added to existing endpoints/messages within v1.x. Clients must ignore unknown fields.
- New endpoints may be added in v1.x. Clients must not assume the endpoint set is fixed.
- Removing or renaming any field requires a major version bump (v2).
- The
schema_versionfield onGET /api/v1/infoand thehelloWS message is the source of truth.
Reference Python client: custom_components/smartghar/api.py (v0.1.0+).
Community-contributed clients in other languages welcome — open a PR linking your project under "Reference clients" here.