SPAN Smart Panel
The span_panel driver polls a SPAN Smart Panel over its on-LAN REST API. It auto-discovers circuits, mirrors them as zones under the power subsystem, and exposes panel-level telemetry — main relay state, grid/feedthrough power, run config, DSM grid state, islanding state, and battery state-of-energy — as device attributes.
All traffic stays on the LAN. There is no SPAN cloud round-trip at runtime.
Status
Built against the SPAN documented REST API. Onboarding uses /api/v2/auth/register to exchange a passphrase for a JWT bearer; telemetry and control use /api/v1/*. The /api/v1 surface is documented to deprecate 2026-12-31 — when SPAN extends /api/v2 to cover telemetry, or the integration moves to MQTT/eBus, the transport swaps internally with no change to how you configure the device.
The panel's built-in MQTT/Homie 5.0 broker is intentionally not used.
Prerequisites
- SPAN panel reachable from the GEM controller on TCP/443 (HTTPS, self-signed cert accepted).
- A panel passphrase. There are two ways to mint one:
- SPAN Home app — issues a passphrase tied to your account.
- Door switch — press the panel door switch for proof-of-proximity, then complete the registration flow within the timeout window.
Setup
- System → Devices: add a device with driver
span_panel. Fill in:ip— panel LAN IPpassphrase— proof-of-proximity passphrase (secure)
- Save and enable. The driver:
- calls
/api/v2/auth/register(the only v2 call it makes) and caches the returned JWT as theaccess_tokendevice attribute, - reads
/api/v1/statusto populateserial_number,firmware_version,door_state, - calls
/api/v1/circuitsto enumerate circuits — each one is auto-created as a zone under thepowersubsystem (created on demand) withzone.address= the circuit UUID andcontrol=power_default. Circuit names are typed in the SPAN app and need not be unique, so a zone name already taken gets the circuit UUID appended (see auto-created zones), - starts polling
/api/v1/panel,/api/v1/circuits,/api/v1/storage/soe,/api/v1/islanding-stateevery 5 s, and re-reads/api/v1/status(serial / firmware / door) about once a minute (every 12 poll cycles).
- calls
Attributes
Device
| Attribute | Required | Type | Notes |
|---|---|---|---|
ip | yes | string | Panel LAN IP. |
passphrase | yes | string (secure) | Proof-of-proximity passphrase from the SPAN Home app or door-switch flow. |
port | no | int | Override the panel port (default 443 for https, 80 for http). Useful when proxying. A value that is not a port number (1-65535) is ignored with a warning and the protocol default is used. |
protocol | no | string | https (default) or http. Use http only behind a plaintext proxy. |
poll_interval | no | int | Telemetry poll interval in seconds. Default 5, range 1..300. A blank, zero, negative or unparseable value uses 5. |
access_token | no | string (secure, readonly) | JWT cached after registration. The driver auto-refreshes by re-registering on 401/403. |
Runtime-populated (read-only):
| Attribute | Type | Source |
|---|---|---|
serial_number | string | registration response, refreshed from /api/v1/status (system.serial) |
firmware_version | string | /api/v1/status (software.firmwareVersion) |
main_relay_state | string | /api/v1/panel.mainRelayState |
grid_power_w | float | /api/v1/panel.instantGridPowerW |
feedthrough_power_w | float | /api/v1/panel.feedthroughPowerW |
run_config | string | /api/v1/panel.currentRunConfig |
dsm_grid_state | string | /api/v1/panel.dsmGridState |
islanding_state | string | /api/v1/islanding-state.islanding_state |
battery_soe_percent | float | /api/v1/storage/soe (soe.percentage) |
door_state | string | /api/v1/status (system.doorState) |
discovered_circuits | json | Last circuits snapshot for diagnostics. |
last_error | string | Most recent failure reason (registration error, panel unreachable, etc.) — written whenever a poll or connect fails. |
Telemetry from /api/v1/panel, /api/v1/circuits, /api/v1/storage/soe, and /api/v1/islanding-state refreshes every poll (5 s by default). Identity fields read from /api/v1/status — serial_number, firmware_version, door_state — refresh only at connect and then about once a minute, so the door reading is not real-time.
Zone
Zone address is the circuit UUID from /api/v1/circuits. Per-circuit attributes populated by the poller:
| Attribute | Type |
|---|---|
state | string — on (relay closed) or off (relay open) |
relay_state | string — raw CLOSED / OPEN |
instant_power_w | float |
consumed_energy_wh | float |
produced_energy_wh | float |
priority | string — MUST_HAVE / NICE_TO_HAVE / NON_ESSENTIAL |
is_user_controllable | bool |
is_sheddable | bool |
is_never_backup | bool |
tabs | string (CSV) |
Per-circuit power feeds the Power Management dashboard: instant_power_w carries the power.circuit metric role (Top Circuits widget) and the two energy counters carry energy.consumed / energy.produced (Energy Today widget). instant_power_w is signed — positive is consumption, negative is backfeed/export. The device-level battery_soe_percent carries the battery.soc role, so a SPAN with attached storage shows up on the dashboard's battery readouts.
Configurable
| Attribute | Type | Default | Notes |
|---|---|---|---|
ct_clamp_reversed | bool | false | Correct a single physically-reversed CT clamp on a pure-load circuit — e.g. a fridge that reports a steady negative instant_power_w. When enabled, GEM negates instant_power_w and swaps the consumed_energy_wh / produced_energy_wh counters for that circuit. |
ct_clamp_reversed is a retrofit escape hatch — the proper fix is recommissioning the CT in SPAN. Do not enable it to paper over a tab-mapping swap (two circuits crossed at the panel), which would only hide the partner circuit's error instead of fixing it.
Commands
| Command | Args | Purpose |
|---|---|---|
on | address | Set the circuit's relay to CLOSED (zone on). |
off | address | Set the circuit's relay to OPEN (zone off). |
reboot | address, delay | Open the relay, wait delay seconds (default 5), close it again. Useful for power-cycling a non-essential load. A delay longer than about 24 days is refused before the relay moves. If the device is reloaded or disabled during the delay, the relay is still closed at the end of it. |
All three relay commands are refused with circuit … is not user-controllable when SPAN marks the circuit non-controllable (e.g. the mains feed or never-backup loads). The driver checks the last cached circuit snapshot before sending, so the refusal is immediate.
Which circuits a user may switch
Relay commands from a UI, the REST API, or voice are confined to circuits that have a zone on this
device — enforced centrally at the command dispatch layer, so every command that carries a circuit
address is covered. A circuit uuid with no matching zone — including one whose zone exists but is
disabled — returns not authorized: address is not mapped to a zone on this device.
A circuit left out of GEM is usually left out on purpose: the alarm panel, the network rack, the
NVR, the controller itself, or a circuit an electrician locked out. SPAN's own isUserControllable
flag describes what the hardware permits, not who GEM allows, so it is not a substitute.
Administrators are unaffected, and so are macros, triggers, schedules and other drivers — existing automation keeps working, including automation that switches a circuit with no zone.
Quirks
- SPAN's API serializes float32 telemetry as full float64, leaking artifacts like
1557.4998779296875for what was1557.5. The driver rounds to 1 decimal place — only ~7 significant digits are real. - The panel rejects relay writes for circuits where
isUserControllable=false. The driver checks the cached snapshot before sending and returns an error early. - After auth-related 401/403 errors the driver re-registers automatically using the stored passphrase. If the passphrase was rotated externally, registration will fail until the device row is updated.
- Each auth recovery is attempted once per request. If a panel hands back a token it then refuses, the driver stops after one re-registration and records the rejection in
last_errorrather than retrying indefinitely. - When a re-registration is refused, the background poll waits before trying again (30 seconds, doubling up to 5 minutes); a relay command still re-registers straight away. Requests that are refused at the same moment share a single re-registration.
Troubleshooting
| Symptom | Check |
|---|---|
registration returned no access token on first connect | Wrong passphrase, or the proof-of-proximity window expired. Mint a fresh passphrase from the SPAN Home app and retry. |
Repeated re-register failed warnings | Passphrase has been invalidated (e.g. rotated). Update the device's passphrase attribute with a freshly minted value. |
Circuit relay command returns circuit ... is not user-controllable | The circuit is locked at the panel level (e.g. main breakers, never-backup loads). Cannot be controlled remotely. |
Circuit relay command returns not authorized: address is not mapped to a zone on this device | The circuit has no enabled zone on this device. Create one, or run the command as an administrator. See Which circuits a user may switch. |
| No new zones appear after wiring change | Reload the device (System → Devices → the Reload action on the device row) to re-call /api/v1/circuits. The poller does not auto-create new zones — it only refreshes telemetry on circuits discovered at connect. The one exception: if the panel could not be reached when the device connected, zones are created from the first poll the panel answers. |
| Device shows offline or telemetry stops updating | Check the device's last_error attribute for the most recent failure reason. A reachable panel that fails every endpoint marks the device disconnected; any one endpoint succeeding keeps it connected. A reply that is not JSON (for example a proxy's HTML page) counts as a failure. After three failed polls in a row the driver polls less often — every 30 seconds, doubling up to every 5 minutes — and returns to the normal interval as soon as the panel answers. Relay commands are never held back. |