Ask in the forum

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 Fair Use quota (200/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. An SOC ping also refreshes loxone.d0_soc_eod on the Decide PV pack (midnight projection from live SoC; PV and grid kWh of the open hour count only the minutes still ahead, see PV forecast). Telemetry calls do not count against your daily Fair Use / billable pool.

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) — or the last Hours session was active (charge_now==1) and live SoC ≥ session stop (session-complete, 120 s debounce) — the server runs an internal Hours Decide asynchronously — snapshot + PV pack + optional plant.plan_updated. That Decide does not count against the Fair Use / billable pool. 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. Immediate replan at session stop: Extract v1.23 O5 (PulseSessionStop) → Fetch I5+I6 (telemetry + Decide) — no mid-poll fetch from Extract alone. Sticky latch (charge_latched, charge_stop_hysteresis_pp = stop−H) lives in Decide publish (§6.7) — telemetry triggers the replan but does not write O3/O12 itself.

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 the mean of complete days since meter activation × 365 (ceiled to 100 kWh) into the plant profile — including on a SOC-only ping once history exists. Day / remaining-today estimates still use recency-weighted typical. Details: Hours Demand.

The strip auto-refreshes about every 10 minutes while idle (active Hours session ~90 s, only when charge_now is exactly 1/"1" and not latched — never Number(true)=1) when 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_needed without 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

FieldRequiredWhat it means
battery.soc_pctYes*Battery charge 0–100 %
meter.import_kwh_totalYes*Cumulative grid import kWh (any start value is fine)
meter.observed_atNoWhen measured (default: now)
grid_charge_allowedNoMaster 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

FieldWhat it means
telemetry_statusok, incomplete_input, invalid_input, or not_available
soc_pctStored value (clamped 0–100)
meter.is_baselinetrue on first ping / after portal reset / meter swap
meter.import_kwh_totalStored cumulative total
meter.observed_atWhen stored
meter.delta_kwhkWh since previous ping (null on baseline)
meter.reset_suspectedtrue if reading dropped vs previous (treated as new baseline)
quota_classtelemetry when accepted; access_denied on basic/anon not_available (not billed)

Live strip (portal / Sparvo)

GET /plant-outlook/live-telem and GET /api/live-telem include e.g.:

FieldMeaning
day_import_kwhSum of meter deltas today (internally 0 until the first positive delta)
awaiting_first_deltatrue until the first positive delta after baseline/reset — UI shows waiting for meter data, not the meter total
awaiting_baselinetrue when no meter total is stored yet (waiting for ping after reset)
meter_total_kwhLast cumulative reading (also while awaiting_first_delta)
meter_since_dateLocal 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_input with missing: ["plant_profile"] (or tier not_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 prefer Content-Type: application/json and 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. VO FetchPlantTelemetryPvMeter — Input = PV yield, body {"meter":{"pv_production_kwh_total":<v.2>}} (optional; no Decide on PV-Δ) 4. Optional VO FetchPlantGridChargeAllow — Input = switch 0/1, body {"grid_charge_allowed":<v.0>} (send only on change) 5. SOC+meter+PvMeter trigger from FetchPlantHours O4; response to /user/common/spotprice_telemetry.json 6. Headers: X-API-Key + Content-Type: application/json

The API accepts partial POSTs (SOC only, import meter only, PV meter only, grid_charge_allowed only, or combinations). Response sibling pv_meter + updated.pv_meter. On a grid_charge_allowed update, the response also echoes Hours EMS fields: charge_now, prefix_hours_needed_today, prefix_energy_charge_kwh, hours_needed* (all 0 when off) — server plan clears immediately; local Extract JSON still follows via Decide/webhook. Import and PV meters are ingested independently (an error on one sibling does not skip the other). Optional PV yield meter refines D0 in Hours Decide, Outlook/GET /v1/plant/pv-forecast / Loxone: completed hours = actual; current hour only once meter Δ > 0 kWh (else forecast); rest of today × scale 0.5…1.5 vs calendar-day forecast. Chart: green = PV (all plants); with meter, soft green = forecast. pv_kind=meter_blend for all keys as soon as any chart actual exists (including current hour only). Forecast-day cap (actual already > 1.2× model day) only skips scale — UI keeps blend. Kill HOURS_PV_METER_REFINE=0 → no scale; UI/GET still paints actual (meter_blend + skipped); Decide horizon stays unscaled on kill; no Decide on PV-Δ alone. FetchPlantHours v1.17 — O4 → Analog Memory → VO <v.2>: Telemetry only. Read grid charge: Extract O10 after Decide — Loxone Hours.

Next steps

Start with SpotpriceAPI

Register for Community or code samples with your personal key.

Live API status