TapHome
Community Beta · unverified Energy & metering

SpotLogic – battery optimisation over the spot market (HTTP API)

SpotLogic is a cloud service that plans when a photovoltaic battery charges, covers the house or sells to the grid by spot prices. The template reads the command for the current slot (0–7), the target state of charge, the next command and the plan age over HTTPS, and uploads battery telemetry every 2 minutes. The PV, consumption and price series must be supplied by the installer; the inverter is driven by own smart rules.

SpotLogicPacket Parser → HTTP6 devicesRevision 1.1 Last updated: November 10, 2026

Submitted by Adam Pelikán

Import into the TapHome appHardware › Packet Parser › Add from template

Download Template
On this page

Integration guide

SpotLogic is a cloud service that computes a charge and discharge plan for a photovoltaic battery over the spot electricity market. It is a calculation service, not a data source: the control unit sends battery telemetry together with the PV generation forecast, the house consumption forecast and the import and export prices, and SpotLogic answers with the command for the current time slot. This template connects TapHome to the SpotLogic API over HTTPS (spotlogic.cz, port 443) with a Packet Parser HTTP module. It reads the command and uploads the telemetry; it does not control the inverter — the command is connected to the inverter with the installer’s own smart rules.

There is nothing to wire. Setting up the service means activating SpotLogic for the site, collecting the battery and grid parameters, giving the control unit outgoing internet access and preparing the forecast and price series.

SpotLogic needs forecast and price series in every telemetry upload (PV generation and house consumption for today and tomorrow, import and export prices). This template cannot fill them on its own — see Forecast and price series. Until they are filled, SpotLogic rejects the telemetry with HTTP 422, does not compute a plan, and the command stays at No plan.

Configuration

1. Activate SpotLogic and keep the credentials

SpotLogic sends the PLC ID and the access token by e-mail when the device is activated. The token is secret, bound to exactly one PLC ID and valid for the subscription period. Both travel only in the query string of each request (?plc_id=…&token=…), as the SpotLogic contract requires — never in the request body.

Keep the token out of shared documents and public repositories. A wrong PLC ID and token pair, an invalid token or an expired subscription all end with HTTP 401, and SpotLogic deliberately does not say which of the three it was.

2. Collect the battery and grid parameters

SpotLogic plans only with the numbers it receives in the telemetry. Before the import, look them up in the inverter and battery documentation and in the grid connection contract:

ValueWhere it comes fromUnit
Battery capacitybattery data sheet; must be greater than 0kWh
Battery max powerone value for charging and dischargingkW
Max grid import powersize of the main breaker; 0 means the site never importskW
Max grid export powerexport limit of the grid connection; 0 means the site never exportskW
Hard battery floorlowest state of charge allowed by the inverter manufacturer, default 10%
Battery reservereserve the plan never sells to the grid from (for example for unpredictable consumption), default 10%

The current state of charge of the battery must also be available in TapHome, typically from the inverter’s own integration, so that a smart rule can write it into this template (see Uploading telemetry below).

3. Network and clock of the control unit

  • The control unit needs outgoing internet access over HTTPS to spotlogic.cz on port 443. SpotLogic does not connect back to the control unit — the forecast and price series travel in the body of the telemetry upload, so no inbound port has to be opened.
  • The telemetry carries the time of sending from the control unit clock, and SpotLogic builds the plan window from it. A few minutes of drift is harmless; hours of drift move the whole plan to the wrong slots. Keep the control unit clock correct.
  • The time zone (IANA name, for example Europe/Prague) decides where “today” ends and “tomorrow” begins. Enter the zone of the site.

4. Import parameters

The template is imported into the Packet Parser interface of the TapHome app, and the following parameters are filled in at import:

ParameterMeaningUnit
PLC ID, access tokenfrom the activation e-mail—
Time zoneIANA name, default Europe/Prague; day boundaries of the plan come from it—
Battery capacitymust be greater than 0kWh
Battery max powerone value for charging and dischargingkW
Max grid import / export power0 means the site never imports / never exportskW
Max state of chargerecorded by SpotLogic only, the current planner does not enforce it%
Hard battery floorfloor from the inverter manufacturer, default 10%

