Skip to content

Latest commit

 

History

History
237 lines (160 loc) · 9.75 KB

File metadata and controls

237 lines (160 loc) · 9.75 KB

SmartGhar Hub Protocol — v1

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.

Versioning

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 v2 namespace 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.

Discovery (mDNS / Zeroconf)

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.

Authentication

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 include Authorization: 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.

REST endpoints

All endpoints return JSON (Content-Type: application/json). Errors return { "error": "<code>", "detail": "<human-readable>" } with appropriate HTTP status.

GET /api/v1/info

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"
  }
}

GET /api/v1/devices

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" }
    }
  ]
}

Field stability notes (added in firmware rx-v2.7.11)

  • mac: 12-char lowercase hex of the TX's eFuse MAC. This is the recommended anchor for unique_id in 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 to id for 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. The level_pct shown is the prior good value. Absent on older firmware — treat absence as false.
  • 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 from sensor_error — here the read succeeds but returns a plausible-but-stuck value. Absent on older firmware — treat absence as false.

GET /api/v1/devices/<id>

Single device with full state and config.

GET /api/v1/devices/<id>/history

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 }
  ]
}

PUT /api/v1/devices/<id>

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.

POST /api/v1/devices/<id>/identify

Blink the LEDs associated with this device for ~5 seconds. No body. 204 on success.

GET /api/v1/hub/led and PUT /api/v1/hub/led

Read/write LED config (brightness, mode, per-tank colors).

GET /api/v1/hub/display and PUT /api/v1/hub/display

Read/write OLED display config (brightness, rotation).

GET /api/v1/hub/buzzer and PUT /api/v1/hub/buzzer (added rx-v2.8.4)

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.

POST /api/v1/hub/buzzer/test (added rx-v2.8.4)

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.

POST /api/v1/hub/identify

Blink the hub's status LED to help the user find it physically.

POST /api/v1/hub/ota/check

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).

POST /api/v1/hub/wifi/forget

Clear stored Wi-Fi credentials and re-enter setup AP mode. Destructive — clients should confirm before calling.

WebSocket — WS /api/v1/stream

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.

Device kinds — payload schemas

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.

tank

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.

power (reserved, ships with PowerSync)

State fields (planned): v_rms, i_rms, p_active, p_reactive, pf, freq, kwh_today.

Other kinds (reserved): pump_relay, gas, soil, door, air

Compatibility rules

  • 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_version field on GET /api/v1/info and the hello WS message is the source of truth.

Reference clients

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.