id back. The same shape works for immediate dispatch, scheduled dispatch, and slots inside a schedule, and it works whether the device on the other end is a battery, an EV charger, or a thermostat.
Agents do not click buttons. They read a response, choose a verb, build the parameters, and post. One read and one write are enough to drive any supported device, and the read teaches the write. A developer who learns the loop on a battery already knows it for an EV charger.
The action push surface covers batteries, EV chargers, and thermostats. Solar inverters and vehicles are read-only: a POST /solar-inverter/{deviceId} or POST /vehicle/{deviceId} returns 422 DIRECT_ACTION_UNSUPPORTED. Use those families for monitoring; control the surrounding system via the writable device types.
The JSON snippets on this page show only the action grammar and the relevant slices of the device read. Every real API response is wrapped in { success, data, meta }: the action body lands under data, and the device read’s state, commands, and settings all sit inside data. See error envelope for the canonical wrapper and the failure variant.
The action shape
command selects the variant. parameters carries the mode’s inputs. start and end declare temporal intent. onConflict resolves collisions with active actions on the same device. Every field you send is either honoured, validated, or rejected. No keys are silently discarded.
Command vocabulary is intent, not feature flags
A command names an intent the device should pursue, not a button on a vendor app. The verb is the same canonical token whichever manufacturer answers, and the request shape is the same whichever device type answers. Each device type defines its own set of commands, drawn from one shared design language. The canonical battery surface defines six commands:| Command | Meaning |
|---|---|
charge | Direct imperative. Charge from grid or solar, optionally bounded by target and power. |
discharge | Direct imperative. Discharge to load or grid, optionally bounded by target and power. |
idle | Pause charge and discharge. The battery sits. |
auto.balanced | Optimise for self-consumption. Charge when solar is plentiful, discharge when the home needs it. |
auto.reserve | Reserve capacity for grid outage. Stay charged above the reserve floor. |
auto.export | Maximise grid export. Discharge when the export tariff is attractive. |
| Command | Meaning |
|---|---|
charge | Begin or resume charging the connected vehicle. |
idle | Pause charging, holding the session. |
| Command | Meaning |
|---|---|
heat | Actively heat to a target temperature. |
cool | Actively cool to a target temperature. |
auto | Maintain a comfort band: heat below heatSetpoint, cool above coolSetpoint. |
idle | Pause all heating and cooling. |
follow_schedule | Resume the device’s native program, relinquishing any active override. |
idle is the canonical stop verb everywhere: it pauses a battery, pauses a charging session, and pauses heating and cooling, so an agent that learns “send idle to stop” applies it across every device type. target is the canonical “aim for this level” parameter: a battery charges to a target percent, a thermostat heats to a target degree. The same name, the same {value, unit} shape, a different physical quantity carried by the unit. Direct commands (charge, discharge, heat, cool, idle) are explicit instructions; the auto commands (auto.balanced, auto.reserve, auto.export, and the thermostat’s auto) name a device’s own self-managing mode in canonical vocabulary.
A command present in the device’s commands map is supported. Absent means rejected. There is no supported: boolean field. See capabilities for how each device declares its subset.
Coming soon. Live control for EV chargers and thermostats. The sandbox environment runs the full command surface end to end, so you can build and test the entire integration. A live EV charger or thermostat push returns 422
COMMAND_NOT_SUPPORTED until the live path opens; battery control is live.command selector, the {value, unit} parameters, and the onConflict resolution are identical to the battery example above. Only the verbs and the parameter names differ, and the device tells you those on the read.
Self-documenting parameters
Every numeric parameter is aQuantity: { value, unit }. The unit travels with the value, so 80 is never ambiguous between percent, kilowatts, or amps.
target, power, reserve. target is an upper bound for charge and a lower bound for discharge. power caps the rate. reserve preserves a state-of-charge floor while the mode runs. The thermostat reuses target for heat and cool, and adds heatSetpoint and coolSetpoint for the auto comfort band. The EV charger’s charge and idle take no parameters at all. The shape is always {value, unit}; the unit (percent, kw, celsius) tells you the quantity.
Send a unit the device does not accept and the API returns 422 UNSUPPORTED_UNIT. Send a parameter the mode does not accept and you get 422 UNSUPPORTED_PARAMETER with a deviceCapabilities snapshot in details, so you can fix the call in one round trip. Send a value outside the declared range and you get 422 PARAMETER_OUT_OF_RANGE; the bounds live on the device read, not in the request schema. See error envelope for the full list.
Per-mode execution support
Each command on a device declares anexecution array drawn from { "immediate", "scheduled", "windowed" }. Each token names a distinct request shape:
| Token | Request shape | Meaning |
|---|---|---|
immediate | No start | Fire on receipt. |
scheduled | start only | Defer firing until start. |
windowed | start and end | Run between start and end, then revert. |
execution array returns 422 EXECUTION_NOT_SUPPORTED with details: { requestedExecution, supportedExecution }. On a battery, auto.balanced declares ["immediate", "scheduled"] (no end makes sense for a long-running mode), while charge may declare ["immediate", "scheduled", "windowed"]. EV charger and thermostat commands also declare scheduling shapes: an EV charger’s charge and a thermostat’s heat/cool declare ["immediate", "scheduled", "windowed"], while idle and the thermostat’s auto accept ["immediate", "scheduled"]. The thermostat’s follow_schedule is ["immediate"] only. The array tells you, per command, exactly which shapes the device runs, so you never have to assume.
The execution shape also decides the action’s starting lifecycle state. An immediate push (no start) lands in acknowledged and reaches completed or failed from there. A deferred or windowed push (with start, optionally with end) waits in scheduled until the fire time, then advances to acknowledged and on. scheduled is a state only deferred and windowed pushes pass through; an immediate push never sits there.
Conflict resolution
Only one non-terminal action can target a device at a time. Submit a new push while another is active or scheduled andonConflict decides what happens:
| Strategy | Behaviour |
|---|---|
cancel_and_replace | Cancel the conflicting action, run the new one. |
queue_after | Defer the new action’s start to the conflict’s end. |
| Omitted | Return 409 with the conflicting action ID and the available strategies. |
cancel_and_replace works everywhere. queue_after is honoured only where the device declares it. EV chargers and thermostats declare only cancel_and_replace, so a queue_after push to one returns 422 STRATEGY_NOT_SUPPORTED. The 409 body lists the strategies a given device accepts, so you never guess. The model is documented end-to-end on conflict resolution.
Datetime and time-window contract
start and end are plant-local wall-clock ISO 8601 strings: YYYY-MM-DDTHH:MM:SS. No timezone offset, no Z. The platform interprets the wall-clock in the device’s plant timezone, so a customer in any timezone reads “5pm at the device” the same way. Any offset (Z, +01:00, -05:00) is rejected at the API boundary — the error explains how to drop the suffix.
Relative durations (30m, 1.5h) are accepted on start only and normalised to absolute instants at request time. start must be in the future and within 30 days. end must be after start in wall-clock terms (DST gaps are handled — see below). Time windows are right-open intervals [start, end).
02:30 on the night the UK jumps from 01:00 GMT to 02:00 BST) are rejected with START_NONEXISTENT_WALL_CLOCK. Wall-clocks that occur twice (autumn fall-back overlap) resolve to the first occurrence (the pre-transition offset). Submit a wall-clock outside the overlap if the second occurrence is required.
Responses normalise start and end to absolute UTC. You send 2026-06-10T22:00:00 for a plant in Europe/London during BST, the action read returns 2026-06-10T21:00:00.000Z. The wall-clock you submitted is preserved in plant time, then resolved against the plant’s IANA timezone for storage and dispatch. The UTC echo is the dispatch-ready instant, not a re-interpretation of your intent.
Capability introspection
The device read returns the same vocabulary you post back. AGET /battery/{deviceId} carries commands alongside the rest of the device record under data:
data.commands.charge.parameters on the read maps directly to action.parameters on the write. The bounds (min, max) and units come straight from the device’s declared capabilities. Bounds are optional; an absent min or max means open-ended on that side, as documented in capabilities. Presence in the map means supported. Absence means rejected.
Actions are not settings
Actions answer “what should the device do?” Settings answer “within what bounds should it operate?” Actions are time-boundable, conflict-able, and audited. Settings are persistent, non-conflicting, and fire-and-forget. The litmus test: “does this make sense with a time window?” You can say “charge from 6pm to 10pm”. You would never say “set the safety reserve to 10% from 6pm to 10pm”. If it is time-boundable, it is an action. The boundary drives where a capability lives. An EV charger’s maximum charging power is a ceiling the charger operates under, not an intent it pursues, so it is themax_charge_rate setting (in kw), written through POST /ev-charger/{deviceId}/settings, exactly as the battery’s max_charge_rate is. The charger’s two actions stay clean intents: charge and idle. You set the ceiling once, then start and stop sessions against it.
Actions run through POST /battery/{deviceId}, POST /ev-charger/{deviceId}, or POST /hvac/{deviceId} and create an audit record. Settings, on the device types that declare a settings surface (POST /battery/{deviceId}/settings and POST /ev-charger/{deviceId}/settings), do not. A thermostat exposes commands but no settings surface. See the API reference for the full surface.
Frequently asked questions
Do EV chargers and thermostats use the same push shape as batteries?
Yes. Every device type accepts the same{ action: { command, parameters }, onConflict } body. The verbs differ by type: a battery takes charge, discharge, idle, and the auto modes; an EV charger takes charge and idle; a thermostat takes heat, cool, auto, idle, and follow_schedule. The envelope, the {value, unit} parameters, and the conflict resolution are identical. Read the device’s commands map to see which verbs it accepts.
What is auto.balanced?
auto.balanced is a canonical battery mode that names the device’s self-managing self-consumption mode under a shared name. It is one of three auto modes (auto.balanced, auto.reserve, auto.export) that every supported battery speaks. The client sends the same request shape across every device.
How do I push an action with a time window?
Sendstart and end as plant-local wall-clock ISO 8601 strings (YYYY-MM-DDTHH:MM:SS, no offset, no Z) in the action body. The command must declare windowed in its execution array; charge and discharge do, the auto modes typically do not. The action runs from start, then reverts at end. ISO strings with offsets (Z, +01:00, etc.) are rejected — the platform interprets the wall-clock in the device’s plant timezone. Sub-minute windows are rejected on devices whose schedulers operate at minute resolution.
What is the difference between command and parameters in the action body?
command is the verb the device should pursue, drawn from the set its type defines (a battery’s charge, an EV charger’s charge, a thermostat’s heat). parameters is the bag of inputs that verb accepts. On a battery’s charge: target (an SOC bound), power (a rate cap), reserve (an SOC floor). On a thermostat’s heat: target (a temperature). Some commands accept none; a battery’s auto modes and an EV charger’s charge carry no parameters, since their behaviour is intent-only.
Can I send a charge and an auto.balanced action at the same time?
Only one non-terminal action targets a device at a time. Submitauto.balanced while a charge is scheduled or acknowledged and the API returns 409 CONFLICT unless onConflict resolves it. Use cancel_and_replace to drop the existing action and run the new one, or queue_after to defer the new action until the existing one ends. One active intent per device.
What happens if I send a parameter the device does not support?
The API returns 422UNSUPPORTED_PARAMETER with details.unsupportedParameters[] and a deviceCapabilities snapshot. The snapshot has the same shape as the GET response, so the caller rebuilds the request from the rejection without a second fetch. Sending an unknown unit returns 422 UNSUPPORTED_UNIT with the supported units. Sending a value outside the declared range returns 422 PARAMETER_OUT_OF_RANGE.
Related concepts
Capabilities
How the device tells you which commands, parameters, and units it accepts.
Conflict Resolution
How
onConflict handles overlap and 409s.Scheduling
Push as the only execution primitive. Schedules as coordinators.
Error Envelope
The shape of every rejection, including
UNSUPPORTED_UNIT and EXECUTION_NOT_SUPPORTED.