Numbers are written with a decimal point (a comma is converted). The PLC ID and the token are stored in the module variables PlcId and Token. All values stay editable afterwards as module variables.

5. Checking the credentials

The interactive API documentation of SpotLogic (on spotlogic.cz under /docs) has an Authorize dialog for the PLC ID and the token, and its TapHome telemetry example is complete: with valid credentials SpotLogic accepts it with HTTP 202. Update its time of sending (TimeStampUnix) to the current time first. Accepted example data are buffered for a plan like real telemetry, so this check belongs before the command is connected to the inverter.

An HTTP 422 answer about an incomplete forecast horizon also proves that the PLC ID and the token are correct — a wrong token gives HTTP 401.

Device capabilities

Reading the command (every 20 s)

The module polls GET /api/v1/dispatch every 20 seconds and the devices parse the answer:

  • SpotLogic command — command for the slot in progress (action_id):
ValueTapHome valueSpotLogic nameMeaning
0No planno_planNo usable plan — keep the inverter in its own safe mode
1Self-useself_useSelf-consumption: charge from PV surplus, discharge to cover the house
2Force chargeforce_chargeCharge the battery at maximum power
3Force discharge to gridforce_dischargeDischarge the battery to the grid at maximum power
4Self-use, no dischargingself_use_ChargeOnlySelf-consumption, charging allowed, discharging forbidden
5Self-use, no chargingself_use_DischargeOnlySelf-consumption, discharging allowed, charging forbidden
6Stop PV (reserved)stopPVReserved — not issued by the current planner
7Grid charge without PV (reserved)ChargeMaxNoPVReserved — not issued by the current planner

Service attributes show the command name and when the command ends (local time).

  • SpotLogic target state of charge — state of charge (%) the plan expects at the end of the slot. Stop forced charging or discharging when the battery reaches it; with self-use do not charge above it. SpotLogic adds that with self_use_DischargeOnly the battery should not be discharged below it. The value is a result of the plan, not a setpoint that has to be forced, and it may temporarily be lower than the battery reserve after the house consumed the reserve.
  • SpotLogic next command — the command of the following slot, with its start time as a service attribute.
  • SpotLogic plan age — age of the served plan in seconds. It can drive a fallback smart rule: when it grows well above the recompute interval, the plan is not being recomputed.

The answer HTTP 404 no_plan is not an error: the command is 0 and the device shows an information message. SpotLogic asks to switch to the inverter’s own safe mode in that state and never to repeat the last known command from memory. HTTP 401 (wrong PLC ID or token, expired subscription) sets an error on the devices and on the module; SpotLogic asks not to repeat such calls, so fix the credentials.

Uploading telemetry (about every 2 minutes)

The module sends POST /api/v1/optimize with PLCtype: "taphome" at most every 2 minutes. It sends the time of sending, the time zone, the battery and grid parameters and two values from devices:

  • Battery state of charge (input) — write the current state of charge 0–100 % from the inverter with a smart rule (when the inverter template gives 0–1, multiply by 100). It is rounded to a whole percent, because SpotLogic rejects fractional values. Until it has a value, nothing is sent and the module shows a warning.
  • Battery reserve for SpotLogic — the SOCmin reserve, default 10 %: the level the plan never sells to the grid from.

The module service attributes show the result of the last upload, the recompute interval reported by SpotLogic (default 900 s), when the plan was computed, the plan run and the API version. A rejected upload (HTTP 422) is a module error with the message SpotLogic returned. The upload answer never contains the plan — SpotLogic computes it in the background and the module picks it up with the next command reading.

Forecast and price series

SpotLogic expects six series in the body, each as pairs [unix_ts, value]:

Module variableContentContract
PvForecastTod, PvForecastTomPV generation today / tomorrow, kWh per hourhourly, without gaps, from the current hour to the end of tomorrow
LoadForecastTod, LoadForecastTomhouse consumption today / tomorrow, kWh per hourthe same
PriceImp, PriceExpimport / export price, CZK/kWhconstant cadence (hourly or 15 minutes), today is enough, negative prices allowed

