Plant Telemetry API
POST /v1/plant/telemetry — send SOC % and meter (kWh) regularly. Loxone: Telemetry only or bundled Hours & telemetry. Telemetry softcap Community 2000/day — does not eat Decision quota (120/day).
Plant Telemetry lets your system report live battery level and grid meter readings to SpotpriceAPI. The server learns your real consumption pattern over time. Hours Demand uses that for better charging plans. Each SOC ping is also stored as history (~15‑min rhythm) so Plant outlook can show the battery trajectory. Telemetry calls do not count against your daily Decision fair-use limit.
Exception: if live SOC is only 10 percentage points above min SoC (then again at 5 pp, and at the floor) — or recovers the other way (floor → 5 pp → 10 pp → headroom) — the server runs an internal Hours Decide asynchronously — snapshot + optional plant.plan_updated. That Decide does not count against Decision quota. Loxone Extract picks up the new discharge cap (O12) on the next Hours Decide (:00 / SOC band / I6), unless a webhook triggers a plan pull.
In the portal Plant outlook and the Sparvo app (home + plant) you see a live strip:
1. Battery SOC % 2. Day import so far (kWh) — waiting for meter data until the first positive delta; never the raw meter total 3. Estimated full-day import (kWh) 4. Current meter total since baseline date (kWh) — label “since {date}” (first transmission or last reset)
The strip shows values only — no method subtitle under estimated day use. Internally the estimate comes from meter history (recency-weighted, spike-damped; today plus a same-day overlay on the WD/WE hour shape, not clock × 24) or annual÷365 until ≥1 full day. Same number as Hours remaining-today and outlook D0. Hours explain “until this morning” additionally uses partial hour + recent rate (v1.9) for short-term cover — not identical to the calendar-day total above. After ≥1 full meter day we write typical × 365 (ceiled to 100 kWh) into the plant profile — including on a SOC-only ping once history exists. Details: Hours Demand.
The strip auto-refreshes about every 15 minutes while the tab is visible (no full page reload), with an “Updated …” timestamp. Endpoints: Portal GET /plant-outlook/live-telem, Sparvo GET /api/live-telem.
On /plant-outlook and Sparvo /plant below (order: live strip → Hours explain → PV day chart):
- Starting battery level (manual) — slider fallback only until live telemetry is connected (locked when live SOC is active).
- Reset meter baseline — next EMS ping becomes the new start value.
Battery level, day import, and meter total appear only in the live strip, not again in separate cards. Without live telemetry the portal still shows values in the telemetry card (fallback).
Portal slider: As soon as POST /v1/plant/telemetry sends SOC (soc_source=hours_api), it overrides the slider and locks it (~36 h freshness). Without fresh API SOC the slider is enabled again.
Everyday example
Every 15 minutes your inverter reports: “Battery 42%, grid meter 3,725.4 kWh total.” That first total is a baseline (not “today’s use”). Later pings send the rising total; SpotpriceAPI stores only the deltas.
When to use / when not
Use when:
- You have SOC% and a cumulative import meter
- You want smarter
hours_neededwithout guessing load
Do not use when:
- You need the actual charge schedule → call Hours Demand after telemetry (the server also auto-Decides when SOC is within 10 pp / 5 pp of min SoC, or at the floor)
- You only want prices → Forecast
What you need
- Community tier or higher
- Plant profile in the portal (linked to your API key)
- Header:
X-API-Key: sf_live_YOUR_KEY - JSON body with SOC and meter total
Step by step
1. Create plant profile once in the portal (location for PV). 2. POST whenever SOC changes meaningfully (e.g. every 10 %) or on a fixed interval. 3. Send battery.soc_pct and meter.import_kwh_total (cumulative — e.g. 3725, not “12 kWh today”). 4. First ping sets a baseline — meter.is_baseline=true, delta_kwh empty. That is normal. Portal/Sparvo Day use so far shows waiting for meter data until the first positive delta (the cumulative total is not today’s kWh). 5. A first 0 then the real total is also a baseline (placeholder), not consumption. 6. From the second ping with an increase, meter.delta_kwh shows kWh used since last ping. 7. Optional: portal Reset meter baseline clears the stored total; the next ping is a new start value. 8. Call Hours Demand when you want an updated charging plan.
Minimal request and response
curl -X POST https://api.spotpriceapi.com/v1/plant/telemetry \
-H "Content-Type: application/json" \
-H "X-API-Key: sf_live_YOUR_KEY" \
-d '{
"battery": { "soc_pct": 40 },
"meter": { "import_kwh_total": 3725.4 }
}'
First ping (baseline):
{
"telemetry_status": "ok",
"soc_pct": 40.0,
"meter": {
"is_baseline": true,
"import_kwh_total": 3725.4,
"observed_at": "2026-08-05T08:00:00+00:00",
"delta_kwh": null
},
"quota_class": "telemetry"
}
Later ping:
{
"telemetry_status": "ok",
"soc_pct": 41.0,
"meter": {
"is_baseline": false,
"import_kwh_total": 3728.9,
"observed_at": "2026-08-05T10:00:00+00:00",
"delta_kwh": 3.5
},
"quota_class": "telemetry"
}
Endpoint
POST https://api.spotpriceapi.com/v1/plant/telemetry
Request body fields
| Field | Required | What it means |
|---|---|---|
battery.soc_pct | Yes* | Battery charge 0–100 % |
meter.import_kwh_total | Yes* | Cumulative grid import kWh (any start value is fine) |
meter.observed_at | No | When measured (default: now) |
grid_charge_allowed | No | Master switch 0/1 (or true/false). Omit = leave plant profile unchanged. Partial POST OK (same as SOC/meter-only). |
\*Partial POSTs may send SOC only, meter only, grid_charge_allowed only, or combinations.
Important response fields
| Field | What it means |
|---|---|
telemetry_status | ok, incomplete_input, invalid_input, or not_available |
soc_pct | Stored value (clamped 0–100) |
meter.is_baseline | true on first ping / after portal reset / meter swap |
meter.import_kwh_total | Stored cumulative total |
meter.observed_at | When stored |
meter.delta_kwh | kWh since previous ping (null on baseline) |
meter.reset_suspected | true if reading dropped vs previous (treated as new baseline) |
quota_class | Always telemetry — separate from decision quota |
Live strip (portal / Sparvo)
GET /plant-outlook/live-telem and GET /api/live-telem include e.g.:
| Field | Meaning |
|---|---|
day_import_kwh | Sum of meter deltas today (internally 0 until the first positive delta) |
awaiting_first_delta | true until the first positive delta after baseline/reset — UI shows waiting for meter data, not the meter total |
awaiting_baseline | true when no meter total is stored yet (waiting for ping after reset) |
meter_total_kwh | Last cumulative reading (also while awaiting_first_delta) |
meter_since_date | Local ISO date of the current baseline series (YYYY-MM-DD); after portal reset falls back to cleared_at until the next ping — UI “meter total since …” |
Common mistakes
- No plant profile in portal →
incomplete_inputwithmissing: ["plant_profile"](or tiernot_available) - Sending daily use instead of cumulative total
- Expecting a charging plan from telemetry alone — you still need Hours Demand
- Polling telemetry every second — use sensible intervals; see Rate limits
- Loxone: official VO placeholders are
<v>/<v.2>on the command Input — not named<v.Vi…>(those often stay literal →hint=loxone_vi_not_substituted). See Loxone / Miniserver below. - German decimal commas (
85,0) are sanitized server-side; still preferContent-Type: application/jsonand save to/user/common/spotprice_telemetry.json
Loxone / Miniserver
Per Loxone docs, a Virtual Output command only substitutes <v> / <v.1> / <v.2> / <v.3> / <v.t> — that is the value on the command Input, not <v.ViName>.
Recommended:
1. VO FetchPlantTelemetrySoc — Input = SOC, body {"battery":{"soc_pct":<v.2>}} 2. VO FetchPlantTelemetryMeter — Input = meter, body {"meter":{"import_kwh_total":<v.2>}} 3. Optional VO FetchPlantGridChargeAllow — Input = switch 0/1, body {"grid_charge_allowed":<v.0>} (send only on change) 4. SOC+meter trigger from FetchPlantHours O4; response to /user/common/spotprice_telemetry.json 5. Headers: X-API-Key + Content-Type: application/json
The API accepts partial POSTs (SOC only, meter only, grid_charge_allowed only, or combinations). FetchPlantHours v1.9 — O4 → Analog Memory → VO <v.2>: Telemetry only. Read grid charge: Extract O10 after Decide — Loxone Hours.
Next steps
- Hours Demand — get charging hours after telemetry
- Rate limits — telemetry softcap
- Webhooks — notified when plan changes
- Loxone hours guide — automate telemetry from Miniserver
Register for Community or code samples with your personal key.