TL;DR
GET /devicesreturns each device'scapabilities[]— this is the only source of truth for what a device can do right nowPUT /devices/{id}/{capabilityName}to write — returns202 Accepted(async), then re-fetch the device to see the result- If a capability has
interventions[], it's blocked — show the user what to do (e.g. re-authenticate, upgrade subscription) - Capability not in the array? The device doesn't support it. Don't treat it as an error.
- Capabilities can carry an
optionsobject with per-device guardrails (e.g. allowed amperage range) — validate inputs against it, see Capability options time-controlschedule times must be exact five-minute grid points (for example,14:35:00)target-control= guaranteed minimum runtime inside a daily window (auto-planned);time-control= fixed on/off schedules — see target-control details and time-control details- See the Capability Reference for all payloads,
actionDetailsper action, and which actions work on which device category
This documentation is intended for B2B customers of the clever-PV Connect API and explains the capability concept from an API perspective.
Basic idea
A device has a list of capabilities. A capability describes one ability of the device (e.g., turning on/off, setting a charge limit, displaying SoC, connecting a vendor).
- Read: Capabilities are returned via the device endpoints as part of the device object.
- Write: Some capabilities can be updated/executed via a generic "PUT capability" endpoint.
- Important: Capabilities are dynamic (see section "Dynamics: capabilities are not static").
API objects (response view)
Device (simplified)
A device contains, among other fields, model and capabilities.
Code
The core capability's connectivityStatus is one of:
online— device is reachable and reporting.offline— device is unreachable (repeated polling failures, or onboarding not completed).connecting— device onboarded but has not reported any signal yet; status is still being determined.
Capability (response view)
Each capability is an object with:
name: string, the unique capability name (e.g.,core,connect,set-charge-limit)data: arbitrary JSON object (capability-specific; structure depends on the capability)options: arbitrary JSON object with capability-specific metadata (e.g., how the capability should be executed or). It may be empty or missing, and its shape is capability-dependent.interventions: list of blockers (see Interventions (blockers) – meaning & examples) that explain why a capability is currently not usable
Endpoints (read & write capabilities)
Read capabilities
- GET
v1/users/{userId}/devices
Returns a paginated list of devices (wrapper object) incl.capabilities:
Code
- GET
v1/users/{userId}/devices/{deviceId}
Returns a device incl.capabilities.
Write/execute capabilities
- PUT
v1/users/{userId}/devices/{deviceId}/{capability}{capability}is the capability name (e.g.,on-off,connect,set-charge-limit)- Request body is capability-specific (see "Capability Reference")
- Response is
202 Accepted(asynchronous) – then fetch the device again via GET
Example (turn device on/off):
Code
Response:
Code
Dynamics: capabilities are not static
Capabilities can appear, disappear, or be blocked depending on:
- Device category & model (e.g., a car may have
set-charge-limit, an electric meter may not) - Vendor/onboarding flow (e.g.,
connectwith Form/OIDC/OCPP options) - Current device state (online/offline, data available, telemetry delayed)
- Subscription/product enablement (e.g., features may show
subscription-upgrade-required) - Setup completeness (e.g.,
home-location-required,connector-type-required)
Blocked vs unsupported:
- Blocked capability: The capability exists in
capabilities[]but includes one or moreinterventions. It is present but not currently usable. - Unsupported capability: The capability does not exist in
capabilities[]. A PUT to that capability results in a capability-not-supported error. - Not yet listed (no data): Setup and read-only capabilities whose data is not available yet are currently omitted from
capabilities[]instead of being listed with emptydata— e.g.home-locationbefore a location is stored,display-estimated-rangebefore the vehicle has reported a range,add-charging-locationbefore the vendor contract is known. The corresponding setup intervention (e.g.home-location-required) is shown on thecorecapability. APUTto such a setup capability is still accepted (202) if the device model supports it; the capability appears incapabilities[]after the write.
What "CapabilityData" means:
Most capabilities have a corresponding *CapabilityData (and for writable ones often *CapabilityUpdateData) schema that defines the shape of the JSON payload. This is the contract for what you receive in capabilities[].data and what you send in the PUT body for writable capabilities.
Swagger has no path per capability — writes all go through PUT /v1/users/{userId}/devices/{deviceId}/{capability}. Look up a capability's payload under its *CapabilityData / *CapabilityUpdateData schema.
How clients should handle this
- Treat capabilities as a server-driven contract: Do not assume a capability exists.
- If a capability is missing: The device cannot currently do it (or not for this user). Do not treat it as an error.
- If interventions are present: The capability exists, but is currently blocked. Use the intervention id to show the user the next action.
- Always re-fetch after PUT: After a
202 Accepted, fetch the device again via GET to see the new status/blocker. Some writes apply in more than one asynchronous step, so an immediate re-fetch can show a transitional state — compare the returneddataagainst the values you sent and re-read after 1–2 s if they differ (see target-charging details for a concrete case). - Be tolerant of unknown fields: In
data,options, and enum values.
Interventions (blockers) – meaning & examples
Some capabilities include an interventions[] array that explains why the capability is currently blocked (even though it is present in capabilities[]). Each intervention has a stable id you can use to drive UI actions and troubleshooting.
For a full list of known intervention IDs and typical resolutions, see Interventions.
Capability options (dynamic guardrails)
Next to data, a capability can carry an options object with capability-specific metadata — most importantly the allowed input ranges for writes. Options are resolved per device (e.g. the amperage limits of the concrete wallbox model), so read them from the device response instead of hard-coding ranges. When a capability has no options, the field is omitted — always treat options as optional.
Capabilities that currently emit options:
| Capability | options fields | Enforced on PUT? |
|---|---|---|
min-max-amperage | absoluteMinAmperage, absoluteMaxAmperage — hardware limits of the device | Yes — out-of-range or min > max is rejected |
max-amperage | absoluteMaxAmperage | Yes |
set-charging-power | minAmperage, maxAmperage — the currently configured regulation band | Cars: yes. Wallboxes: not checked |
set-charge-limit | minChargeLimit (0), maxChargeLimit (100) | Yes |
phase-mode | availablePhaseModes — the automatic* modes appear only when the model supports phase switching | Automatic modes rejected when unsupported |
charging-mode | chargingMode — list of allowed values | Enum parsing only |
display-charging-state | chargingState — list of possible values | read-only capability |
measurement-purpose | purposes — list of allowed values | — |
connect | flow, formOptions, docUrl | — |
operation-mode | supportedModes (not emitted by every device) | Yes (forceDischarge always rejected) |
Two things to build into your client:
- Validate against the options before sending. Where the server does enforce a range, a violation currently surfaces as
500/errorCode: internalErrorrather than a proper400validation error — so client-side validation against the advertised options is the only way to give users a helpful message. - Ranges without options:
sun-share(sensible: 1–100),priority, andrated-capacityadvertise no range and are not server-side validated — validate client-side.
Error handling (Problem Details)
The OpenAPI specification defines two different error response shapes:
ProblemDetails(generic ASP.NET problem details; used by many endpoints)ConnectApiProblemDetails(partner-friendly problem details withtype+errorCode+traceId+ optional validationissues)
Which one you receive depends on the endpoint and status code.
ConnectApiProblemDetails shape (simplified)
Code
ProblemDetails shape (simplified)
Code
Relevant HTTP status codes for capability PUT
- 400: Request/validation failed (e.g., wrong type, missing required fields)
- 401: Unauthorized (token/app id/signature missing/invalid)
- 402: Payment required (feature/subscription required)
- 412: Precondition failed (e.g., consent/certificate missing)
- 422: Capability not supported (device does not support it / currently not executable)
- 500 with
errorCode: "carLinkedToWallbox": the car is linked to a clever-PV wallbox (connector-type=cpvWallbox) and charging control lives on the wallbox device — see target-charging details. Treat theerrorCodeas the signal, not the status class.
How the optimization features fit together
The write capabilities fall into five groups. Within a group the capabilities depend on each other; across groups they combine (see the individual reference entries for the exact interplay):
- Master switch —
smart-modegates all optimization below (excepttime-controlschedules);preferred-operating-modestores the user-selected regime and force-disables everything onmonitoring. - PV surplus —
solar-optimization(the on/off), tuned byrated-capacity(switch-on threshold),sun-share(minimum solar share),priority(who gets surplus first),switch-delay(debounce), and — on cars/wallboxes —charging-mode(regulation style). - Price —
price-control(threshold-based; cars, wallboxes, switches, heating rods) andprice-optimization(thresholdless heat-pump variant). OR-combined with PV surplus. - Battery SoC —
soc-planuses the home battery as a buffer for this consumer. - Schedules & planners —
time-control(fixed schedules, independent of smart-mode),target-control(runtime target; switches/heating rods),target-charging(energy/SoC target; cars/wallboxes).
Which devices typically have which capabilities?
This is guidance. The decisive factor is always what is returned in the device's
capabilities[].
Car (vehicles)
- Typical (read):
core,display-soc,display-estimated-range,display-charging-state,display-power - Charging control (write):
set-charging-power,set-charge-limit(Tesla only),charging-mode,boost,phase-mode(if supported),min-max-amperage - Optimization (write):
smart-mode,preferred-operating-mode,solar-optimization(+sun-share),price-control,target-charging,soc-plan,priority,time-control - Optional:
phase-switching,sleep-mode,location-detection,battery-capacity+consumption(target-charging setup — the vehicle's capacity is not provided by the vendors, the user enters it) - Onboarding/setup:
connect,home-location,connector-type,add-charging-location(VW CCO only) - Tesla-specific:
install-certificate,set-scopes,support-tesla-vehicle-command-protocol
Notes:
- EV charging is fully controllable through capabilities. The typical app-facing charging modes map as follows:
- Always charge — disable
smart-mode: the car behaves as without any optimization (plug in → charge until full or until the charge limit). - Surplus charging — enable
solar-optimization; addsun-share< 100 for a deliberate grid share. - Charge plan / departure time (daily or per-weekday) —
target-chargingwith an SoC or kWh target byexecutionTime. - Price-optimized —
price-controlwith a ct/kWh threshold.
- Always charge — disable
display-charging-stateis the capability to use for the device's charging state.add-charging-location(VW CCO): the PUT body is empty ({ }) and only verifies that the end user has set a charging location for the clever-PV charging contract in the vehicle/VW app — it does not set a location and does not use the vehicle's current position.chargingLocationIdin the capability'sdatais read-only and identifies that contract. See Interventions →add-charging-location-required.set-charge-limitis the vehicle-wide ceiling (currently only Tesla receives the command); the per-plan “Target SoC” istarget-charging'stargetSoCand can sit below it.- Car linked to a clever-PV wallbox.
connector-type=cpvWallboxmarks the car as charging through a wallbox that clever-PV controls itself (it setslink-to-wallboxtolinked: true). Charging control then lives on the wallbox device: enablingtarget-charging,boost, oron-offon the car is rejected with500/errorCode: "carLinkedToWallbox", andtime-controlschedules written to the car are stored inactive.wallPlugandunknownWallboxdo not link. See target-charging details for detection and the wallbox-side setup.
Wallbox
- Typical (read):
core,display-charging-state,display-charging-current,display-charging-phases,display-power - Charging control (write):
set-charging-power,charging-mode,boost,phase-mode(if supported),min-max-amperage - Optimization (write):
smart-mode,solar-optimization(+sun-share),price-control,target-charging(kWh target only — no SoC available),soc-plan,priority,time-control - Optional:
phase-switching,sleep-mode,connect(vendor/ocpp dependent),battery-capacity+consumption(see notes)
Notes:
- Plug-in does not start charging by itself — whether a session starts depends on the charger's authorization setting and on an enabled optimization; see Charging Behaviour After Plug-in.
- A wallbox is functionally the car's twin: the same optimization capabilities apply, with two differences — no SoC (target-charging works with
targetWhonly), and automatic phase switching (automatic2Phases/automatic3Phasesinphase-mode) is a wallbox specialty that cars don't have. - "Charge as fast as possible right now" is
boost: a single enable forces max-amperage / three-phase charging and suspends the smart optimizations until boost is disabled or the car is unplugged. Boost alone is the full sequence — it turns the wallbox on itself, so noon-offwrite is needed before or after (anonafter boost can even undercut the boost amperage), and a vehicle must be plugged in or the enable is accepted but not applied (re-fetch showsenabled: false) — see the capability reference for the full lifecycle and asset-health guidance (frequent boost sessions are fine; rapidon-off/boosttoggling loops are what wears the hardware). - If a car in the home is configured with
connector-type: cpvWallbox, its charging plan belongs on the wallbox:target-chargingon the wallbox governs whatever vehicle charges there (kWh target only), while the car's owntarget-charging/boost/on-offwrites are rejected withcarLinkedToWallbox— see target-charging details. - For the common "one wallbox, one car" setup, the user can enter the car's
battery-capacity(Wh) andconsumption(Wh/km) on the wallbox. They are not auto-detected; clever-PV uses them to translate a kWh charging target into an approximate km / percent equivalent in the UI — and both are required beforetarget-chargingcan be enabled.
Heat pump
- Typical (read):
core,display-temperature,display-heating-state - Optional (read):
display-power(only when the vendor/model reports power),pv-control,consumption-overview - Onboarding/setup:
connect,email-verification - Control (write, when present in
capabilities[]):smart-mode(master switch — no optimization runs without it),preferred-operating-mode(monitoring/smartControl),solar-optimization(PV-surplus control, tuned viarated-capacity), andprice-optimization(price-optimized running times). Which of these a given model exposes is catalog-driven, so always rely on the device'scapabilities[].
Notes:
-
Deliberately simplified control surface. Heat pumps expose fewer settings than switches or heating rods: there is no
price-control(no user-facing price threshold —price-optimizationis a plain on/off and clever-PV plans the cheapest running times internally), and notarget-control,time-control,soc-plan, oron-off. There is also no manual mode: the meaningfulpreferred-operating-modevalues aremonitoringandsmartControl. -
rated-capacityis pre-filled with 2000 W at device creation (a typical heat-pump draw); the user can override it with the unit's real value. -
No raw setpoint or temperature control. clever-PV never writes a target temperature or power value to a heat pump. Every optimization decision (smart, price, target) is translated into a single internal on/off command that maps to an SG-Ready-style state — recommended (encourage the unit to consume now) or normal (hand control back to the unit's own program). SG-Ready states are not addressable directly through the API. Returning to normal is vendor-dependent: some integrations accept an explicit "back to normal" command, others only support the recommended trigger — there the run simply expires on the vendor side and clever-PV cannot cut it short. Never assume that the absence of a clever-PV command means the unit has stopped; read
display-heating-stateinstead. -
What "on" physically does depends on the vendor. clever-PV sends one recommended signal, but each cloud integration translates it differently — do not build on a single physical behaviour:
- Vaillant — triggers a one-time domestic-hot-water (DHW) overcharge. It is refused while the DHW tank is already at or above its target temperature, or while the vendor reports the overcharge feature as blocked.
- Bosch/Buderus — switches the configured DHW circuits to
Comfort(and back toAutofor normal). This is an operating-mode change on the hot-water circuit, not a one-shot charge. - Viessmann — depends on the control mode configured for the device: either a DHW one-time charge, or a genuine SG-Ready smart-grid state (
recommendedOperation/normalOperation), which the unit may also apply to space heating. - Solvis — sets the heat-source mode, which is not restricted to hot water.
In every case the signal is a recommendation: the unit may defer or ignore it, e.g. when the tank is already at target. A heat pump can alternatively be driven by an external SG-Ready relay modeled as a
switchdevice. -
boostis not available for heat pumps (car/wallbox only). -
display-heating-statereports{ mode, state, type, lastUpdate }:modeis the control regime currently driving the unit (normal= the unit follows its own program,sgReady= clever-PV is currently encouraging consumption via SG-Ready / DHW charge,powerControl= heating rods only),stateisidleorrunning, andtypeis what the unit produces while running (heating,cooling,warmWater, ornullwhen the vendor doesn't report it). Whichtypevalues appear depends on the manufacturer; tolerate unknown values. Whenstateisidle, ignoretype— it is the last known value, not a current activity; only show it whilestateisrunning. -
display-temperaturecurrently exposes only a single temperature value — the best reading available from the vendor; which sensor it comes from (outdoor, room, tank, …) is vendor-dependent. Per-zone temperatures are planned for the future and will be read-only; ignore them for a first integration. We do not provide temperature curves/time series via the Connect API. -
consumption-overviewcan display consumption bars for today and yesterday (see the Electric meter section for the typical capability set).
Electric meter
- Typical (read):
core,display-grid-power,display-solar-power,display-community-power,consumption-overview - Setup (write):
measurement-purpose(one-time / may be immutable depending on the device) - Optional:
connect(if vendor auth is required) - Optional (read, when a battery is attached):
display-battery-state,display-soc(when the vendor exposes SoC telemetry),battery-capacity - Optional (write, when a controllable battery is attached):
operation-modeto set the battery tonormal,idle,forceCharge, orforceDischarge(always readoptions.supportedModes— not every vendor supports discharge);control-permissionto grant clever-PV permission to control the battery (required by some vendors — e.g. SigEnergy, Fronius — beforeoperation-modewrites are accepted);battery-capacityto register the battery's nominal energy capacity (Wh) when the vendor integration does not expose it automatically
Notes:
- Hybrid inverters (a single physical device that combines a grid/PV meter with battery storage) are modeled as a single
electricMeterdevice that additionally exposes the battery-related capabilities listed above. They do not appear as a separatepowerStoragedevice. As always,capabilities[]is the source of truth for which features are actually exposed for a given hybrid inverter (varies by vendor/model). - Battery operating-mode control (
normal/idle/forceCharge/forceDischarge) is exposed as theoperation-modecapability on the electric meter the battery is attached to, not on the power storage device itself. Always readoptions.supportedModesbefore writing a mode. An optionalpowersetpoint (watts,> 0, only withforceCharge/forceDischarge) may be sent; omittingpoweris valid and uses the backend/vendor default. - The battery-related capabilities appear once the battery has been registered on the meter, which happens from live battery telemetry rather than at onboarding. Right after onboarding a storage system they can still be missing from
capabilities[]; poll the device until they show up instead of assuming the battery is unsupported.
PowerStorage (battery/storage)
- Currently unused, no devices available. Batteries are exposed through their hybrid inverters as type electricMeter
Switch
- Typical (read):
core,display-power - Typical (write):
on-off, plus optimization-related settings (depending on product/setup)
Notes:
- Switch devices can participate in optimizations (e.g., as a SG-Ready relay for heat pumps).
- Some devices are dual-purpose and appear in the device list under both
switchandelectricMeter— pick the role by whichmodelIdyou onboard; theelectricMetervariant is metering-only (noon-off). This covers most Shelly metering models and AVM SmartEnergy 200/210. Only Shelly EM/3EM meters additionally requiremeasurement-purpose(gridMeter/pvProduction); choosingpvProductionre-creates the meter as a producingswitchthat reports PV output but is still noton-off-controllable.
When to use which write capability
Many writable capabilities have overlapping intent (e.g. multiple ways to "turn a wallbox on"). Pick by the user goal:
| User goal | Use this capability | Don't confuse with |
|---|---|---|
| Turn a switch / wallbox on or off right now, indefinitely | on-off (state: on/off) | target-control (time-bound), time-control (scheduled) |
| Charge a car / wallbox as fast as possible right now | boost (enabled: true — max amperage, three phases, optimizations suspended until disabled or unplug) | on-off (turns on at current settings, optimizations may throttle it back down) |
| Run a session until a target runtime or end time is reached, then auto-stop | target-control (targetRuntime inside a startTime–endTime window; switches/heating rods — see target-control details). For a SoC/energy target on cars/wallboxes use target-charging | on-off (no auto-stop), time-control (scheduled, recurring) |
| Optimize a switch/relay to only consume PV surplus | solar-optimization (enabled: true; tune via rated-capacity, sun-share, priority, switch-delay) | smart-mode (broader smart logic), on-off (manual override) |
| Run scheduled turn-on/off events (weekly or one-time) | time-control (replace-all schedule list) | target-control (single run), on-off (immediate) |
| Cap a car's max state of charge | set-charge-limit (Tesla only) | target-charging.targetSoC (per-session target) |
| Set a per-session SoC target for a car / wallbox | target-charging (targetSoC) | set-charge-limit (vehicle-wide limit) |
| Force a wallbox into 1-phase or 3-phase charging | phase-mode (single setting) | phase-switching (auto-switch behaviour) |
| Force-charge / force-discharge / idle / normal-mode an attached battery | operation-mode on the electric meter, not the storage device. Always read options.supportedModes — forceDischarge is only valid when listed. | a write to the powerStorage device (no such write) |
| Charge or discharge at a fixed watt setpoint | operation-mode with optional power (watts, > 0, only with forceCharge / forceDischarge). Omitting power is valid and uses the backend/vendor default. | operation-mode without power (same modes, vendor default power) |
| Allow clever-PV to control the battery at all (one-time grant, some vendors) | control-permission (request: "allow") | operation-mode (the per-command mode write — blocked with a control-permission-required intervention until permission is granted) |
| Run a heat pump at the cheapest electricity prices | price-optimization (on/off, no threshold — heat pumps have no price-control) | price-control (threshold-based; cars/wallboxes/switches/heating rods) |
| Disable smart logic and just let the device run as-is | preferred-operating-mode (mode: "manualControl" or "monitoring" — careful: monitoring clears all smart features and schedules, it does not pause them, and switching back to smartControl re-enables nothing: set smart-mode to true again yourself) | smart-mode (master toggle that pauses without clearing) |
Rule of thumb: check capabilities[] for the device — if a capability is present, it is the right path. If two seem to overlap, prefer the more specific one (e.g. target-control over on-off when there is an end condition).
Capability Reference
All examples use camelCase JSON. Enums are serialized as strings (e.g., "online", "threePhases").
Note: "GET" means the capability's
dataobject insideGET /v1/users/{userId}/devices/{deviceId}→capabilities[]. There is no per-capability GET endpoint —GET .../devices/{deviceId}/{capability}returns405 Method Not Allowed; onlyPUTexists on that route.datain GET can differ depending on the vendor (e.g., telemetry availability). Always apply null-tolerance. Schema names refer to OpenAPI component schemas from the Swagger file.
The list below is the single source of truth for per-capability payload formats and detailed semantics (e.g., units like Watt/Ampere, SoC meaning, edge cases). The category lists above are only illustrative; always rely on capabilities[] returned by the API.
core
{
"name": "Device Name",
"connectivityStatus": "online"
}{
"name": "New Device Name"
}connect
{
"connectionId": "00000000-0000-0000-0000-000000000000",
"vendorId": "11111111-1111-1111-1111-111111111111"
}{
"type": "oidcProxy",
"data": {
"url": "proto://intercepted?code=..."
}
}home-location
{
"city": "Munich",
"postalCode": "80331",
"street": "Example Street 1",
"latitude": 48.137154,
"longitude": 11.576124
}{
"city": "Munich",
"postalCode": "80331",
"street": "Example Street 1",
"latitude": 48.137154,
"longitude": 11.576124
}capabilities[] only once a location is stored; before that the home-location-required intervention is shown on core and the PUT is still accepted. Coordinates drive behavior; the address is informational. latitude/longitude are what the system uses to detect whether the vehicle is at its home location (see location-detection), which is what gates solar-optimized charging. city, postalCode and street are stored, but are not used in any control logic. The API does not validate this payload: omitting latitude/longitude defaults them to 0,0, so the request still returns 202 and clears the home-location-required intervention — but a position of 0,0 means the vehicle is never detected as at home and solar-aware charging stays silently disabled. Always send real coordinates. Valid ranges: latitude -90 to +90, longitude -180 to +180 (WGS84); these are not range-checked server-side, so validate them client-side.connector-type
{
"connectorType": "wallPlug",
"powerMode": "eleven"
}connectorType: wallPlug, unknownWallbox, cpvWallbox. powerMode optional: threePointSix, sevenPointTwo, eleven, twentytwo. cpvWallbox links the car to a clever-PV-controlled wallbox: it sets link-to-wallbox to linked: true and moves charging control to the wallbox device — target-charging, boost and on-off on the car are then rejected with 500 carLinkedToWallbox, and time-control schedules on the car are stored inactive. wallPlug and unknownWallbox do not link. See target-charging details.add-charging-location
{
"chargingLocationId": "vendor-location-id"
}{}add-charging-location-required intervention is shown on core. The PUT verifies the charging location entered in the car; if none is set it fails with 412 (ChargingLocationNotVerified).install-certificate
{
"url": "https://..."
}{}data in the device GET carries a url — a Tesla deep link of the form https://tesla.com/_ak/tesla.clever-pv.com (static, identical for all Tesla vehicles). Show/open this link on the user's smartphone with the Tesla app installed: it opens the Tesla app, where the user confirms adding the clever-PV virtual key to the vehicle — that confirmation is the actual installation. Afterwards, PUT with an empty body { } signals "key installed"; on success the certificate-required intervention is cleared. The PUT itself triggers no redirect and no action on the vehicle. Flow: read the url from the device GET → user confirms in the Tesla app → PUT { } → re-GET the device and verify the intervention is gone.set-scopes
{
"url": "https://..."
}{}missing-scopes-required intervention (e.g. the user revoked clever-PV's permissions in their Tesla account) — while it is active the vehicle cannot be controlled. The capability's data in the device GET carries a url to the Tesla account settings (third-party apps section); show this link to the user so they can (re-)grant the required permissions there. Afterwards, PUT with an empty body { } signals "permissions granted": the connection is re-verified and on success the missing-scopes-required intervention is cleared. Flow: read the url from the device GET → user grants the permissions at Tesla → PUT { } → re-GET the device and verify the intervention is gone. If it persists, the permissions were not (fully) granted and the intervention is re-added.support-tesla-vehicle-command-protocol
{
"supported": true
}email-verification
{
"consentGiven": false
}{}on-off-smart
{
"mode": "smart"
}measurement-purpose
{
"purpose": "gridMeter"
}on-off
{
"state": "on"
}{
"state": "off"
}state mirrors the live state, so an on does not carry over to a wallbox's or car's next plug-in (see Charging Behaviour After Plug-in). For "as fast as possible right now" use boost. As with all writes, the PUT returns 202 — re-fetch the device to see the applied state.charging-mode
{
"chargingMode": "clever"
}{
"chargingMode": "casual"
}solar-optimization regulates a charging session — only relevant while solar optimization is enabled; price control, SoC plan, and target charging ignore it. clever: follow every fluctuation of the surplus — charging power is adjusted on each control cycle (~15 s) and charging stops as soon as the surplus is gone. casual: tolerant mode for users who dislike frequent switching — decisions use a rolling average of the surplus, and a passing cloud is tolerated for roughly 2 minutes before down-regulating or stopping. chargeAlways: charging starts on surplus like the other modes, but is never stopped by the surplus loop — once started, power is only regulated between the configured minimum and maximum amperage until the vehicle is full, even if the sun disappears entirely (for small PV systems whose owners want any surplus to trigger a complete charge). Appears on car and wallbox devices. options.chargingMode lists the allowed values; an unknown value is rejected with 400.set-charge-limit
{
"chargeLimit": 80
}chargeLimit is the vehicle-wide maximum state of charge in percent — the absolute ceiling the car never charges past, regardless of any other feature. options advertises the allowed range (minChargeLimit: 0, maxChargeLimit: 100); out-of-range values are rejected. Only Tesla vehicles actually receive the command — for other vendors the value is stored and read back, but never reaches the car (silent no-op). Contrast with target-charging's targetSoC, the per-plan goal: it should be at or below the charge limit, but the API does not validate the pair — a target above the limit is accepted and simply never reached (planning stops once the vehicle reports its limit as reached). Between a reached target and the charge limit, other optimizations (solar surplus, price control) may keep charging unless enableTargetControlLimit caps the session.set-charging-power
{
"desiredAmperage": 10
}desiredAmperage is in Ampere (A). options echoes the device's currently configured regulation band as minAmperage / maxAmperage (configured via min-max-amperage); for cars, values outside this band are rejected — validate against the options client-side.min-max-amperage
{
"minAmperage": 6,
"maxAmperage": 16
}minAmperage and maxAmperage. options carries the hardware guardrails as absoluteMinAmperage / absoluteMaxAmperage; writes outside that range, or with minAmperage > maxAmperage, are rejected — read the options before rendering input controls. Changing the band also recomputes a car's derived rated-capacity.phase-mode
{
"phaseMode": "threePhases"
}phase-switching
{
"supported": true
}sleep-mode
{
"supported": true
}boost
{
"enabled": true
}{
"enabled": true
}car and wallbox devices only. Enabling issues a single charge command at the device's configured maximum amperage (see min-max-amperage); wallboxes with automatic phase switching are switched to three phases first. Boost alone starts the charge — do not combine it with on-off: no on is needed before, and an on written after boost re-applies the configured charging current, which can undercut the boost maximum (with optimization suspended, nothing raises it back up). A vehicle must be plugged in when you enable boost; without one the write is accepted (202) but not applied — a re-fetch still shows { "enabled": false }. Boost is a state, not a pulse: it stays active until you write { "enabled": false } or the vehicle is unplugged (unplugging clears it automatically) — one enable per charging session is enough, and re-enabling while already active is a no-op. While boost is active, the smart optimizations (solar, price, target charging) are suspended for the device, so nothing throttles the charge down. Disabling boost does not switch the device off — the normal optimization simply takes over again on the next control cycle. On a car that is linked to a wallbox the write is rejected — use boost on the wallbox instead. Frequent boost sessions are safe: charging at maximum amperage is normal rated operation for the hardware. What to avoid for asset health is rapid toggling of boost or on-off in your own control loop — every transition is a real contactor switch, possibly a phase switch (with a short charging pause), and a car wake-up, and some manufacturer clouds rate-limit commands; use hysteresis of a few minutes between state changes. As with all writes, the PUT returns 202 — re-fetch the device to see the applied state.link-to-wallbox
{
"linked": true
}{
"linked": true
}car devices only while the home has a wallbox. linked: true means the car charges through a wallbox that clever-PV controls, and charging control lives on that wallbox device: enabling target-charging, boost or on-off on the car is rejected with 500 carLinkedToWallbox — read this field before offering those modes on the car; no intervention is shown for the state. The flag is set automatically by connector-type = cpvWallbox (and cleared by wallPlug/unknownWallbox), or directly via PUT here. A PUT to this capability (with either value) additionally switches off the car's smart-mode, solar/price optimization, boost and charging plan and deactivates its schedules; setting connector-type alone does not. See target-charging details.location-detection
{
"latitude": 48.137154,
"longitude": 11.576124,
"atHome": true
}display-soc
{
"soc": 52,
"lastUpdate": "2026-01-30T12:00:00Z"
}soc is state of charge in percent (0–100). If you need kWh, compute from SoC (%) and the known total capacity (see battery-capacity). Appears on cars and on electricMeter devices with an attached battery (including hybrid inverters), whenever the vendor exposes SoC telemetry.display-estimated-range
{
"range": 280
}display-charging-state
{
"chargingState": "charging"
}charging (energy is actively flowing), completed (the charge target was reached and charging stopped), waitingOnCar (a charging session was initiated but the car is not drawing power — it is still starting up, has suspended charging, or its own charge limit is reached), awaitingStart (cable plugged in and ready, waiting for a start — the normal state from which clever-PV starts charging), error (the vendor reports a fault), noCarConnected (see below), and waitingForStatus (no usable state has been reported yet — typically right after onboarding or while the vendor state can't be mapped). Be tolerant of unknown values. noCarConnected does not mean the device is offline or mis-onboarded. It means the charging cable is currently not plugged into the vehicle — a normal operating state for a car that isn't charging. For VW/Audi/Škoda/Cupra vehicles it maps directly from the vendor's notReadyForCharging / disconnected state. To tell whether the device itself is reachable, use connectivityStatus on the core capability (online / offline), not this value — a device can be online and report noCarConnected at the same time.display-charging-current
{
"amperage": 10
}display-charging-phases
{
"phases": 3
}display-power
{
"power": 3200,
"lastUpdate": "2026-01-30T12:00:00Z"
}display-temperature
{
"temperature": 43.5,
"lastUpdate": "2026-01-30T12:00:00Z"
}display-heating-state
{
"mode": "sgReady",
"state": "running",
"type": "warmWater",
"lastUpdate": "2026-01-30T12:00:00Z"
}display-grid-power
{
"gridPower": -450,
"lastUpdate": "2026-01-30T12:00:00Z"
}display-solar-power
{
"solarPower": 1800,
"lastUpdate": "2026-01-30T12:00:00Z"
}display-community-power
{
"communityPower": 120,
"lastUpdate": "2026-01-30T12:00:00Z"
}display-battery-state
{
"state": "charging",
"chargingPower": 1500,
"lastUpdate": "2026-01-30T12:00:00Z"
}state (idle | charging | discharging | disabled) is the authoritative direction. chargingPower is in Watt (W); treat it as a magnitude — use abs(chargingPower). The sign of chargingPower is not normalised across vendors: some adapters forward the raw vendor value (where positive may mean discharging), others normalise to "positive = charging". It is therefore valid to see, e.g., state = "discharging" together with chargingPower > 0 — always derive direction from state, never from the sign.operation-mode
{
"mode": "normal"
}{
"mode": "forceCharge",
"power": 3000
}electricMeter device when a controllable battery is attached and the vendor adapter supports it. Allowed values on PUT: normal, idle, forceCharge, and forceDischarge when it is listed in options.supportedModes. options.supportedModes, when present, lists the modes this specific battery supports — not every device emits it, so treat it as optional; when it is missing, do not assume forceDischarge is available. options.supportsPowerSetpoint is true when the vendor supports forceCharge (and therefore a watt setpoint). power is optional, in Watt (W), must be > 0, and is accepted only with forceCharge or forceDischarge; omit it to use the backend/vendor default. Upper power bounds are enforced by the vendor, not validated by the API. The watt setpoint applies to this write only — it is not sticky and is not returned on GET (data is { "mode": "..." }). GET may return forceDischarge; unknown only when the internal battery state cannot be mapped to a supported mode. Blocked when the home's battery Smart Price Control automation is active — see smart-price-control-enabled in Interventions. Some vendors (currently SigEnergy and Fronius) additionally require clever-PV to be granted control of the battery first: until control-permission is allowed, this capability carries the control-permission-required intervention and a PUT fails with 400.control-permission
{
"state": "allowed"
}{
"request": "allow"
}electricMeter devices whose battery vendor requires an explicit controllable state (currently SigEnergy and Fronius). state: allowed | pending | forbidden (be tolerant of unknown values). request on PUT: allow or forbid; any other value returns 400. Vendor specifics: for SigEnergy, allow switches the inverter into its remote-control (NorthBound) mode and takes effect immediately; for Fronius, allow starts a vendor registration that sends a verification e-mail to the user — state stays pending until the e-mail is confirmed, then becomes allowed. forbid reverts this (SigEnergy back to self-consumption mode, Fronius deregisters). The state is read from the vendor and fails safe to forbidden when the vendor cannot be reached; it may be cached for up to 1 hour, so a change made directly in the vendor portal can lag — changes made via this API are reflected immediately. Returns 422 capabilityNotSupported when no controllable battery is attached to the meter yet (batteries are detected from live telemetry, so a freshly onboarded inverter may first need to report battery data). While state is not allowed, operation-mode carries the control-permission-required intervention and PUTs to it fail with 400.battery-capacity
{
"batteryCapacity": 10000
}{
"batteryCapacity": 10000
}10000 = 10 kWh. We fill this automatically when the manufacturer reports it, at the moment the battery is first registered; vendors that don't expose it leave it unset. On GET, batteryCapacity is therefore nullable, and that null is the only signal that it is unknown — there is no intervention for a missing capacity. A value you write via PUT is never overwritten by us afterwards. On an electricMeter the capability is only present once a battery has actually been registered on that device, which happens from live battery telemetry rather than at onboarding; while it is absent from capabilities[] a PUT is still accepted (202) but has no effect. Used internally for charging/discharging planning and forecasting. Appears on an electricMeter with an attached battery, and on wallbox devices that report onboard EV battery information.consumption
{
"consumption": 150
}{
"consumption": 150
}150 = 15 kWh/100 km. On GET, consumption is nullable: null means it has not been configured yet. Appears on car and wallbox devices. Together with battery-capacity it forms the target-charging setup: while either value is missing, target-charging carries the target-charging-setup-required intervention and cannot be enabled.price-control
{
"enabled": false,
"threshold": 25
}{
"enabled": true,
"threshold": 25
}threshold, and off again once the price rises above it. threshold is in cents per kilowatt-hour (ct/kWh) — e.g. 25 = 0.25 €/kWh. A common mistake is sending euros: 0.25 means 0.25 ct/kWh, so the device would practically never run. The comparison uses the home's end-customer price — spot price plus the variable grid-fee component of the home's tariff — not the bare exchange price. GET returns the stored configuration; when never configured it reports the defaults enabled: false, threshold: 25. The setting is only meaningful when the home has a dynamic electricity tariff — consider hiding it in your UI otherwise. Combined with solar-optimization the two conditions are OR-linked: the device runs when at least one is met. While enough PV surplus is available the device stays on even when the price is above the threshold (price control never turns off a device under active surplus control), and while the price is below the threshold it keeps running even without surplus. Only takes effect while smart-mode is enabled — disabling smart-mode pauses price control without clearing the configuration. Appears on car, wallbox, switch, and heatingRod devices. Heat pumps deliberately do not have this capability — they use price-optimization instead. Enabling can fail with 402 Payment Required when the subscription does not include price control; the capability then carries the subscription-upgrade-required intervention. As with all writes, the PUT returns 202 — re-fetch the device to see the applied state.price-optimization
{
"enabled": true
}{
"enabled": true
}price-control. Deliberately a plain on/off toggle with no threshold: a heat pump has to run every day anyway, so instead of a user-facing price limit clever-PV plans the most favorable running times in the background (tariff- and solar-aware) and encourages the unit to consume in those slots via SG-Ready — whether it actually heats is decided by the heat pump's own control (see the heat pump notes). Internally this drives the same target-times mechanism that target-control uses on switches and heating rods. Appears on heatPump devices only. Only takes effect while smart-mode is enabled. Enabling can fail with 402 Payment Required (subscription-upgrade-required intervention). As with all writes, the PUT returns 202 — re-fetch the device to see the applied state.solar-optimization
{
"enabled": true,
"ratedCapacity": 2200,
"sunShare": 100
}{
"enabled": true
}{ "enabled": true|false } — the two extra fields in GET are read-only context, configured through their own capabilities: ratedCapacity is the switch-on threshold in watts (the surplus that must be available before the device turns on; set via rated-capacity; for stepless heating rods the system's minimum surplus threshold is returned instead of the configured value), and sunShare is the minimum solar share in percent (100 = run on pure surplus only; lower values let the device keep running with a share of grid power — e.g. 50 means only half the threshold must be covered by surplus; set via sun-share). sunShare defaults to 100 when never configured. Fine-tuning happens via rated-capacity, sun-share, priority (which device gets the surplus first), and switch-delay (debounce against passing clouds). The configuration only takes effect while smart-mode is enabled — disabling smart-mode pauses solar optimization without clearing it. On cars, ratedCapacity is computed automatically from the charging configuration (phases × amperage × 230 V — e.g. 1380 W at 1 phase / 6 A, 4140 W at 3 phases / 6 A) and is re-derived whenever connector-type, phase-mode, or amperage settings change — treat the value as read-only for cars. Enabling can fail with 402 Payment Required when the home's subscription does not include surplus control for this device; in that case the capability carries the subscription-upgrade-required intervention. Disabling always succeeds. As with all writes, the PUT returns 202 — re-fetch the device to see the applied state.pv-control
{
"enabled": true,
"priority": 1,
"ratedCapacity": 2200
}enabled via solar-optimization, priority via priority, ratedCapacity via rated-capacity.smart-mode
{
"enabled": true,
"reason": "solar"
}{
"enabled": true
}wallbox, switch, car, heatPump, and heatingRod devices. The optimization features — solar-optimization, price-control, price-optimization, soc-plan, target-control/target-charging — are configured independently but only run while smart-mode is enabled. Disabling smart-mode pauses all of them at once without clearing their configuration; re-enabling resumes them. Exception: time-control schedules are not gated by smart-mode — active schedules keep firing even while it is disabled (only preferred-operating-mode monitoring deactivates them). Side effects of disabling: heat pumps and heating rods receive an immediate turn-off command, and on cars and wallboxes a running target-charging plan is switched off (not just paused). GET returns enabled plus reason (none | solar | price | soc): the reason why the control loop last switched the device — not which feature is enabled. Target charging and price optimization both report price; legacy switch/wallbox devices always report none. Careful: writing preferred-operating-mode: monitoring force-disables smart-mode in the background, and switching back to smartControl does not re-enable it — you must PUT { "enabled": true } here yourself afterwards.preferred-operating-mode
{
"mode": "smartControl"
}{
"mode": "smartControl"
}manualControl (user operates the device, no smart logic intended), smartControl (clever-PV optimization intended), or monitoring (observe only). The stored value is mostly declarative — the surplus-control loop does not consult it on its control cycles; whether optimization actually runs is governed by the individual feature capabilities (solar-optimization, price-control, smart-mode, …). But writing it is not side-effect-free. Switching to monitoring force-disables the device's automation: it clears the smart-feature flags (solar optimization / smart mode, price control, SoC control, target charging), deactivates all time-control schedules, and for heat pumps and heating rods sends an immediate turn-off command. These flags are cleared, not paused — unlike disabling smart-mode, switching back to smartControl restores nothing; every feature and schedule must be re-enabled explicitly, including smart-mode itself (writing smartControl never sets smart-mode to true for you). Write order matters: set the operating mode first, then smart-mode and the feature toggles — a smart-mode write sent before a switch to monitoring is silently overwritten by it. Per-category behavior of manualControl: on switches, any write other than smartControl (i.e. also manualControl) disables smart mode and target charging; on cars, heat pumps, and heating rods, manualControl only stores the value and touches no flags. Availability: not present on producer (PV-measuring) switches; legacy wallboxes do not persist an operating mode.soc-plan
{
"enabled": true,
"minControlSoc": 20,
"maxControlSoc": 80
}{
"enabled": true,
"minControlSoc": 20,
"maxControlSoc": 80
}maxControlSoc, the device is turned on — and it stays on, drawing from the battery, until the SoC has dropped to minControlSoc, where it is turned off again. Example with { "minControlSoc": 20, "maxControlSoc": 80 }: once the battery exceeds 80 %, the consumer (e.g. a heating rod or wallbox) is activated and keeps running while the battery discharges, until the SoC reaches 20 % — then it switches off and waits for the battery to fill back up past 80 %. The band between the two values is the hysteresis that prevents rapid on/off toggling. Two subtleties: the turn-off at minControlSoc is suppressed while the battery is actively charging (with PV replenishing the battery the device keeps running even at or below the minimum), and the SoC plan takes precedence over the regular solar-optimization loop — a device under active SoC control is exempt from the normal surplus turn-off. Fields: minControlSoc defaults to 30 when never configured; maxControlSoc is nullable — until it is set, the plan never triggers a turn-on. The plan only executes while smart-mode is enabled; writing it while smart-mode is off stores the configuration, but execution stays paused. Availability: only present when the home has a battery; appears on consumer devices (switches — not producer switches —, wallboxes, cars, heating rods). Enabling requires the SoC-control subscription feature — otherwise the PUT fails with 402 Payment Required and the capability carries the subscription-upgrade-required intervention. The plan can also be switched on schedule via time-control's socPlanOn action (with minSoC/maxSoC in actionDetails). As with all writes, the PUT returns 202 — re-fetch the device to see the applied state.rated-capacity
{
"ratedCapacity": 2200
}{
"ratedCapacity": 2200
}2200 for a 2.2 kW load. This value is the switch-on threshold for solar-optimization: while surplus control is enabled, the device is only turned on once the home's available PV surplus exceeds ratedCapacity (scaled by the device's sunShare — at sunShare: 50, only half of it must be covered by surplus). Set it too low and the device switches on before the sun can actually power it, pulling the remainder from the grid; set it too high and it rarely or never switches on despite available surplus. So configure it to the real nameplate/typical consumption of the connected load — this matters especially for switches, where clever-PV cannot know what is plugged in behind the relay. GET returns the currently configured value (0 = not configured yet). Defaults at device creation: heat pumps are pre-filled with 2000 W and heating rods with 3000 W — override them with the unit's real typical draw where known. For cars the value is derived automatically from the charging configuration (phases × amperage × 230 V) and is overwritten again by any connector-type, phase-mode, or amperage write — a PUT here is accepted but not authoritative, so display it read-only for cars. The same value is also visible read-only in pv-control and in the GET data of solar-optimization (exception: stepless heating rods, whose solar-optimization view reports the system's minimum surplus threshold instead, because they modulate from that minimum rather than switching at rated power). Applies to surplus-controllable devices (switches, wallboxes, heating rods, heat pumps — and cars, where it is derived, see above). Not to be confused with battery-capacity, which is an energy amount in watt-hours.priority
{
"priority": 1
}{
"priority": 1
}solar-optimization enabled. Lower number = higher priority. On each control cycle the loop turns eligible devices on in ascending order (the lowest priority value first) and, when the surplus shrinks — or a home battery is being drained to power controlled loads — turns them off in descending order (the highest value first). A device with priority: 1 therefore receives surplus before, and keeps it longer than, a device with priority: 3. With only one surplus-controlled device in the home the value has no visible effect. Duplicate values are allowed — the API does not enforce uniqueness — but when several devices share the same priority their relative order is not guaranteed; assign distinct values whenever the order matters. GET returns the configured value (0 = not configured yet, which sorts ahead of any positive value). The value is also visible read-only in pv-control.switch-delay
{
"switchDelayOn": 300,
"switchDelayOff": 180
}{
"switchDelayOn": 300,
"switchDelayOff": 180
}solar-optimization switching decisions, in seconds. switchDelayOn is the time the surplus condition must hold before the device is turned on; switchDelayOff is the time the surplus must be gone before it is turned off again. This prevents a passing cloud from toggling the load every control cycle — and protects hardware that tolerates frequent cycling poorly (heat pump compressors in particular; heat-pump and heating-rod integrations therefore ship with vendor defaults of several minutes). Values below 60 seconds are silently normalized to 0 (no delay) — sending { "switchDelayOn": 30 } returns 202 but stores 0, so use 0 or ≥ 60. A successful write also clears the device's current control lock, so the new delays take effect on the next control cycle. GET returns the stored values (0 = no delay configured).target-control
{
"enabled": true,
"targetControlLock": null,
"currentRuntime": 1.25,
"hasReachedTarget": false,
"lastTargetControl": "2026-08-04T09:35:00Z",
"startTime": "06:00:00",
"endTime": "18:00:00",
"targetRuntime": "02:00:00",
"startDate": "2026-08-04T04:00:00Z",
"endDate": "2026-08-04T16:00:00Z",
"weekDays": [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday"
],
"type": "repeat",
"useSmartChargePlan": true,
"sendPushNotification": false,
"enableTargetControlLimit": true
}{
"enabled": true,
"startTime": "06:00:00",
"endTime": "18:00:00",
"targetRuntime": "02:00:00",
"weekDays": [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday"
],
"type": "repeat",
"useSmartChargePlan": true,
"sendPushNotification": false,
"enableTargetControlLimit": true
}HH:mm:ss in the home's local timezone; currentRuntime is in hours. PUT is a partial update — only enabled is required, omitted fields keep their value. Appears on switches and heating rods; cars/wallboxes use target-charging, heat pumps use price-optimization (which drives the same mechanism internally). Semantics, field tables, and the enableTargetControlLimit behavior: target-control reference.target-charging
{
"enabled": true,
"targetControlLock": null,
"currentRuntime": 0,
"hasReachedTarget": false,
"lastTargetControl": null,
"executionTime": "07:00:00",
"targetWh": 12000,
"targetSoC": null,
"unitType": "kwh",
"targetDate": "2026-08-07T05:00:00Z",
"weekDays": [
"monday",
"wednesday",
"friday"
],
"type": "repeat",
"useSmartChargePlan": true,
"sendPushNotification": true,
"enableTargetControlLimit": false,
"chargedWh": 3500,
"consumption": 150,
"batteryCapacity": 60000,
"currentSoC": 46,
"socLimit": 80,
"lastUpdate": null
}{
"enabled": true,
"executionTime": "07:00:00",
"targetWh": 12000,
"unitType": "kwh",
"weekDays": [
"monday",
"wednesday",
"friday"
],
"type": "repeat",
"useSmartChargePlan": true,
"sendPushNotification": true,
"enableTargetControlLimit": true
}unitType selects the target kind: kwh targets an energy amount via targetWh (in watt-hours, e.g. 12000 = 12 kWh), percent targets a state of charge via targetSoC (%). On PUT, unitType is required together with the matching target field (targetWh for kwh, targetSoC for percent). executionTime is the completion time of day by which the target should be reached — not a start time; clever-PV plans the charging session backwards from it. Wire format is a strict time-of-day string HH:mm:ss: exactly two digits per component, seconds required, range 00:00:00–23:59:59, interpreted in the home's local timezone. "07:00:00" is valid; "07:00", "7:00:00", and hours > 23 are rejected with 400 (deserialization error naming the property). On GET the field is null until a plan has been configured; on PUT it is optional like the other non-enabled fields — omitting it keeps the stored value. Unlike time-control, no five-minute-grid validation is enforced here yet — but the clever-PV app uses 5-minute steps and server-side validation may follow, so send 5-minute grid values. type (once/repeat) and weekDays schedule it like target-control, and the read-only targetDate is the derived UTC timestamp of the next occurrence. useSmartChargePlan selects the planning strategy: true (the default) places charging into the cheapest slots before the deadline (spot price plus grid fee); false charges as late as possible, so the session finishes just before executionTime. In both cases the plan aims to complete about 15 minutes before executionTime as a safety buffer. The plan optimizes on prices only — it does not schedule around the PV forecast or the home battery. PV interplay: while the planner is actively driving a session the device is exempt from the surplus loop; outside active plan slots, solar-optimization (if enabled) keeps charging on surplus as usual — which is how the gap between a reached target and the vehicle's charge limit can still fill with solar power, unless enableTargetControlLimit locks the device off after the target. Appears on car and wallbox devices — for a car with a controllable vendor API the plan lives on the car device; for wallboxes (e.g. generic OCPP chargers, where the plugged-in vehicle is unknown) it lives on the wallbox, through which charging control is executed. On wallboxes use unitType: "kwh" only — percent/targetSoC is accepted (202) but is a silent no-op, because no SoC or battery data is available to plan against. GET additionally reports chargedWh, consumption, batteryCapacity, currentSoC, socLimit (all nullable) — currentSoC/socLimit come from vehicle telemetry and stay null when none is available (typical for OCPP wallboxes); apply null-tolerance. Requires setup: the device's battery-capacity and consumption must both be configured (> 0) — otherwise the capability carries the target-charging-setup-required intervention and enabling fails with 422 interventionFound (see Interventions). Cars linked to a clever-PV wallbox (connector-type = cpvWallbox, link-to-wallbox linked: true): enabling on the car is rejected with 500 carLinkedToWallbox without an intervention — put the plan on the wallbox device instead. Enabling is applied in two asynchronous steps: an initial plan (08:00:00, 5000 Wh, kwh, once) is created first and your values are applied about a second later, so a GET right after the 202 can return the initial plan. lastUpdate is not populated for this capability. To avoid the transitional read, send the values with enabled: false first and enable with a second PUT; otherwise compare the read-back against what you sent and re-read after 1–2 s. Details: target-charging details.time-control
{
"nextSchedule": {
"id": "schedule-id",
"type": "repeat",
"weekDays": [
"monday"
],
"executionTime": "02:30:00",
"action": "turnOn",
"actionDetails": null,
"isActive": true,
"description": "Example",
"sendPushNotification": false
},
"schedules": [
{
"id": "schedule-id",
"type": "repeat",
"weekDays": [
"monday"
],
"executionTime": "02:30:00",
"action": "turnOn",
"actionDetails": null,
"isActive": true,
"description": "Example",
"sendPushNotification": false
}
]
}{
"schedules": [
{
"id": "existing-schedule-id",
"type": "repeat",
"weekDays": [
"monday"
],
"executionTime": "09:30:00",
"action": "turnOff",
"actionDetails": null,
"isActive": true,
"description": "Turn off Monday morning",
"sendPushNotification": false
},
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "repeat",
"weekDays": [
"tuesday"
],
"executionTime": "10:30:00",
"action": "socPlanOn",
"actionDetails": {
"minSoC": 20,
"maxSoC": 80
},
"isActive": true,
"description": "SoC plan Tuesday morning",
"sendPushNotification": false
}
]
}executionTime must be an exact five-minute grid point. For a recurring on + off pair, send two schedule objects in the same list (one turnOn, one turnOff). Schedules fire independently of smart-mode — only isActive counts; preferred-operating-mode monitoring deactivates all schedules. Not available on heat pumps. Details and validation errors: time-control reference.consumption-overview
{ "series": [{ "startTimeUtc": "2026-01-29T00:00:00Z", "endTimeUtc": "2026-01-30T00:00:00Z", "interval": "hourly", "entries": [{ "timestamp": "2026-01-29T00:00:00", "total": 0.12, "solar": 0.05, "price": 0.00, "normal": 0.07 }], "solarHours": 1.5, "priceHours": 0.0, "normalHours": 22.5 } }] }target-control details
target-control (internally "target times") guarantees a device a minimum runtime inside a daily time window — e.g. "run for 2 hours between 06:00 and 18:00 on weekdays". clever-PV decides when inside the window the device runs; you only define the window, the target runtime, and the repetition.
It appears on switch and heatingRod devices. Cars and wallboxes use the energy-based target-charging capability instead — same concept, but with an energy/SoC target rather than a runtime target. Heat pumps expose price-optimization, a thresholdless on/off façade that drives the same target-times mechanism internally.
How it works
- You configure a window and a target runtime.
startTime/endTimedefine the daily window andtargetRuntimethe on-time the device should accumulate inside it — allHH:mm:ss, interpreted in the home's local timezone.typeselects a single occurrence (once) or a weekly repetition onweekDays(repeat). - The backend derives the concrete occurrence. From the weekdays and window it computes the next
startDate/endDateas UTC timestamps. These are returned read-only in GET; you never write them. - clever-PV plans the run. With
useSmartChargePlan: true(the default) the target runtime is placed into the most favorable slots of the window, using the home's electricity tariff and solar situation. Withfalsethe device simply runs from the window start until the target runtime is reached. - Runtime is accounted while the device actually runs.
currentRuntime(decimal, hours) accumulates while the window is active and the device is on — for a switch while the relay is on, for a heat pump while clever-PV is driving it (seedisplay-heating-state:mode≠normalandstate=running).lastTargetControlis the timestamp of the last accounting tick — informational only. - Target reached. When
currentRuntimereachestargetRuntime,hasReachedTargetbecomestrueand the device is switched off if clever-PV was running it. WithoutenableTargetControlLimit, other optimizations (solar surplus, price control) may still run the device beyond the target — the target is a minimum, not a cap. - End of window. When
endDatepasses, progress resets (currentRuntimeback to0,hasReachedTargetback tofalse). Arepeatconfiguration advancesstartDate/endDateto the next matching weekday; aonceconfiguration automatically flipsenabledtofalse.
enableTargetControlLimit (maximum runtime)
When true, the target runtime becomes a hard cap as well as a minimum: once the target is reached — or outside the planned window — the device is kept off even if solar surplus is available or the electricity price is below the price-control threshold. While the device is locked out, targetControlLock contains the UTC timestamp until which the lock holds (the start of the next window); otherwise it is null. When false, target-control only guarantees the minimum runtime and other optimizations remain free to add runtime.
Write fields (TargetControlCapabilityUpdateData)
PUT is a partial update: enabled is the only required field. Optional fields that are omitted (or null) keep their current value, so you can toggle enabled without resending the configuration.
| Field | Type | Description |
|---|---|---|
enabled | boolean | Required. Turns the feature on or off. Switching from false to true starts a target-times session; true to false stops it. |
startTime | string or null | Window start, HH:mm:ss, home-local time. |
endTime | string or null | Window end, HH:mm:ss, home-local time. Must be after startTime. |
targetRuntime | string or null | Desired on-time inside the window, HH:mm:ss (e.g. 02:00:00 = 2 hours). Should fit inside the window. |
weekDays | string[] or null | e.g. ["monday", "friday"]. Used with type: "repeat". |
type | string or null | once (single occurrence) or repeat (weekly). |
useSmartChargePlan | boolean or null | true = place the runtime into the most favorable slots of the window (tariff/solar aware); false = run plainly from the window start. Defaults to true on first configuration. |
sendPushNotification | boolean or null | Notify the user when the target is reached. |
enableTargetControlLimit | boolean or null | Treat the target as a hard cap (see above). |
Read-only status fields (GET only)
| Field | Type | Description |
|---|---|---|
currentRuntime | number | On-time accumulated in the current window, in hours (decimal, e.g. 1.25 = 1 h 15 min). Resets to 0 at the end of each window. |
hasReachedTarget | boolean | true once currentRuntime has reached targetRuntime for the current window. |
targetControlLock | string or null | UTC timestamp until which the device is locked off (only used with enableTargetControlLimit); null when no lock is active. |
startDate / endDate | string or null | UTC timestamps of the current/next concrete window occurrence, derived from weekDays + startTime/endTime in the home's timezone. |
lastTargetControl | string or null | Timestamp of the last runtime-accounting tick. Internal bookkeeping — do not build logic on it. |
Notes:
- Until target-control has been configured once, the optional fields can be
nullin GET — apply null-tolerance. - REST responses serialize
weekDaysandtypeas camelCase strings ("monday","repeat"). On the live telemetry stream,target-controlpayloads are currently forwarded in the platform-internal representation, where these two enums appear as numbers (weekDays:0= Sunday …6= Saturday;type:0= once,1= repeat). Handle both if you consume the stream.
target-charging details
Cars linked to a clever-PV wallbox. Setting connector-type to cpvWallbox on a car means the vehicle charges through a wallbox that clever-PV controls itself. Charging control then moves to the wallbox device, so that two devices never steer the same charging session:
- Enabling
target-chargingon the car ("enabled": truewhile it is disabled) is rejected with500anderrorCode: "carLinkedToWallbox". Parameter-only updates on an already enabled plan are not affected. The same rejection applies toboostandon-offon the car;time-controlschedules written to the car are accepted but stored inactive. wallPlugandunknownWallboxdo not link the car — the rule is specific tocpvWallbox.- Detect it before writing: the
link-to-wallboxcapability on the car (present when the home has a wallbox) reportslinked: true. This is exactly the state the check reads, and it can also be set directly through that capability.connector-type.connectorType == "cpvWallbox"is the usual way the link gets set and works as a secondary indicator. There is currently no intervention for this state, so hide the plan modes on the car based on these fields instead of letting the user run into the error. - Where the plan goes:
PUT /v1/users/{userId}/devices/{wallboxDeviceId}/target-chargingon the wallbox. It governs whatever vehicle charges at that wallbox. UseunitType: "kwh"withtargetWhonly (percentis accepted with202but not applied — no SoC is available for planning), and configurebattery-capacityandconsumptionon the wallbox first, otherwise enabling fails withtarget-charging-setup-required.
Enabling applies in two asynchronous steps. A PUT with "enabled": true on a device that has no plan yet is processed as (1) activate — an initial plan is created with executionTime: "08:00:00", targetWh: 5000, unitType: "kwh", type: "once", weekDays: [] and targetDate = the next 08:00 local time — and (2) apply your parameters on top of it. The gap between the two steps is typically about a second. A GET inside that window returns the initial plan — not your values and not the previous state. Device detail (GET .../devices/{deviceId}) and device list (GET .../devices) read the same state; there is no separate cache, so which one you call makes no difference.
- There is no per-write status, ETag, or completion signal yet, and
lastUpdateontarget-chargingis not populated — don't rely on it. - Avoid the transitional read: send the plan values first with
"enabled": false(they are stored while disabled), then a secondPUTwith"enabled": true. Activation reuses the stored plan, so no initial plan is created.enabledis required in everyPUT. - Or verify the read-back: compare the re-fetched
dataagainst the fields you sent (executionTime,targetWh/targetSoC,unitType,weekDays,type,useSmartChargePlan;targetDateis derived and recomputed) and re-read after 1–2 s if they differ. If the values still haven't converged after a few seconds, contact support with thetraceIdof the202response.
time-control details
PUT uses replace-all semantics: send the full desired list of schedules. Any schedule not included in the request will be deleted.
Schedules fire at executionTime in the home's local timezone. nextSchedule in GET is also computed against the home's local time.
The API validates every schedule in the incoming list before deleting or recreating any existing schedule. If schedule validation fails, the request returns 400 Bad Request and the existing schedule list is left unchanged.
Execution-time grid
executionTime must be an exact point on the five-minute execution grid:
- Use
HH:mm:ssbetween00:00:00and23:55:00, inclusive. - Minutes must be divisible by 5 (
00,05,10, …,55). - Seconds and fractional seconds must be zero.
Examples: 14:35:00 is valid; 14:37:00, 14:35:12, and 14:37:10 are invalid. An off-grid value returns 400 Bad Request with errorCode: "invalidScheduleExecutionTime" instead of creating a schedule that can never run.
Existing legacy schedules may contain an off-grid time. They can still appear in GET responses, but a replace-all PUT that sends one back unchanged is rejected. Move the time to a five-minute grid point before submitting the full list.
Schedule fields:
| Field | Type | Description |
|---|---|---|
id | string or null | Pass the existing id to update. Use null for a server-generated id, or provide your own GUID. |
type | string | repeat (weekly) or once (one-time). |
weekDays | string[] | Required (not null; use []). e.g. ["monday", "tuesday"]. No duplicates. repeat: ≥1 day (all seven = daily). once: ≤1 day; [] = next executionTime (today if still ahead, else tomorrow). |
executionTime | string | HH:mm:ss on an exact five-minute grid from 00:00:00 through 23:55:00; seconds must be 00. |
action | string | See actions table below. |
actionDetails | object or null | Required for some actions (see the Actions & actionDetails per device category table immediately below this one), null for all others. |
isActive | boolean | Whether the schedule is enabled. |
description | string or null | PUT requires a non-empty description. GET can return null for legacy schedules. |
sendPushNotification | boolean | Send a push notification when the schedule fires. |
Actions & actionDetails per device category:
| Action | Car | Wallbox | Switch | HeatingRod | actionDetails |
|---|---|---|---|---|---|
turnOn | yes | yes | yes | yes | Car/Wallbox: { "amperage": 16, "phaseMode": "singlePhase" } (required). Others: null. |
turnOff | yes | yes | yes | yes | null |
smartOn | yes | yes | yes | yes | null |
boostOn | yes | yes | — | — | null |
boostOff | yes | yes | — | — | null |
socPlanOn | yes | yes | yes | yes | { "minSoC": 20, "maxSoC": 80 } (required) |
socPlanOff | yes | yes | yes | yes | null |
targetTimesOn | — | — | yes | yes | null |
targetTimesOff | — | — | yes | yes | null |
phaseMode values: singlePhase, twoPhases, threePhases, automatic2Phases, automatic3Phases.
Heat pumps do not have time-control — their optimization runs through price-optimization and solar-optimization instead.
Schedules fire independently of smart-mode: an active schedule executes even while smart-mode is disabled. What deactivates schedules is isActive: false — or a write of preferred-operating-mode monitoring, which deactivates all of them. Note that a scheduled action behaves exactly as if the user had performed it manually at that moment; on the next control cycle the smart optimizations (if enabled) may override its effect — e.g. price control can turn a device back on after a scheduled turnOff.
Schedule validation errors
Schedule-specific failures use the ConnectApiProblemDetails response shape. Handle the stable errorCode; use detail only as a human-readable explanation.
| Status | errorCode | Meaning |
|---|---|---|
400 | invalidScheduleExecutionTime | executionTime is outside the supported day range or is not exactly on the five-minute grid. |
400 | invalidSchedulePayload | Schedule fields are missing or inconsistent, such as an empty description, a repeating schedule without weekdays, more than one weekday for once, a weekday listed more than once, or missing required actionDetails. |
409 | scheduleOverlapping | Two schedules in the submitted desired state overlap. |
Example off-grid response:
Code
Client checklist
- Always evaluate the device's
capabilities[](instead of assumptions per vendor/model). - Render interventions in the UI (blocker → clear user action).
- Only apply PUT to capabilities you actually see on the device.
- After PUT, reload the device (asynchronous process). A read immediately after the
202can be transitional — comparedataagainst what you sent and re-read if it differs. - Evaluate ProblemDetails:
type,status,errorCode,traceId.