The variables hold the series as JSON text, for example [[1788645600,0.0],[1788649200,0.42]], and are inserted into the body unchanged; the default [] is an empty series. The integration itself works; what it needs is the data. TapHome cannot yet share one JSON text between scripts, and a smart rule script cannot send HTTP requests. The six series — about 500 values together — therefore have to be collected from other scripts (for example a forecast or spot-price template) and handed over to this module through hundreds of helper devices, or built in the module read script itself. The integration becomes really practical once TapHome adds a text variable shared between scripts — see the idea Text variable and HTTP from smart rule scripts. Until then, fill the six variables and the template works as is. The script explains this in a comment at its top and at the place where the series are inserted.

Where the data come from is up to the installer; SpotLogic names these typical sources:

  • PV generation — a PV forecast service fed with the location, orientation and tilt of the panels and the installed power, or the inverter manufacturer’s cloud.
  • House consumption — the site’s own history: hourly consumption averaged by hour of day and type of day (working day, weekend, holiday) is enough to start with.
  • Prices — the spot price recalculated by the supply contract (import, for example spot plus a margin) and by the contract for selling surplus energy (export).

A day with a daylight-saving change has 23 or 25 hours, so a complete PV or consumption series has 47, 48 or 49 values. Samples older than one hour before the current hour are cropped, as are samples beyond the end of tomorrow; a series that ends before the end of tomorrow is rejected.

Troubleshooting

SymptomCause and solution
Command stays at No planNo plan has been computed yet, or the telemetry is rejected. Check the module error and the service attribute Last telemetry upload. A plan older than 60 minutes is not served either. After the first accepted upload the plan can take a while — at least one recompute interval.
Module error HTTP 422The telemetry violates the SpotLogic data contract; the message says which part. Most often the forecast or price series are missing or do not cover the period up to the end of tomorrow.
Error HTTP 401PLC ID and token do not match or the subscription expired. Correct the module variables PlcId and Token.
Warning “Telemetry not sent”The state of charge has not been written yet, or a battery or grid parameter is empty.
Plan age keeps growingNew telemetry is not reaching SpotLogic or its data sources fail; the last valid plan stays in force until it is 60 minutes old.
Error HTTP 500 or 502Server error or failing data source on the SpotLogic side; the last valid plan stays in force. SpotLogic recommends repeating both with growing delays; the module simply repeats both requests on the next poll.
Plan steers the wrong hoursThe control unit clock or the time zone is wrong — the plan window is built from the time of sending and the declared zone.

Available devices 6

These devices appear in TapHome after import.

SpotLogic Module
Service Attributes
Last telemetry upload
Plan recompute interval
Plan computed at
Plan run
API version
Custom Variables
PlcId (string)
Token (string)
TimeZone (string)
BatCapacity (string)
BatPmax (string)
GridImportPmax (string)
GridExportPmax (string)
SocMax (string)
SocHardMin (string)
SocNow (numeric) = -1
SocMin (numeric) = 10
PvForecastTod (string) = []
PvForecastTom (string) = []
LoadForecastTod (string) = []
LoadForecastTom (string) = []
PriceImp (string) = []
PriceExp (string) = []
DispatchJson (string)
DispatchCode (numeric)
OptimizeJson (string)
OptimizeCode (numeric)
OptimizeAt (string)
LastOptimizeUnix (numeric)

SpotLogic

Read (module)
# =============================================================================
#  STATUS: WORKING, BUT THE DATA MUST BE BROUGHT IN BY HAND
# =============================================================================
#  The integration itself works - dispatch reading, action_id -> inverter mode
#  mapping, next command, plan diagnostics and error reporting are finished.
#  What it needs is the data: six series (PV forecast, load forecast, import and
#  export spot price; ~500 values, hourly or 15-minute, horizon now -> end of
#  tomorrow) as JSON text [[unix_ts, value], ...] in the module variables
#  PvForecastTod ... PriceExp (see below).
#  Until TapHome ships a shared variable between scripts, those series must be
#  collected from other scripts and handed over through hundreds of helper
#  devices - that is why the integration is only really practical once that
#  feature lands.
#  Tracking: https://taphome.com/cs/ideas/script-data-http/  (TapHome idea FR-003)
#  In the meantime: fill the six variables below and the template works as is.
# =============================================================================

# 1) GET /api/v1/dispatch - the command for the slot in progress (module poll ~20 s).
#    plc_id and token travel ONLY in the query string (SpotLogic contract).
VAR r := SENDHTTPREQUEST("/api/v1/dispatch?plc_id=" + PlcId + "&token=" + Token);
DispatchCode := r.StatusCode;
DispatchJson := r.Content;
IF r.StatusCode = 401
    ADDERROR(1, "SpotLogic rejected the PLC ID or the token (HTTP 401) - check the module variables PlcId and Token; the subscription may have expired. Not retried by SpotLogic rules, fix the credentials.");
ELSEIF r.StatusCode != 200 AND r.StatusCode != 404
    VAR dmsg := PARSEJSON(r.Content, "$.message", true);
    ADDERROR(2, "SpotLogic dispatch failed: HTTP " + r.StatusCode + IF(ISNULL(dmsg), "", " " + dmsg));
END

# 2) POST /api/v1/optimize - telemetry, at most every ~2 min (SpotLogic asks for no more often).
datetime now := NOW();
VAR nowUnix := now.UNIXTIME;
IF nowUnix - LastOptimizeUnix >= 115
    LastOptimizeUnix := nowUnix;
    VAR cap := TODOUBLE(REPLACE(BatCapacity, ",", "."));
    VAR bp := TODOUBLE(REPLACE(BatPmax, ",", "."));
    VAR gi := TODOUBLE(REPLACE(GridImportPmax, ",", "."));
    VAR ge := TODOUBLE(REPLACE(GridExportPmax, ",", "."));
    VAR smax := TODOUBLE(SocMax);
    VAR shard := TODOUBLE(SocHardMin);
    IF SocNow < 0 OR SocNow > 100
        OptimizeCode := -1;
    ELSEIF ISNAN(cap) OR ISNAN(bp) OR ISNAN(gi) OR ISNAN(ge) OR ISNAN(smax) OR ISNAN(shard) OR LENGTH(PlcId) = 0 OR LENGTH(Token) = 0
        OptimizeCode := -2;
    ELSE
        # Forecast and price series: module variables PvForecastTod, PvForecastTom,
        # LoadForecastTod, LoadForecastTom, PriceImp, PriceExp hold JSON text
        # [[unix_ts, value], ...]. The template does NOT fill them - see the banner
        # at the top of this script. Until they are filled, SpotLogic answers 422.
        VAR body := "{\"PLCtype\":\"taphome\"";
        body := body + ",\"TimeZone\":\"" + TimeZone + "\"";
        body := body + ",\"TimeStampUnix\":" + TOSTRING(nowUnix, "F0");
        body := body + ",\"BatCapacity\":" + TOSTRING(cap, "F2");
        body := body + ",\"BatPmax\":" + TOSTRING(bp, "F2");
        body := body + ",\"GridImportPmax\":" + TOSTRING(gi, "F2");
        body := body + ",\"GridExportPmax\":" + TOSTRING(ge, "F2");
        body := body + ",\"SOCnow\":" + TOSTRING(SocNow, "F0");
        body := body + ",\"SOCmin\":" + TOSTRING(SocMin, "F0");
        body := body + ",\"SOCmax\":" + TOSTRING(smax, "F0");
        body := body + ",\"SOChardmin\":" + TOSTRING(shard, "F0");
        body := body + ",\"PvForecastTod\":" + PvForecastTod;
        body := body + ",\"PvForecastTom\":" + PvForecastTom;
        body := body + ",\"LoadForecastTod\":" + LoadForecastTod;
        body := body + ",\"LoadForecastTom\":" + LoadForecastTom;
        body := body + ",\"PriceImp\":" + PriceImp;
        body := body + ",\"PriceExp\":" + PriceExp + "}";
        VAR o := SENDHTTPREQUEST("/api/v1/optimize?plc_id=" + PlcId + "&token=" + Token, "POST", body, "Content-Type: application/json");
        OptimizeCode := o.StatusCode;
        OptimizeJson := o.Content;
        OptimizeAt := TOSTRING(now, "yyyy-MM-dd HH:mm:ss");
    END
END

# The outcome of the last telemetry upload is reported on every run, not only on the upload run.
VAR omsg := "";
IF LENGTH(OptimizeJson) > 0
    omsg := PARSEJSON(OptimizeJson, "$.message", true);
    IF ISNULL(omsg)
        omsg := "";
    END
END
IF OptimizeCode = -1
    ADDWARNING(3, "Telemetry not sent: the device Battery state of charge (input) has no value 0-100 yet - write it from the inverter with a smart rule.");
ELSEIF OptimizeCode = -2
    ADDWARNING(7, "Telemetry not sent: fill the module variables PlcId, Token, BatCapacity, BatPmax, GridImportPmax, GridExportPmax, SocMax and SocHardMin (numbers with a decimal point).");
ELSEIF OptimizeCode = 401
    ADDERROR(4, "SpotLogic rejected the telemetry: PLC ID or token invalid (HTTP 401).");
ELSEIF OptimizeCode = 422
    ADDERROR(5, "SpotLogic rejected the telemetry (HTTP 422): " + omsg + " - the forecast and price series (module variables PvForecastTod ... PriceExp) must be filled, see the template description.");
ELSEIF OptimizeCode != 0 AND OptimizeCode != 202
    ADDERROR(6, "SpotLogic telemetry upload failed: HTTP " + OptimizeCode + " " + omsg);
END
Service Attributes
Last telemetry upload
IF(OptimizeCode = 0, "not sent yet", IF(OptimizeCode < 0, "waiting for inputs", "HTTP " + OptimizeCode + " at " + OptimizeAt))
Plan recompute interval
VAR v := PARSEJSON(OptimizeJson, "$.data.recompute_interval_sec", true);
IF ISNULL(v)
    RETURN("-");
END
RETURN(TOSTRING(v, "F0") + " s");
Plan computed at
VAR s := PARSEJSON(DispatchJson, "$.meta.plan_created_at", true);
IF ISNULL(s)
    RETURN("-");
END
datetime t := DATETIME(s);
VAR l := t.LOCALDATETIME;
RETURN(TOSTRING(l, "yyyy-MM-dd HH:mm"));
Plan run
VAR v := PARSEJSON(DispatchJson, "$.meta.run_id", true);
RETURN(IF(ISNULL(v), "-", v));
API version
VAR v := PARSEJSON(DispatchJson, "$.meta.version.api", true);
RETURN(IF(ISNULL(v), "-", v));
SpotLogic command Multi-value Switch
Values / States: No plan · Self-use · Force charge · Force discharge to grid · Self-use, no discharging · Self-use, no charging · Stop PV (reserved) · Grid charge without PV (reserved)

SpotLogic command

Read switch state
IF DispatchCode = 0
    RETURN(NaN);
END
IF DispatchCode = 401
    ADDERROR(1, "SpotLogic rejected the PLC ID or the token (HTTP 401) - check the module variables PlcId and Token.");
    RETURN(NaN);
END
IF DispatchCode = 404
    ADDINFO(2, "No usable plan yet (no_plan) - keep the inverter in its own safe mode.");
    RETURN(0);
END
IF DispatchCode != 200
    ADDERROR(3, "Command not available: HTTP " + DispatchCode);
    RETURN(NaN);
END
VAR id := PARSEJSON(DispatchJson, "$.data.command.action_id", true);
IF ISNULL(id)
    ADDERROR(4, "Unexpected SpotLogic answer: data.command.action_id is missing.");
    RETURN(NaN);
END
RETURN(id);
Service Attributes
Command name
VAR v := PARSEJSON(DispatchJson, "$.data.command.action_name", true);
IF ISNULL(v)
    v := PARSEJSON(DispatchJson, "$.data.action_name", true);
END
RETURN(IF(ISNULL(v), "-", v));
Command valid until
VAR s := PARSEJSON(DispatchJson, "$.data.command.valid_until", true);
IF ISNULL(s)
    RETURN("-");
END
datetime t := DATETIME(s);
VAR l := t.LOCALDATETIME;
RETURN(TOSTRING(l, "yyyy-MM-dd HH:mm"));
SpotLogic target state of charge Variable

SpotLogic target state of charge

Read
IF DispatchCode = 0
    RETURN(NaN);
END
IF DispatchCode = 401
    ADDERROR(1, "SpotLogic rejected the PLC ID or the token (HTTP 401) - check the module variables PlcId and Token.");
    RETURN(NaN);
END
IF DispatchCode != 200
    RETURN(NaN);
END
VAR v := PARSEJSON(DispatchJson, "$.data.command.target_soc_percent", true);
IF ISNULL(v)
    RETURN(NaN);
END
RETURN(v);
SpotLogic next command Multi-value Switch
Values / States: No plan · Self-use · Force charge · Force discharge to grid · Self-use, no discharging · Self-use, no charging · Stop PV (reserved) · Grid charge without PV (reserved)

SpotLogic next command

Read switch state
IF DispatchCode = 0
    RETURN(NaN);
END
IF DispatchCode = 401
    ADDERROR(1, "SpotLogic rejected the PLC ID or the token (HTTP 401) - check the module variables PlcId and Token.");
    RETURN(NaN);
END
IF DispatchCode = 404
    RETURN(0);
END
IF DispatchCode != 200
    ADDERROR(3, "Next command not available: HTTP " + DispatchCode);
    RETURN(NaN);
END
VAR id := PARSEJSON(DispatchJson, "$.data.context.next_action_id", true);
IF ISNULL(id)
    RETURN(NaN);
END
RETURN(id);
Service Attributes
Next command name
VAR v := PARSEJSON(DispatchJson, "$.data.context.next_action_name", true);
RETURN(IF(ISNULL(v), "-", v));
Next command starts
VAR s := PARSEJSON(DispatchJson, "$.data.context.next_action_time", true);
IF ISNULL(s)
    RETURN("-");
END
datetime t := DATETIME(s);
VAR l := t.LOCALDATETIME;
RETURN(TOSTRING(l, "yyyy-MM-dd HH:mm"));
SpotLogic plan age Variable

SpotLogic plan age

Read
IF DispatchCode = 0
    RETURN(NaN);
END
IF DispatchCode = 401
    ADDERROR(1, "SpotLogic rejected the PLC ID or the token (HTTP 401) - check the module variables PlcId and Token.");
    RETURN(NaN);
END
IF DispatchCode != 200
    RETURN(NaN);
END
VAR v := PARSEJSON(DispatchJson, "$.meta.plan_age_sec", true);
IF ISNULL(v)
    RETURN(NaN);
END
RETURN(v);
Battery state of charge (input) Variable

Battery state of charge (input)

Read
IF(SocNow < 0, NaN, SocNow)
Write
SocNow := Va;
Battery reserve for SpotLogic Variable

Battery reserve for SpotLogic

Read
RETURN(SocMin);
Write
SocMin := Va;
Connection: Packet Parser → HTTP

Import parameters

Enter these values when you import the XML.

Asked in the appDefault
SpotLogic PLC ID (from the activation e-mail)Enter at import
SpotLogic access token (from the activation e-mail, secret)Enter at import
Time zone (IANA name)Europe/Prague
Battery capacity [kWh]Enter at import
Battery max charge and discharge power [kW]Enter at import
Max grid import power [kW] (0 = never import)Enter at import
Max grid export power [kW] (0 = never export)Enter at import
Max state of charge [%] (recorded only)100
Hard battery floor from the inverter manufacturer [%]10

Found a problem with this device template?

Tell us what's wrong, what's missing, or how the template should behave. We rely on your feedback to keep the catalog accurate.

Template · TapHome portal SpotLogic – battery optimisation over the spot market (HTTP API) Open this template in the Customer Portal to apply it to one of your homes, or to draft a refinement and submit it back to the catalog. Packet Parser → HTTP View in TapHome portal