# Clever-PV Connect API — Full Documentation Bundle > This file concatenates every page of the Clever-PV Connect API documentation > in the same order as the published navigation. MDX imports and JSX components > have been stripped; semantic content is preserved. The live OpenAPI 3.0.4 > specification is appended at the end of this file. > Generated at build time from `pages/*` and `https://api.clever-pv.com/connect/swagger/v1/swagger.json`. --- # Welcome to Clever-PV Connect API *(source: `pages/introduction.mdx`)* ## TL;DR - Single REST API for 80+ vendors (most of the vendors already available in the [clever-PV app](https://clever-pv.com/en/integrationen/)) — you build one integration, we handle the vendor protocols - Auth: OAuth2 Client Credentials → every request uses `Bearer {token}` + a `userId` path param you define - Core flow: `GET /vendors` → connect vendor → `GET /devices` → read/write `capabilities[]` - All device interaction happens through **capabilities** — never hardcode what a device can do, always check `capabilities[]` at runtime The Clever-PV Connect API enables you to integrate and manage devices from various vendors in your applications. Whether you're building a mobile app, web platform, or IoT solution, the Connect API provides a unified interface to connect devices from vendors like Tesla, Viessmann, Vaillant, Solvis, and more. ## What is the Connect API? The Connect API is a RESTful service that enables connecting third-party devices to custom applications. It handles vendor-specific authentication flows, device discovery, and device management, allowing you to focus on building great user experiences rather than managing complex OAuth implementations and vendor-specific protocols. ## Key Features - **Multi-Vendor Support**: Connect devices from multiple vendors through a single, consistent API - **Flexible Authentication**: Supports various authentication methods including OAuth2/OIDC with PKCE, proxy-based OAuth, and form-based authentication - **Device Management**: Discover, link, and manage devices from connected vendors - **Real-time Capabilities**: Access device capabilities and data including charging status, location, power consumption, and more - **Live Telemetry**: Receive real-time device state updates via Azure Event Hubs for continuous monitoring, dashboards, and analytics ## Getting Started To get started with the Connect API, follow our comprehensive [Device Onboarding Flow](/device-onboarding-flow) guide, which walks you through: 1. [Discovering available vendors](/device-onboarding-flow#step-1-list-vendors) 2. [Understanding connection options for each vendor](/device-onboarding-flow#step-2-get-connection-options) 3. Implementing the [appropriate authentication flow](/device-onboarding-flow#when-to-use-which-flow) (`oidcProxy`, `oidc`, `ocpp`, `push`, or `form`) 4. [Creating connections](/device-onboarding-flow#step-4-create-connection) and [linking devices](/device-onboarding-flow#step-8-link-device) 5. Accessing device data and [capabilities](/device-capabilities) 6. Setting up [Live Telemetry](/live-telemetry) for real-time device data streaming For the API-level OAuth2 setup (obtaining your `Bearer` token), see [Authentication](/api-authentication). ## Base URLs [component: BaseUrls] ## API Versioning All endpoints are versioned under a `/v1/` path prefix (e.g., `GET /v1/users/{userId}/devices`). Our versioning policy: - **Additions are backwards-compatible within `/v1/`** — new endpoints, new optional fields on responses, new enum values, and new capabilities can appear at any time without a version bump. Clients must tolerate unknown fields and unknown enum values. - **Breaking changes ship behind a new version prefix** (e.g., `/v2/`). The `/v1/` prefix continues to work in parallel during the deprecation window. - **Deprecations are announced** in this documentation and in the OpenAPI spec via `deprecated: true` before any endpoint is removed. When asking an LLM or copilot to write integration code against this API, prefer the current `/v1/` patterns documented here over any older snippets you may find. ## Using this documentation with AI tools Point your coding assistant at [`/llms.txt`](/llms.txt) for a curated index of every page, or [`/llms-full.txt`](/llms-full.txt) for the entire documentation plus the live OpenAPI spec concatenated into a single file — ready to paste into a chat or feed to a RAG pipeline. --- # Users and Homes *(source: `pages/users-and-homes.mdx`)* ## TL;DR - **No user creation endpoint** — just call any endpoint with your `userId` and the user + home are created on the fly - The `userId` is **your** identifier (min. 5 chars) — use whatever ID your system already has for the user - One user = one home. Use the same `userId` consistently across all calls for that user - **Deleting a user is the one exception**: `DELETE /v1/users/{userId}` never creates on the fly — it hard-deletes the user (home, devices, vendor connections, subscription) and returns `404` if the user doesn't exist ## Overview The Clever-PV Connect API currently operates in a User/Home context. All endpoints require a `userId` parameter to identify which user and home the operation should be performed for. ## Automatic Creation **There are intentionally no dedicated endpoints for creating Users and Homes.** Instead, Users and Homes are created automatically when you first call any endpoint that requires a `userId` parameter. This implicit, on-demand creation mechanism simplifies the integration process: - When you call any Connect API endpoint with a `userId` parameter - If no User exists for that `userId`, the system automatically creates: - A new User with the specified `userId` - A new Home associated with that User (using the same ID) ## How It Works The `userId` is chosen and managed by your application. You can use any string identifier that uniquely identifies a user in your system (e.g., UUID, email, or your internal user ID). ### Example When you make your first API call for a user: ```http GET /v1/users/{userId}/vendors ``` If `{userId}` doesn't exist yet, the API will: 1. Create a new User with that `userId` 2. Create a new Home for that User 3. Then proceed with the requested operation (listing vendors in this example) Subsequent calls with the same `userId` will use the existing User and Home. ### Home timezone When the Home is created on that first call, its timezone is taken from the **`X-Connect-TimeZone`** request header — an IANA identifier such as `Europe/Berlin` or `America/New_York`. If you don't send the header, the Home defaults to **`Europe/Berlin`**. This timezone is more than cosmetic: it defines the **day boundaries** for date-based endpoints — e.g. which midnight-to-midnight window a [Historical Energy Data](/historical-energy-data) `date` selects, and the start-of-day truncation for [electricity tariff](/electricity-tariffs) `from`/`to` dates. Returned timestamps remain **UTC**; only the day boundaries follow the Home's timezone. Send `X-Connect-TimeZone` on the request that first provisions each user so the Home is created with the right timezone from the start. The Home's stored timezone is the single source of truth for these day boundaries. ## Deleting a User When you no longer need a user — for example because the end customer closed their account in your system — you can remove it via the Connect API: ```http DELETE /v1/users/{userId} ``` A successful request returns `204 No Content`. ### What gets deleted This is a **hard delete and cannot be undone**. Deleting a user removes, in one operation: - the User and its Home, - all devices linked to that Home, - all vendor connections (OAuth, OCPP, push, form, …), - the Connect-API-managed subscription. ### Important differences from other endpoints Unlike every other user-scoped endpoint, the delete endpoint **does not create the user on the fly**: - If a user exists for the given `userId`, it is deleted and you receive `204 No Content`. - If no user exists for the given `userId`, the request returns **`404 Not Found`** (error code `clientExternalUserNotFound`) — nothing is created. Deletion is **scoped to your client**: you can only delete users that were provisioned through your own Connect API credentials. A `userId` that belongs to a different client is treated as not found. --- # Connect API - Device Onboarding *(source: `pages/device-onboarding-flow.md`)* ## TL;DR - `GET /vendors` → `GET /vendors/{id}/connection-options` → run the OAuth/form flow → `POST /vendors/{id}/connections` → `GET /vendors/{id}/devices` → `POST /devices` - The `flow` field tells you which auth to implement: `oidcProxy` (WebView + intercept redirect), `oidc` (PKCE + deep link), `ocpp` (user pastes URL in wallbox), `push` (user pastes URL in vendor portal), `form` (text/secret fields). `flow` is returned **per vendor on `GET /vendors`** and again on `GET /connection-options` — call `connection-options` for the form/login details, not just to learn the flow type - `GET /vendors/{id}/devices` returns **VendorDevices** — unlinked candidates read live from the vendor, no clever-PV id. They are not the same thing as the **Devices** returned by `GET /devices` after linking. See [Concept: VendorDevice vs. Device](#concept-vendordevice-vs-device) - After `POST /devices` (linking), the device appears in `GET /devices` with its `capabilities[]` — that's where real interaction starts Complete flow for connecting vendor devices via the Connect API. > **Just browsing the catalog?** The vendor/model catalog is also reachable without authentication via the [Public Vendor Catalog](/public-vendor-catalog) — authenticate to get your app-scoped catalog, or call anonymously for the default clever-PV catalog. Onboarding itself still requires the authenticated flow below. --- ## Step 1: List Vendors ```http GET /v1/users/{userId}/vendors ``` **Response:** ```json { "data": [ { "id": "ce1332aa-5d51-4222-8b4a-f759862b3ced", "name": "Home Connect", "flow": "oidcProxy" }, { "id": "c36018d3-334e-4d44-82b9-324f2ff7de41", "name": "Solvis", "flow": "form" }, { "id": "ae1b4468-217e-440d-a2c7-3c44d283a6c1", "name": "Tesla", "flow": "oidcProxy" }, { "id": "09527b83-6665-4202-9666-a59840ef3a27", "name": "Vaillant", "flow": "form" }, { "id": "e591b6fb-9fdc-4283-ade9-d16266572882", "name": "Viessmann", "flow": "oidc" }, { "id": "32630d0a-467b-4f8f-8dd0-b417e78a0b70", "name": "Volkswagen", "flow": "oidcProxy" }, { "id": "4c8e9a1b-3d2f-4e5c-8a7d-1b2c3d4e5f6a", "name": "go-e V3/Gemini/Pro via OCPP", "flow": "ocpp" }, { "id": "8f2e1d5c-9b4a-4c3e-b2a1-7f6e5d4c3b2a", "name": "Generic Push", "flow": "push" } ] } ``` > **Note:** `flow` is a non-nullable string per vendor. Values: `form | oidcProxy | oidc | ocpp | push`. The vendor → flow mapping is not contractual — re-read it on every onboarding attempt. --- ## Step 2: Get Connection Options ```http GET /v1/users/{userId}/vendors/{vendorId}/connection-options ``` **Response Structure:** ```typescript { flow: "form" | "oidcProxy" | "oidc" | "ocpp" | "push", formOptions: Array<{ type: "text" | "secret" | "login", data: Record }>, docUrl: string | null } ``` > **Note:** Enum values are serialized as camelCase strings. --- ## When to use which flow You don't choose the flow — the **vendor** does. You receive it on `GET /vendors` (per vendor) and again on `GET /connection-options`. Your job is to implement the five flow types so your client can handle whichever vendor the user picks. | Flow | Used by (typical) | What the client must implement | Why this flow | | --- | --- | --- | --- | | `oidcProxy` | Tesla, Volkswagen, Home Connect | A WebView that opens `data.url`, intercepts the redirect URL containing `data.redirect`, and POSTs the full URL back. **No token handling on the client.** | Vendor requires a confidential OAuth client; clever-PV holds the secret and exchanges the code server-side. | | `oidc` | Viessmann | Standard OIDC with **PKCE**: generate `code_verifier`/`code_challenge`, register the deep-link `com.cleverpv.cleverpv://oauthredirect/`, exchange code yourself. | Vendor allows a public OAuth client; PKCE keeps it secure without a server secret. | | `ocpp` | Wallboxes (any OCPP-1.6/2.0.1 hardware) | Display a generated OCPP server URL to the user; they paste it into the wallbox's own configuration UI. **No browser flow.** | OCPP is a wallbox-to-backend protocol — the wallbox itself initiates the connection. | | `push` | Push-based electric meters | After `POST /connections`, read `flowOptions[name="pushUrl"]` from the response and display it to the user; they paste it into their vendor portal. **No browser flow, no client secret.** See the dedicated [Push Meter Onboarding](/push-meter-onboarding) page. | Vendor-initiated server-push — the vendor portal posts telemetry to the URL on its own cadence; no client secret leaves the server. | | `form` | Shelly, Vaillant (and others without OAuth) | Render the returned `formOptions` (text/secret fields **or** a vendor consent WebView) and POST the collected values as `data`. | Vendor has no OAuth at all — either a static API key/serial number, or a vendor-specific consent page that returns parameters in the redirect URL. | **Decision rules for client code:** 1. **Always branch on `flow`.** Never assume a vendor uses a specific flow — vendors can change, and new vendors can be added at any time without a Connect-API version bump. 2. **`oidcProxy` and `oidc` look similar but are NOT interchangeable.** `oidcProxy` requires forwarding the full redirect URL; `oidc` requires you to extract the code yourself and send it together with the original `code_verifier`. 3. **`ocpp` has no user-facing browser step.** If you build an in-app onboarding wizard, the `ocpp` branch must show instructions and the OCPP URL — not a WebView. 4. **`push` also has no browser step.** The URL is not in `formOptions` — it is returned in the `flowOptions` array of the create-connection response (see Step 4). Display the URL, instruct the user to paste it into the vendor portal. 5. **For `form`, inspect `formOptions[].type`** before rendering: `text` and `secret` are simple inputs, `login` is a WebView with vendor-specific redirect-parameter parsing (see [Option D below](#option-d-form-vendor-specific-forms)). The vendor IDs returned by `GET /vendors` are stable, but **vendor → flow mapping is not contractual**. Always re-fetch `/connection-options` per onboarding attempt. --- ## Step 3: OAuth Flow Based on the `flow` value, execute the appropriate OAuth flow: **Flow Types:** - **`oidcProxy`**: OAuth flow proxied through Clever PV servers. User authenticates via vendor's OAuth page, Clever PV handles token exchange. - **`oidc`**: Direct OpenID Connect flow. - **`ocpp`**: Open Charge Point Protocol flow for wallboxes. User manually enters OCPP server URL in their device configuration. - **`push`**: Vendor-initiated server-push for electric meters. User manually enters a generated push URL in the vendor portal. URL + API key are returned in the create-connection response (`flowOptions`). - **`form`**: Non-OAuth authentication using vendor-specific forms (text fields, secrets) or vendor consent pages. ### Option A: `"oidcProxy"` **Example Response from Get Connection Options (here: Tesla):** ```json { "flow": "oidcProxy", "formOptions": [ { "type": "login", "data": { "url": "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/authorize?client_id=da4d8a7f845c-4df5-929b-94d3fb60d002&locale={{ locale }}&prompt=login&redirect_uri=https%3A%2F%2Fapp.clever-pv.com%2Foauth2%2Ftesla&response_type=code&scope=openid%20vehicle_device_data%20offline_access%20vehicle_cmds%20vehicle_charging_cmds%20vehicle_location&require_requested_scopes=true&prompt_missing_scopes=true", "redirect": "oauth2/tesla" } } ], "docUrl": "https://hemssupport.zendesk.com/hc/de/articles/23043735141394-Tesla-Auto" } ``` **Flow:** 1. Open `data.url` in WebView (replace `{{ locale }}` with e.g. `de`) 2. User authenticates with vendor 3. Detect and intercept redirection in WebView: URL contains `data.redirect` string 4. Store the intercepted URL (including the code) in memory for usage in the "Create Connection" step **Redirect Detection:** ```javascript webview.onUrlChange = (url) => { if (url.includes(flowData.redirect)) { // e.g. "oauth2/tesla" const code = new URL(url).searchParams.get('code'); closeWebView(); createConnection(code); } }; ``` **Example Redirect URL:** ``` https://app.clever-pv.com/oauth2/tesla?locale=de-DE&code=EU_7e23957a43ca73afa71ecac056d8c869b8ddd548386bd2e98682e6e5df02&issuer=https%3A%2F%2Ffleet-auth.tesla.com%2Foauth2%2Fv3 ``` --- ### Option B: `"oidc"` (Direct OIDC with PKCE) For this flow you have to register the redirect Url `com.cleverpv.cleverpv://oauthredirect/` as a deep link handler. > **Notice**: The `redirectUrl` is predefined by Clever PV and cannot be customized. You must use the redirect URL returned by the API (`com.cleverpv.cleverpv://oauthredirect/`) this might lead to conflicting deep link handling, when the user also has the clever-PV app installed on the same device. > **Example Response (here: Viessmann):** ```json { "flow": "oidc", "formOptions": [ { "type": "login", "data": { "clientId": "e995fcd7eb8205369bf35cba4c3ab9f1", "redirectUrl": "com.cleverpv.cleverpv://oauthredirect/", "discoveryUrl": "https://iam.viessmann-climatesolutions.com/.well-known/openid-configuration", "scopes": ["IoT", "User", "offline_access"], "grantType": "authorization_code" } } ], "docUrl": null } ``` **Flow:** 1. Fetch OIDC config from `discoveryUrl` 2. Generate PKCE: `code_verifier` (random 64 chars) → `code_challenge` (SHA256 + Base64URL) 3. Build authorization URL with `code_challenge` 4. Register deep link handler for `redirectUrl` scheme 5. User authenticates, app receives deep link with `code` **PKCE Generation:** ```javascript const code_verifier = generateRandomString(64); const code_challenge = base64UrlEncode(sha256(code_verifier)); ``` **Authorization URL:** ``` https://iam.viessmann-climatesolutions.com/idp/v3/authorize ?client_id=e995fcd7eb8205369bf35cba4c3ab9f1 &redirect_uri=com.cleverpv.cleverpv://oauthredirect/ &response_type=code &scope=IoT User offline_access &code_challenge={code_challenge} &code_challenge_method=S256 &state={random_state} ``` **Redirect Detection:** ```javascript app.onDeepLink = (url) => { if (url.startsWith(flowData.redirectUrl)) { const code = new URL(url).searchParams.get('code'); createConnection(code, storedCodeVerifier); } }; ``` --- ### Option C: `"ocpp"` (OCPP Wallbox Protocol) For OCPP-enabled wallboxes, the API provides a unique OCPP server URL that the user must manually configure in their wallbox device. **Example Response:** ```json { "flow": "ocpp", "formOptions": [ { "type": "text", "data": { "value": "wss://ocpp.clever-pv.com//", "readonly": true, "label": "OCPP Connection URL", "description": "Enter this URL in your wallbox configuration" } } ], "docUrl": null } ``` **Flow:** 1. Create the connection first: `POST /vendors/{vendorId}/connections` with `type: "ocpp"` 2. Read the stable URL from `flowOptions[name="ocppUrl"]` in the create response 3. Display that URL to the user and instruct them to open their wallbox's configuration interface 4. User enters the OCPP URL in the wallbox settings, saves and restarts the wallbox (if required) 5. Wallbox connects to Clever PV's OCPP endpoint 6. Device becomes available in "List Devices of Connection" step > **Note:** The URL shown by `GET /connection-options` (example above) is a randomized preview — its device tag changes on every read. Always use the `flowOptions[name="ocppUrl"]` URL from the connection, which is stable from creation onward (also re-surfaced on `GET /vendors/connections`). > **Note:** The wallbox will not appear in the devices list until it successfully connects to the OCPP endpoint for the first time. --- ### Option D: `"form"` (Vendor-Specific Forms) For vendors without OAuth, the API returns form fields to collect. Field types: - `"text"`: Simple text input (e.g., serial number) - `"secret"`: Password input (masked, e.g., API key) - `"login"`: WebView-based consent flow (not OAuth) **Example Response (Text Field):** ```json { "flow": "form", "formOptions": [ { "type": "text", "data": { "name": "serialNumber", "label": "Seriennummer" } } ], "docUrl": "https://www.clever-pv.com/anleitungen/vaillant" } ``` **Text Field Flow:** 1. Display input field for `data.label` 2. User enters value 3. POST with collected data as shown in the [Create Connection - For "form"](#for-form) sub-section. **Example Response (Login Field):** ```json { "flow": "form", "formOptions": [ { "type": "login", "data": { "url": "https://customer.acmecorp.com/consent/...", "redirect": "api.clever-pv.com" } } ], "docUrl": null } ``` **Login Field Workflow:** The `"login"` field resembles a redirect-based flow similar to OAuth, but it's **not standard OIDC/OAuth2**. Instead, it's a vendor-specific flow. When a login field is present, it's always the only field in the array. 1. Open `data.url` in WebView (vendor's consent/login page) 2. User authenticates or grants consent on vendor's page 3. Vendor redirects to URL containing `data.redirect` string 4. Intercept redirect URL and parse query parameters 5. Use parsed parameters as `data` in POST request with `type: "form"` > **Important:** This is not OAuth2/OIDC - there's no authorization code exchange, no tokens. The redirect URL simply contains vendor-specific parameters (like gateway IDs, consent status) that you forward to the Connect API. **Example:** ```javascript // 1. Open vendor's consent/login page webview.open(field.data.url); // 2. Detect redirect (vendor redirects after user action) webview.onUrlChange = (url) => { if (url.includes(field.data.redirect)) { // 3. Parse vendor-specific callback parameters const params = new URL(url).searchParams; const formData = { gatewayId: params.get('gatewayId'), contractStatus: params.get('contractStatus') }; closeWebView(); // 4. Send to Connect API with type: "form" fetch(`/v1/users/{userId}/vendors/{vendorId}/connections`, { method: 'POST', body: JSON.stringify({ type: "form", data: formData }) }); } }; ``` --- ### Option E: `"push"` (Push API for Electric Meters) For push-based electric meters, the API generates a unique push URL **on connection creation** — not in the connection-options response. The user pastes that URL into their vendor portal so the vendor can post telemetry to Clever PV. **Example Response (from `GET /connection-options`):** ```json { "flow": "push", "formOptions": [], "docUrl": "https://hemssupport.zendesk.com/hc/de/articles/22299049097234-Push-API" } ``` > **Note:** `formOptions` is intentionally empty — the URL is minted by `POST /connections` (see [Step 4: For `"push"`](#for-push)) and returned inside `flowOptions`. **Flow:** 1. Call `POST /connections` with `{ "type": "push" }` 2. Read `flowOptions[name="pushUrl"].data` from the response and display it 3. Instruct the user to open their vendor portal and paste the URL into the push/webhook URL field 4. User saves the configuration 5. Vendor starts posting telemetry at its own cadence 6. Device becomes available immediately in [Step 7](#step-7-list-devices-of-a-connection) — you do not need to wait for the first push > **See also:** [Push Meter Onboarding](/push-meter-onboarding) for a complete step-by-step walkthrough with real example payloads. --- ## Step 4: Create Connection > **Heads-up — two unrelated `type` fields:** > The `type` discriminator in the **request body below** must be one of `form`, `oidcProxy`, `oidc`, `ocpp`, `push` (the onboarding flows from `DeviceOnboardingFlow`). > The `type` you may have seen inside `formOptions[]` (e.g. `text`, `secret`, `login`) is a **form-field type** and is unrelated to the connection request — never use it as the request `type`. > **Heads-up — `flowOptions` on the response:** > Some flows return a generic **`flowOptions`** array on the connection response — a list of `{name, data}` pairs surfacing flow-specific values the client needs after the connection exists. Today `push` populates it (with `pushUrl` and `apiKey`) and `ocpp` populates it (with `ocppUrl`). Match entries by `name`; order is not guaranteed. ### For `"oidcProxy"`: ```http POST /v1/users/{userId}/vendors/{vendorId}/connections { "type": "oidcProxy", "data": { "url": "https://app.clever-pv.com/oauth2/tesla?locale=de-DE&code=&issuer=https%3A%2F%2Ffleet-auth.tesla.com%2Foauth2%2Fv3" } } ``` ### For `"oidc"`: ```http POST /v1/users/{userId}/vendors/{vendorId}/connections { "type": "oidc", "data": { "code": "AUTH_CODE_123", "codeChallenge": "original_code_verifier" } } ``` > **Note:** Field is named `codeChallenge` but expects the `code_verifier` value. ### For `"ocpp"`: ```http POST /v1/users/{userId}/vendors/{vendorId}/connections { "type": "ocpp", "data": {} } ``` **Response:** ```json { "connectionId": "2c659624-ea66-49db-ae98-716b2e73fb0d", "vendorId": "", "flowOptions": [ { "name": "ocppUrl", "data": "wss://ocpp.clever-pv.com//" } ] } ``` > **Note:** Read `flowOptions[name="ocppUrl"]` from this response (or re-fetched via `GET /vendors/connections`) — don't compose the URL from `connection-options` yourself. Only one pending OCPP connection is allowed at a time per user. If the wallbox doesn't connect, delete the connection before creating a new one. ### For `"push"`: ```http POST /v1/users/{userId}/vendors/{vendorId}/connections { "type": "push" } ``` **Response:** ```json { "connectionId": "", "vendorId": "e4f7a2b5-d8c1-4a4f-b7a2-5e8c1d4f7a2b", "flowOptions": [ { "name": "pushUrl", "data": "https://push.clever-pv.com/api/v2//generic-push/?code=" }, { "name": "apiKey", "data": "" } ] } ``` > **Note:** The push flow needs no `data` payload — the connection itself mints the credential. Read `pushUrl` and `apiKey` from the `flowOptions` array (match by `name`, not by index). Multiple open push connections per vendor are allowed. ### For `"form"`: ```http POST /v1/users/{userId}/vendors/{vendorId}/connections { "type": "form", "data": { "serialNumber": "1234567890" } } ``` Or with login field redirect parameters: ```http POST /v1/users/{userId}/vendors/{vendorId}/connections { "type": "form", "data": { "gatewayId": "abc123", "contractStatus": "accepted" } } ``` **Response:** ```json { "connectionId": "4d42038f-0da4-4b07-b6bb-f48e6c54cd3e", "vendorId": "ae1b4468-217e-440d-a2c7-3c44d283a6c1" } ``` --- ## Step 5: Delete Connection (Optional) **Currently only supported for specific flow.** Support for missing flows will be implemented soon. For OCPP, this is mandatory cleanup when a wallbox fails to connect — only one pending OCPP connection is allowed per user. For Push, deletion is purely optional; multiple open push connections are allowed, so you only need to delete to revoke an API key. ```http DELETE /v1/users/{userId}/vendors/{vendorId}/connections/{connectionId} ``` **Response:** `204 No Content` on success > **Important:** All devices attached to the connection must be deleted first (via `DELETE /v1/users/{userId}/devices/{deviceId}` — see [Step 10: Delete Device](#step-10-delete-device)). If you attempt to delete a connection with linked devices, the endpoint will return an error. **Use cases:** - Wallbox failed to connect to the OCPP endpoint - User entered incorrect configuration - Need to create a new OCPP connection (must delete the existing one first) --- ## Step 6: List User Connections (Optional) ```http GET /v1/users/{userId}/vendors/connections?vendorId={vendorId} Authorization: Bearer {jwt} ``` **Response:** ```json { "data": [ { "connectionId": "4d42038f-0da4-4b07-b6bb-f48e6c54cd3e", "vendorId": "ae1b4468-217e-440d-a2c7-3c44d283a6c1" }, { "connectionId": "", "vendorId": "e4f7a2b5-d8c1-4a4f-b7a2-5e8c1d4f7a2b", "flowOptions": [ { "name": "pushUrl", "data": "https://push.clever-pv.com/api/v2//generic-push/?code=" }, { "name": "apiKey", "data": "" } ] }, { "connectionId": "2c659624-ea66-49db-ae98-716b2e73fb0d", "vendorId": "4c8e9a1b-3d2f-4e5c-8a7d-1b2c3d4e5f6a", "flowOptions": [ { "name": "ocppUrl", "data": "wss://ocpp.clever-pv.com//" } ] } ] } ``` > **Note:** `flowOptions` is re-surfaced on every list call for flows that need it. Today `push` populates it (`pushUrl`, `apiKey`) and `ocpp` populates it (`ocppUrl`); for other connections the field is `null` / omitted. Match items by `name`, not by index — order is not guaranteed. --- ## Concept: VendorDevice vs. Device Step 7 and Step 9 below both return something you might call "a device" — they are **not** the same resource, and conflating them is the most common source of confusion in this flow. | | **VendorDevice** (Step 7) | **Device** (Step 8 / 9) | | --- | --- | --- | | Returned by | `GET /vendors/{vendorId}/devices?connectionId=` | `POST /devices`, `GET /devices`, `GET /devices/{deviceId}` | | Identity | No clever-PV id. Identified by `externalId` (the vendor's own serial number / VIN / gateway id) + the `connectionId` it was found under | Stable clever-PV `id` (GUID) — used for every further call (`PATCH`, `PUT /{capability}`, `DELETE`) | | Where it comes from | Fetched **live from the vendor's own API** on every call — not stored by clever-PV. It disappears from the list as soon as it's linked, or removed from the vendor's account | A **persisted clever-PV resource**, created exactly once via `POST /devices` | | What it's for | A candidate to show the user so they can pick what to link | The linked, functional device — the only place `capabilities[]` (telemetry, control, interventions) exist | **Linking one to the other:** `POST /devices` (Step 8) takes the VendorDevice's `externalId`, its `model.id`, and the `connectionId` — that's the entire bridge between the two objects. `name`/`text` on the VendorDevice are display suggestions only; they are **not** copied over automatically, so pass them again in the `POST /devices` body if you want the same label to stick. ### What `available: false` actually means The API collapses several distinct situations into a single boolean: - a Device already exists for this `externalId` — **in the current home, or in a different home** (the same vendor account, and therefore the same `externalId`, can be visible to more than one clever-PV home) - the vendor itself reports the device as offline, on unsupported firmware, or an unsupported model > **Note:** The response only exposes `available`, not the reason behind it — you cannot tell "already linked by you", "already linked under a different home", and "vendor says it's unusable" apart from the VendorDevice alone. > **Important — this is enforced, not just advisory:** `externalId` uniqueness is checked **globally across all homes** when you call `POST /devices`, not just within the current home. Linking a VendorDevice that is `available: false` — or one that became unavailable between your `GET` and your `POST` — returns `409 Conflict` with `errorCode: DUPLICATE_DEVICE`. It does not return the existing Device or otherwise succeed silently. --- ## Step 7: List Devices of a Connection ```http GET /v1/users/{userId}/vendors/{vendorId}/devices?connectionId={connectionId} ``` **Response:** ```json { "data": [ { "name": "Test Tesla", "text": "", "model": { "id": "1ffc08b8-0319-4790-a7d9-0aa5a56fe34d", "name": "Model Y", "category": "car" }, "externalId": "", "available": true, "connectionId": "4d42038f-0da4-4b07-b6bb-f48e6c54cd3e" } ] } ``` --- ## Step 8: Link Device ```http POST /v1/users/{userId}/devices Content-Type: application/json { "name": "Test Tesla", "externalId": "", "modelId": "1ffc08b8-0319-4790-a7d9-0aa5a56fe34d", "connectionId": "4d42038f-0da4-4b07-b6bb-f48e6c54cd3e" } ``` **Response** Similar to result of step "Get Linked Devices" --- ## Step 9: Get Linked Devices ```http GET /v1/users/{userId}/devices ``` > **Note:** For push-style devices, the `connect` capability's `data` carries a `flowOptions` array mirroring the connection-level `flowOptions` (`pushUrl`, `apiKey`). OCPP devices behave the same way — their `connect` capability surfaces `ocppUrl` so you can recover the WebSocket URL from the device alone without re-fetching the connection. See [Push Meter Onboarding → Step 7](/push-meter-onboarding#step-7-create-device-in-connect-api) for the push shape. **Response:** ```json { "data": [ { "id": "0f43fcf9-62cc-4bab-88b0-b0a4c12bf4b6", "name": "Test Tesla", "externalId": "", "model": { "id": "1ffc08b8-0319-4790-a7d9-0aa5a56fe34d", "name": "Model Y", "category": "car", "vendor": { "id": "ae1b4468-217e-440d-a2c7-3c44d283a6c1", "name": "Tesla" } }, "capabilities": [ { "name": "location-detection", "data": { "latitude": 11.1111111, "longitude": 22.2222222 }, "options": null, "interventions": [] }, { "name": "on-off", "data": { "state": "on" }, "options": null, "interventions": [] }, { "name": "display-power", "data": { "power": 10700, "lastUpdate": null }, "options": null, "interventions": [] }, { "name": "display-charging-state", "data": { "chargingState": "charging" }, "options": { "chargingState": ["noCarConnected", "charging", "waitingOnCar", "completed", "error", "awaitingStart", "waitingForStatus"] }, "interventions": [] }, { "name": "solar-optimization", "data": { "enabled": false, "ratedCapacity": 4140, "sunShare": 100 }, "options": null, "interventions": [] }, { "name": "display-soc", "data": { "soc": 56, "lastUpdate": null }, "options": null, "interventions": [] }, { "name": "core", "data": { "name": "Test Tesla", "connectivityStatus": "online" }, "options": null, "interventions": [] }, { "name": "min-max-amperage", "data": { "minAmperage": 6, "maxAmperage": 16 }, "options": { "absoluteMinAmperage": 6, "absoluteMaxAmperage": 16 }, "interventions": [] }, { "name": "set-charge-limit", "data": { "chargeLimit": 100 }, "options": { "minChargeLimit": 0, "maxChargeLimit": 100 }, "interventions": [] }, { "name": "set-charging-power", "data": { "desiredAmperage": 5 }, "options": { "minAmperage": 6, "maxAmperage": 16 }, "interventions": [] }, { "name": "display-charging-phases", "data": { "phases": 3 }, "options": null, "interventions": [] }, { "name": "phase-mode", "data": { "phaseMode": "threePhases" }, "options": { "availablePhaseModes": ["singlePhase", "twoPhases", "threePhases"] }, "interventions": [] }, { "name": "display-charging-current", "data": { "amperage": 16 }, "options": null, "interventions": [] }, { "name": "rated-capacity", "data": { "ratedCapacity": 4140 }, "options": null, "interventions": [] }, { "name": "display-estimated-range", "data": { "range": 236 }, "options": null, "interventions": [] }, { "name": "connect", "data": { "connectionId": "e50815a9-411f-404f-86c0-202912fd8b34", "vendorId": "ae1b4468-217e-440d-a2c7-3c44d283a6c1" }, "options": { "flow": "oidcProxy", "formOptions": [ { "name": "login", "type": "login", "data": { "url": "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/authorize?client_id=da4d8a7f845c-4df5-929b-94d3fb60d002&locale={{ locale }}&prompt=login&redirect_uri=https%3A%2F%2Fapp.clever-pv.com%2Foauth2%2Ftesla&response_type=code&scope=openid%20vehicle_device_data%20offline_access%20vehicle_cmds%20vehicle_charging_cmds%20vehicle_location&require_requested_scopes=true&prompt_missing_scopes=true", "redirect": "oauth2/tesla" } } ], "docUrl": "https://hemssupport.zendesk.com/hc/de/articles/23043735141394-Tesla-Auto" }, "interventions": [] } ] } ] } ``` --- ## Step 10: Delete Device Unlinks a device. Some devices run a full vendor facing reset path (eg.: SG-Ready back to `Normal` if applicable) that honours `force`. `force` does ignore errors on some vendors here. ```http DELETE /v1/users/{userId}/devices/{deviceId}?force={true|false} ``` | Parameter | Default | Meaning | | --- | --- | --- | | `force` (query) | `false` | If `true`, only delete locally even when the vendor reset fails. Honoured by a subset of vendors. | **Responses:** | Status | When | | --- | --- | | `204 No Content` | Deleted (always returned when `force=true`, unless the device doesn't exist). | | `404 Not Found` | No such device for this user. | | `409 Conflict` | `force=false` and vendor reset failed — device was **NOT** deleted. | **Errorcodes:** | `code` | Meaning | | --- | --- | | `connectionExpired` | Refresh token invalid. An `authenticationRequired` intervention is added to the device's `core` capability. | | `deviceUnreachable` | Vendor server unreachable. Safe to retry. | | `resetFailed` | Any other vendor error during reset/delete notification. | ```json { "code": "deviceUnreachable", "message": "Could not reach the manufacturer's server. The device was NOT removed. Use force=true to delete anyway." } ``` **When to use `force=true`:** eg: the vendor account is gone, the hardware is removed, the connection cannot be restored, or the user has confirmed deletion after a `409`. Default to `force=false` and surface the 409 message so the user can make that choice. **Deleting a whole connection:** connections cannot be deleted while devices are linked. Delete each device first (Step 10), then the connection ([Step 5](#step-5-delete-connection-optional)). --- # Capabilities *(source: `pages/device-capabilities.mdx`)* ## TL;DR - `GET /devices` returns each device's `capabilities[]` — this is the **only** source of truth for what a device can do right now - `PUT /devices/{id}/{capabilityName}` to write — returns `202 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. - `time-control` schedule times must be exact five-minute grid points (for example, `14:35:00`) - See the [Capability Reference](#capability-reference) for all payloads, `actionDetails` per 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"](#dynamics-capabilities-are-not-static)). ## API objects (response view) ### Device (simplified) A device contains, among other fields, `model` and `capabilities`. ```json { "id": "00000000-0000-0000-0000-000000000000", "name": "My Device", "externalId": "vendor-specific-id", "model": { "id": "11111111-1111-1111-1111-111111111111", "name": "Vendor Model Display Name", "category": "car", "vendor": { "id": "22222222-2222-2222-2222-222222222222", "name": "Vendor Display Name" } }, "capabilities": [ { "name": "core", "data": { "name": "My Device", "connectivityStatus": "online" }, "options": { }, "interventions": [ { "id": "authentication-required" } ] } ] } ``` 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](#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`: ```json { "data": [ /* DeviceDto[] */ ], "cursor": "opaque-cursor", "size": 50, "total": 123, "hasMoreResults": true } ``` - **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): ```http PUT /v1/users/USER_ID/devices/DEVICE_ID/on-off Content-Type: application/json { "state": "on" } ``` Response: ```http 202 Accepted ``` ## 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., `connect` with 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 more `interventions`. 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. **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. ### 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. - **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](./interventions)**. ## 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 with `type` + `errorCode` + `traceId` + optional validation `issues`) Which one you receive depends on the endpoint and status code. ### `ConnectApiProblemDetails` shape (simplified) ```json { "type": "capabilityNotSupported", "title": "Capability Not Supported", "status": 422, "detail": "Human readable message", "traceId": "00-...-...", "errorCode": "capabilityNotSupported", "issues": [ { "field": "data.someField", "message": "must not be empty", "code": "NotEmptyValidator" } ], "extensions": { } } ``` ### `ProblemDetails` shape (simplified) ```json { "type": "https://example/problem-type", "title": "Bad Request", "status": 400, "detail": "Human readable message", "instance": "https://example/instance" } ``` ### 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) ## 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` - **Typical (write)**: `set-charge-limit`, `set-charging-power`, `charging-mode`, `phase-mode` (if supported) - **Optional**: `min-max-amperage`, `phase-switching`, `sleep-mode`, `location-detection` - **Onboarding/setup**: `connect`, `home-location`, `connector-type`, (vendor-specific) `add-charging-location` - **Tesla-specific**: `install-certificate`, `set-scopes`, `support-tesla-vehicle-command-protocol` Notes: - `display-charging-state` is the capability to use for the device's **charging state**. - `set-charge-limit` limits the car battery (currently only supported for Tesla). - “Target SoC” is part of `target-charging` (`TargetChargingCapabilityData.targetSoC`). ### Wallbox - **Typical (read)**: `core`, `display-charging-state`, `display-charging-current`, `display-charging-phases`, `display-power` - **Typical (write)**: `set-charging-power`, `phase-mode` (if supported) - **Optional**: `phase-switching`, `sleep-mode`, `connect` (vendor/ocpp dependent), [`battery-capacity`](#cap-battery-capacity) (when the wallbox reports onboard EV battery info) ### 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[]`)**: heat-pump optimization is exposed through the same write capabilities as other controllable devices — e.g. `smart-mode` (master switch), `preferred-operating-mode`, `price-control`, `target-control`, and the PV-surplus tuning settings `priority` / `rated-capacity` / `switch-delay`. Which of these a given model exposes is catalog-driven, so **always** rely on the device's `capabilities[]`. Notes: - **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** 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. - **What "on" physically does:** for the integrated cloud vendors (Vaillant, Bosch/Buderus, Viessmann, Solvis) the *recommended* signal triggers a **domestic-hot-water (DHW) charge** via the vendor cloud — it does **not** drive the space-heating circuit, and the unit may defer or ignore it (e.g. when the DHW tank is already at its target temperature). A heat pump can alternatively be driven by an external **SG-Ready relay** modeled as a [`switch`](#switch) device. - **`boost` is not available for heat pumps** (car/wallbox only). - `display-heating-state` reports the current `mode` / `state` / `reason` (e.g. `reason: "solar"` or `"price"`) — i.e. which optimization currently drives the unit. Enum values are vendor-dependent; tolerate unknowns. - `display-temperature` currently exposes only a single temperature value (expected to be expanded later). We do not provide temperature curves/time series via the Connect API. - `consumption-overview` can display consumption bars for today and yesterday (see the [Electric meter](#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`](#cap-display-battery-state), [`display-soc`](#cap-display-soc) (when the vendor exposes SoC telemetry), [`battery-capacity`](#cap-battery-capacity) - **Optional (write, when a controllable battery is attached)**: [`operation-mode`](#cap-operation-mode) to set the battery to `normal`, `idle`, or `forceCharge`; [`control-permission`](#cap-control-permission) to grant clever-PV permission to control the battery (required by some vendors — e.g. SigEnergy, Fronius — before `operation-mode` writes are accepted); [`battery-capacity`](#cap-battery-capacity) to 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 `electricMeter` device** that additionally exposes the battery-related capabilities listed above. They do **not** appear as a separate `powerStorage` device. 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`) is exposed as the [`operation-mode`](#cap-operation-mode) capability on the **electric meter** the battery is attached to, not on the power storage device itself. ### 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** `switch` and `electricMeter` — pick the role by which `modelId` you onboard; the `electricMeter` variant is metering-only (no `on-off`). This covers most Shelly metering models and **AVM SmartEnergy 200/210**. Only Shelly EM/3EM meters additionally require `measurement-purpose` (`gridMeter`/`pvProduction`); choosing `pvProduction` re-creates the meter as a *producing* `switch` that reports PV output but is still not `on-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) | | Run a session **until a target SoC, runtime, or end time is reached**, then auto-stop | `target-control` (with `targetRuntime`/`endTime`/SoC) | `on-off` (no auto-stop), `time-control` (scheduled, recurring) | | Optimize a switch/relay to **only consume PV surplus** | `solar-optimization` (`enabled: true`, `sunShare`) | `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 / idle / normal-mode an attached battery | `operation-mode` on the **electric meter**, not the storage device | a write to the `powerStorage` device (no such write) | | 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) | | Disable smart logic and just let the device run as-is | `preferred-operating-mode` (`mode: "manual"` or `"smartControl"`) | `smart-mode` (per-feature toggle) | **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: `data` in 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. [component: CapabilityReferenceTable] ### 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. 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:ss` between `00:00:00` and `23: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[]` | e.g. `["monday", "tuesday"]`. `repeat` requires at least one day; `once` accepts at most one. | | `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](#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`. #### 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`, or missing required `actionDetails`. | | `409` | `scheduleOverlapping` | Two schedules in the submitted desired state overlap. | Example off-grid response: ```json { "type": "validationFailed", "title": "Invalid Schedule Execution Time", "status": 400, "detail": "Execution time '14:37:00' is invalid. Schedules are executed on a 5-minute grid: the time must be between 00:00:00 and 23:55:00, minutes must be a multiple of 5 and seconds must be 0 (e.g. 14:35:00).", "traceId": "00-...-...", "errorCode": "invalidScheduleExecutionTime" } ``` ## 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). - Evaluate **ProblemDetails**: `type`, `status`, `errorCode`, `traceId`. --- # Live Telemetry *(source: `pages/live-telemetry.mdx`)* ## TL;DR - Push-based device updates via **Azure Event Hubs** — activate in the [Partner Portal](https://portal.clever-pv.com/connect-api/telemetry), connect with any Event Hubs SDK (C#, Python, JS) - Each event: `{ deviceId, clientId, timestamp, capabilities[] }` — same capability names and data shapes as the REST API - Filter by `clientId` to route events per customer; events are **at-least-once** — deduplicate on `deviceId` + `timestamp` - 7-day retention, 32 partitions — use checkpointing in production to survive consumer restarts The Live Telemetry feature delivers **real-time device state updates** directly to your infrastructure via [Azure Event Hubs](https://learn.microsoft.com/en-us/azure/event-hubs/event-hubs-about). Instead of polling the device endpoints, your application receives a continuous stream of telemetry events as soon as device data changes. --- ## Polling vs. Live Telemetry | | Polling (`GET /devices/{id}`) | Live Telemetry (Event Hub) | | ------------------ | ------------------------------------------- | ------------------------------------------------------- | | **Delivery model** | Pull – you request data on demand | Push – data is delivered as it arrives | | **Latency** | Depends on your polling interval | Near real-time (seconds) | | **Data scope** | Full device snapshot incl. all capabilities | Only telemetry-relevant capabilities with latest values | | **Use case** | UI refresh, on-demand reads | Dashboards, monitoring, analytics, alerting | Live Telemetry does **not** replace the device REST API. Use both together: the REST API for device management, capability writes, and on-demand reads; the telemetry stream for continuous monitoring. --- ## Activation & credentials Live Telemetry is activated and managed through the [clever-PV Partner Portal](https://portal.clever-pv.com/connect-api/telemetry). No API calls are required for setup. ### Step 1: Activate telemetry 1. Log in to the **Partner Portal** 2. Navigate to **Connect API** > **Telemetry** 3. Click **Activate Telemetry** After activation, the portal displays your Event Hub credentials: - **Connection String** — a full Azure Event Hubs connection string with listen (read-only) permissions - **Event Hub Name** — the name of your dedicated Event Hub (format: `telemetry-{name}`) **Example connection string:** ``` Endpoint=sb://cpv-eh-prod.servicebus.windows.net/;SharedAccessKeyName=listen-name;SharedAccessKey=...;EntityPath=telemetry-name ``` > **Important:** Store the connection string securely. It grants read access to your entire telemetry stream. You can retrieve it again at any time from the Partner Portal. ### Step 2: Use the credentials in your application Copy the **connection string** and **Event Hub name** from the Partner Portal and use them in your consumer application (see [Connecting to the Event Hub](#connecting-to-the-event-hub) below). --- ## Connecting to the Event Hub Use the official Azure Event Hubs SDK in your preferred language to consume events. ### C# (.NET) Install the NuGet package: ```bash dotnet add package Azure.Messaging.EventHubs dotnet add package Azure.Messaging.EventHubs.Processor ``` Consume events: ```csharp using Azure.Messaging.EventHubs; using Azure.Messaging.EventHubs.Consumer; using System.Text; var connectionString = "Endpoint=sb://..."; var eventHubName = "telemetry-yourpartner"; var consumerGroup = EventHubConsumerClient.DefaultConsumerGroupName; await using var consumer = new EventHubConsumerClient(consumerGroup, connectionString, eventHubName); await foreach (PartitionEvent partitionEvent in consumer.ReadEventsAsync()) { var json = Encoding.UTF8.GetString(partitionEvent.Data.EventBody.ToArray()); Console.WriteLine(json); } ``` > For production use, consider the `EventProcessorClient` with Azure Blob Storage for checkpointing. See [Microsoft documentation](https://learn.microsoft.com/en-us/azure/event-hubs/event-hubs-dotnet-standard-getstarted-send). ### Python Install the package: ```bash pip install azure-eventhub ``` Consume events: ```python from azure.eventhub import EventHubConsumerClient import json connection_str = "Endpoint=sb://..." eventhub_name = "telemetry-yourpartner" consumer_group = "$Default" def on_event(partition_context, event): data = json.loads(event.body_as_str()) print(f"Device: {data['deviceId']}, Capabilities: {len(data['capabilities'])}") partition_context.update_checkpoint(event) client = EventHubConsumerClient.from_connection_string( conn_str=connection_str, consumer_group=consumer_group, eventhub_name=eventhub_name ) with client: client.receive(on_event=on_event, starting_position="-1") ``` ### JavaScript / Node.js Install the package: ```bash npm install @azure/event-hubs ``` Consume events: ```javascript const { EventHubConsumerClient } = require("@azure/event-hubs"); const connectionString = "Endpoint=sb://..."; const eventHubName = "telemetry-yourpartner"; const consumerGroup = "$Default"; async function main() { const client = new EventHubConsumerClient( consumerGroup, connectionString, eventHubName, ); const subscription = client.subscribe({ processEvents: async (events, context) => { for (const event of events) { const data = event.body; console.log( `Device: ${data.deviceId}, Capabilities: ${data.capabilities.length}`, ); } }, processError: async (err, context) => { console.error(`Error: ${err.message}`); }, }); } main(); ``` --- ## Event schema Every telemetry event is a JSON message with the following structure (schema version `1.0`): ```json { "schemaVersion": "1.0", "deviceId": "0f43fcf9-62cc-4bab-88b0-b0a4c12bf4b6", "clientId": "my-client-id", "timestamp": "2025-06-15T14:32:10Z", "capabilities": [ { "name": "display-grid-power", "data": { "gridPower": 1520, "lastUpdate": "2025-06-15T14:32:10Z" } }, { "name": "display-solar-power", "data": { "solarPower": 4200, "lastUpdate": "2025-06-15T14:32:10Z" } }, { "name": "core", "data": { "connectivityStatus": "online", "lastUpdate": "2025-06-15T14:32:10Z" } } ] } ``` ### Top-level fields | Field | Type | Description | | --------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------- | | `schemaVersion` | `string` | Schema version identifier. Currently `"1.0"`. | | `deviceId` | `string` (GUID) | The device ID as returned by the Connect API (same as `GET /devices`). | | `clientId` | `string` or `null` | The client identifier (OAuth2 `client_id`) that provisioned this device. `null` if the device has no client association. | | `timestamp` | `string` (ISO 8601 UTC) | Timestamp of the telemetry reading. | | `capabilities` | `array` | List of capability readings included in this event. | ### Capability object | Field | Type | Description | | ------ | -------- | -------------------------------------------------------------------------------- | | `name` | `string` | The capability name (e.g., `display-grid-power`). Same names as in the REST API. | | `data` | `object` | Capability-specific data. Structure varies per capability (see reference below). | ### Event Hub message properties Each message also includes these properties on the Event Hub message envelope (accessible via SDK without parsing the body): | Property | Value | | --------------- | ------------------------------------------------------------ | | `deviceId` | Same as `deviceId` in the body | | `schemaVersion` | Same as `schemaVersion` in the body | | `clientId` | Same as `clientId` in the body (only present when not `null`) | > **Tip:** Use the `clientId` message property for server-side Event Hub routing or filtering without deserializing the event body. --- ## Telemetry capabilities by device type Not every capability appears in every event. An event only contains capabilities for which new data is available. The following table shows which capabilities **can** appear per device type. ### Electric Meter | Capability | Fields | Description | | ------------------------- | ----------------------------------------------------------- | --------------------------------------------- | | `core` | `connectivityStatus`, `lastUpdate` | Device online status | | `display-grid-power` | `gridPower` (int, Watt), `lastUpdate` | Current grid power draw/feed-in | | `display-solar-power` | `solarPower` (int, Watt), `lastUpdate` | Current solar production | | `display-community-power` | `communityPower` (int, Watt), `lastUpdate` | Current community/shared power | | `display-soc` | `soc` (int, %), `lastUpdate` | Battery state of charge (if battery attached) | | `display-battery-state` | `state` (string), `chargingPower` (int, Watt), `lastUpdate` | Battery state. `state` is authoritative; treat `chargingPower` as magnitude — see [reference](#display-battery-state). | ### Switch | Capability | Fields | Description | | --------------- | ---------------------------------- | ------------------------- | | `core` | `connectivityStatus`, `lastUpdate` | Device online status | | `on-off` | `state` (`"on"` / `"off"`) | Current on/off state | | `display-power` | `power` (int, Watt), `lastUpdate` | Current power consumption | ### Wallbox | Capability | Fields | Description | | -------------------------- | ---------------------------------- | ------------------------------------------------------------------------------ | | `core` | `connectivityStatus`, `lastUpdate` | Device online status | | `display-charging-state` | `chargingState` (string) | Current charging state (e.g., `"charging"`, `"completed"`, `"noCarConnected"`) | | `on-off` | `state` (`"on"` / `"off"`) | Derived on/off state | | `display-power` | `power` (int, Watt), `lastUpdate` | Current charging power | | `display-charging-current` | `amperage` (int, Ampere) | Current charging amperage | | `display-charging-phases` | `phases` (int) | Number of active charging phases | ### Car (Vehicle) | Capability | Fields | Description | | -------------------------- | ---------------------------------- | -------------------------------- | | `core` | `connectivityStatus`, `lastUpdate` | Device online status | | `display-charging-state` | `chargingState` (string) | Current charging state | | `on-off` | `state` (`"on"` / `"off"`) | Derived on/off state | | `display-power` | `power` (int, Watt), `lastUpdate` | Current charging power | | `display-charging-current` | `amperage` (int, Ampere) | Current charging amperage | | `display-charging-phases` | `phases` (int) | Number of active charging phases | | `display-soc` | `soc` (int, %), `lastUpdate` | Battery state of charge | | `display-estimated-range` | `range` (int, km) | Estimated remaining range | --- ## Capability data reference All field names use **camelCase**. Enums are serialized as **strings**. Nullable fields may be `null` or absent — always apply null-tolerance when parsing. ### `core` ```json { "connectivityStatus": "online", "lastUpdate": "2025-06-15T14:32:10Z" } ``` - `connectivityStatus`: `"online"` — always `"online"` in telemetry events (events are only sent when the device reports data) - `lastUpdate`: ISO 8601 UTC timestamp ### `display-grid-power` ```json { "gridPower": 1520, "lastUpdate": "2025-06-15T14:32:10Z" } ``` - `gridPower`: integer, Watt. Positive = drawing from grid, negative = feeding into grid. ### `display-solar-power` ```json { "solarPower": 4200, "lastUpdate": "2025-06-15T14:32:10Z" } ``` - `solarPower`: integer, Watt. ### `display-community-power` ```json { "communityPower": 800, "lastUpdate": "2025-06-15T14:32:10Z" } ``` - `communityPower`: integer, Watt. ### `display-soc` ```json { "soc": 72, "lastUpdate": "2025-06-15T14:32:10Z" } ``` - `soc`: integer, percentage (0–100). ### `display-battery-state` ```json { "state": "charging", "chargingPower": 3500, "lastUpdate": "2025-06-15T14:32:10Z" } ``` - `state`: string enum — `"idle"`, `"charging"`, `"discharging"`, `"disabled"`. **This is the authoritative direction of the battery flow** and is the field you should consume to know what the battery is doing. - `chargingPower`: integer, Watt. **Treat as 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". Do not derive direction from the sign — always use `state`. It is therefore valid to receive an event with, for example, `state = "discharging"` together with `chargingPower > 0` (raw vendor sign) — `state` is correct, and the magnitude is `abs(chargingPower)`. Recommended client logic: ```pseudo direction = state // "idle" | "charging" | "discharging" | "disabled" magnitudeW = abs(chargingPower ?? 0) signedW = direction == "discharging" ? -magnitudeW : direction == "charging" ? magnitudeW : 0 ``` ### `display-power` ```json { "power": 10700, "lastUpdate": "2025-06-15T14:32:10Z" } ``` - `power`: integer, Watt. Current power consumption of the device. ### `display-charging-state` ```json { "chargingState": "charging" } ``` - `chargingState`: string enum. Values include `"noCarConnected"`, `"charging"`, `"waitingOnCar"`, `"completed"`, `"error"`, `"awaitingStart"`, `"waitingForStatus"`. 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 it maps from the vendor's `notReadyForCharging` / `disconnected` state). To check 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. ### `on-off` ```json { "state": "on" } ``` - `state`: `"on"` or `"off"`. ### `display-charging-current` ```json { "amperage": 16 } ``` - `amperage`: integer, Ampere. ### `display-charging-phases` ```json { "phases": 3 } ``` - `phases`: integer (1, 2, or 3). ### `display-estimated-range` ```json { "range": 236 } ``` - `range`: integer, kilometers. --- ## Consumer groups Azure Event Hubs supports **consumer groups**, which allow multiple independent readers to consume the same telemetry stream at their own pace. Each consumer group maintains its own read position (offset). Use cases: - Separate consumer groups for your analytics pipeline, alerting system, and dashboard backend - Development/testing consumer group alongside production ### Default consumer group Every Event Hub comes with a built-in `$Default` consumer group. This is sufficient if you only have a single consumer application. ### Managing consumer groups Consumer groups are managed through the [Partner Portal](https://portal.clever-pv.com/connect-api/telemetry): 1. Navigate to **Connect API** > **Telemetry** 2. In the **Consumer Groups** section, you can: - View all existing consumer groups - Create new consumer groups by entering a name - Delete consumer groups you no longer need > **Important:** You cannot delete the `$Default` consumer group. Deleting a consumer group while consumers are connected will disconnect them. When creating a consumer in your application, specify the consumer group name from the Partner Portal: ```csharp var consumerGroup = "analytics-pipeline"; await using var consumer = new EventHubConsumerClient(consumerGroup, connectionString, eventHubName); ``` --- ## Credential rotation If you need to rotate the Shared Access Key for your Event Hub (e.g., after a security incident or as part of regular key hygiene), you can do so from the Partner Portal: 1. Navigate to **Connect API** > **Telemetry** 2. Click **Rotate Credentials** 3. Copy the new connection string > **Important:** The previous key becomes **invalid immediately** after rotation. All consumers using the old connection string will be disconnected and must be updated with the new connection string. **Zero-downtime rotation strategy:** 1. Rotate the credentials in the Partner Portal 2. Deploy the new connection string to all consumer applications 3. Restart consumers to pick up the new credentials Since there is only one active key at a time, a brief reconnection window is unavoidable. Keep this window as short as possible by automating the credential deployment. --- ## Best practices - **Single stream per partner**: All devices from all your clients share one Event Hub. Use the `clientId` field to identify which of your clients a device belongs to, and the `deviceId` field to identify the specific device. This lets you route events to client-specific processing pipelines. - **At-least-once delivery**: Events may be delivered more than once (e.g., during Event Hub rebalancing). Design your consumers to be **idempotent** — use `deviceId` + `timestamp` as a deduplication key. - **Null tolerance**: Not every capability field is guaranteed to be present in every event. Always handle missing or `null` fields gracefully. - **Schema versioning**: Always check `schemaVersion` before parsing. The current version is `"1.0"`. Future updates will increment the version and may add new fields. Unknown fields should be ignored (forward compatibility). - **Data retention**: Events are retained in the Event Hub for **7 days**. If your consumer is offline for longer, older events will be lost. - **Checkpointing**: For production consumers, use checkpointing (e.g., Azure Blob Storage for .NET `EventProcessorClient`, or equivalent in other SDKs) to resume from the last processed position after restarts. - **Partition count**: The Event Hub is provisioned with **32 partitions**. You can run up to 32 parallel consumer instances within the same consumer group for maximum throughput. - **Event frequency**: Telemetry events are sent whenever device data changes. The frequency varies by device type and vendor — from every few seconds (e.g., Shelly switches) to every few minutes (e.g., vehicle telemetry). - **Correlating with the REST API**: The `deviceId` in telemetry events matches the `id` field from `GET /v1/users/{userId}/devices`. Use this to correlate telemetry data with device metadata (name, model, vendor, etc.) from the REST API. --- # Historical Energy Data *(source: `pages/historical-energy-data.mdx`)* ## TL;DR - `GET /energy-data/daily?date=2025-03-15` — 5-minute resolution with per-device consumption and activity windows - `GET /energy-data/period?date=2025-03-15&period=weekly` — aggregated by day/week/month/year - Energy in **Wh**, power readings in **W**, all money values (costs, savings, revenue) in **cents** — divide by 100 for display - Timestamps are **UTC** (`...Z`) — convert to the user's timezone in your frontend - The requested `date` is a calendar **day in the home's timezone** (fixed when the home is created, default `Europe/Berlin`) — see [Timezone behaviour](#timezone-behaviour) - No data for a date? Returns `200` with zeroes, not `404` The Historical Energy Data endpoints provide **aggregated energy metrics** for a user's home over past time periods. While [Live Telemetry](/live-telemetry) pushes real-time device state updates via Event Hubs, these REST endpoints let you retrieve processed historical data — including grid consumption, solar production, battery activity, device-level breakdowns, and cost calculations. --- ## Live Telemetry vs. Historical Energy Data | | Live Telemetry (Event Hub) | Historical Energy Data (REST) | | ------------------ | ---------------------------------- | ------------------------------------------------------- | | **Delivery model** | Push — events stream as they occur | Pull — you request data for a specific date | | **Resolution** | Per-change (seconds) | 5-minute intervals (daily) or day/week/month aggregates | | **Data scope** | Current device capabilities | Energy totals, costs, savings, device activity windows | | **Use case** | Dashboards, monitoring, alerting | Reports, analytics, billing, energy insights | Use both together: Live Telemetry for real-time monitoring and Historical Energy Data for retrospective analysis and reporting. --- ## Endpoints overview | Method | Endpoint | Description | | ------ | --------------------------------------- | ------------------------------------------------------ | | `GET` | `/v1/users/{userId}/energy-data/daily` | Daily data with 5-minute resolution intervals | | `GET` | `/v1/users/{userId}/energy-data/period` | Aggregated data for weekly, monthly, or yearly periods | Both endpoints return energy totals in **watt-hours (Wh)**, monetary **cost and savings figures in cents** (minor unit of the user's tariff currency), and per-device breakdowns. --- ## Authentication These endpoints use the same authentication as all Connect API endpoints: - **Scheme**: Bearer token (JWT) - **Scope**: `connect_api` - **User context**: The `{userId}` path parameter is your external user ID (the same ID you use when provisioning users) See [Authentication](/api-authentication) for details on obtaining a token. --- ## Daily energy data ``` GET /v1/users/{userId}/energy-data/daily?date={date} ``` Returns energy data for a single day with **5-minute resolution** intervals, per-device consumption breakdowns, and device activity windows. ### Request parameters | Parameter | Location | Type | Required | Description | | --------- | -------- | -------- | -------- | --------------------------- | | `userId` | path | `string` | yes | External user ID | | `date` | query | `string` | yes | Date in `yyyy-MM-dd` format | ### Response ```json { "date": "2025-03-15", "summary": { "gridConsumptionWh": 8450, "gridFeedInWh": 12300, "productionWh": 18750, "batteryChargedWh": 5200, "batteryDischargedWh": 3100, "costs": { "gridConsumptionCosts": 253, "gridFeedInRevenue": 98, "totalSavings": 412, "pvSavings": 345 } }, "intervals": [ { "timestamp": "2025-03-15T06:00:00Z", "gridConsumptionWatt": 520, "productionWatt": 0, "batteryStateOfChargePercent": 45, "batteryChargingWatt": null, "batteryDischargingWatt": 200, "devices": [ { "deviceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Wallbox", "category": "Wallbox", "consumptionWh": 150 } ] } ], "devices": [ { "deviceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Wallbox", "category": "Wallbox", "totalConsumptionWh": 11200, "costs": 336, "savings": 180, "activities": [ { "startedAt": "2025-03-15T08:30:00Z", "endedAt": "2025-03-15T11:45:00Z", "consumptionWh": 9500, "durationMinutes": 195, "costs": 285 } ] } ] } ``` ### Response fields | Field | Type | Description | | ----------- | -------- | ---------------------------------------------------------- | | `date` | `string` | The requested date (`yyyy-MM-dd`) | | `summary` | `object` | Aggregated energy totals and costs for the day | | `intervals` | `array` | 5-minute resolution data points with power readings (Watt) | | `devices` | `array` | Per-device energy summary with activity windows | --- ## Period energy data ``` GET /v1/users/{userId}/energy-data/period?date={date}&period={period} ``` Returns aggregated energy data for a longer time period (weekly, monthly, or yearly), broken down into sub-intervals (e.g., individual days within a week). ### Request parameters | Parameter | Location | Type | Required | Description | | --------- | -------- | -------- | -------- | ------------------------------------------------- | | `userId` | path | `string` | yes | External user ID | | `date` | query | `string` | yes | Anchor date in `yyyy-MM-dd` format | | `period` | query | `string` | yes | Aggregation period: `weekly`, `monthly`, `yearly` | ### Response ```json { "date": "2025-03-15", "period": "weekly", "summary": { "gridConsumptionWh": 58200, "gridFeedInWh": 85400, "productionWh": 132000, "batteryChargedWh": 36500, "batteryDischargedWh": 28700, "costs": { "gridConsumptionCosts": 1746, "gridFeedInRevenue": 683, "totalSavings": 2850, "pvSavings": 2210 } }, "consolidatedIntervals": [ { "date": "2025-03-10", "gridConsumptionWh": 8200, "gridFeedInWh": 12100, "productionWh": 18800, "batteryChargedWh": 5100, "batteryDischargedWh": 4050, "costs": { "gridConsumptionCosts": 246, "gridFeedInRevenue": 97, "totalSavings": 405, "pvSavings": 315 } } ], "devices": [ { "deviceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Wallbox", "category": "Wallbox", "totalConsumptionWh": 35000, "costs": 1050, "savings": 560, "activities": null } ] } ``` ### Response fields | Field | Type | Description | | ----------------------- | -------- | ---------------------------------------------------------------- | | `date` | `string` | The anchor date (`yyyy-MM-dd`) | | `period` | `string` | The requested period (`weekly`, `monthly`, `yearly`) | | `summary` | `object` | Aggregated energy totals and costs across the entire period | | `consolidatedIntervals` | `array` | Sub-period aggregates (e.g., one entry per day in a weekly view) | | `devices` | `array` | Per-device totals for the period (no activity windows) | --- ## Timezone behaviour Two distinct timezone concepts apply to these endpoints — keep them apart: - **Returned timestamps are always UTC.** Every `timestamp`, `startedAt`, and `endedAt` value is the true UTC instant of the underlying measurement, serialised as ISO 8601 with a trailing `Z` (e.g. `2025-03-15T06:00:00Z`). Convert to the user's local timezone in your own application. - **The requested `date` is a day in the home's timezone.** The `date` parameter (and the `period` anchor date) selects a calendar day from midnight to midnight in the home's timezone, which is why a day's intervals do not start at `00:00Z`. The home's timezone is fixed when the home is created (default `Europe/Berlin`) — see [Home timezone](/users-and-homes#home-timezone) for how it is set. --- ## Response field reference ### Summary Energy totals for the requested period. All energy values are in **watt-hours (Wh)**. | Field | Type | Description | | --------------------- | ---------- | ------------------------------------------------ | | `gridConsumptionWh` | `decimal` | Total energy drawn from the grid | | `gridFeedInWh` | `decimal` | Total energy fed into the grid | | `productionWh` | `decimal?` | Total solar production (null if no solar system) | | `batteryChargedWh` | `decimal` | Total energy charged into the battery | | `batteryDischargedWh` | `decimal` | Total energy discharged from the battery | | `costs` | `object` | Cost and savings breakdown (see [Costs](#costs)) | ### Costs All amounts in this object are **in cents** (minor currency unit of the user's configured tariff, e.g. euro cents). Values are `decimal` so fractional cents can appear where the backend uses them. | Field | Type | Description | | ---------------------- | --------- | -------------------------------------------------------- | | `gridConsumptionCosts` | `decimal` | Cost of grid-drawn energy, in **cents** | | `gridFeedInRevenue` | `decimal` | Revenue from feed-in, in **cents** | | `totalSavings` | `decimal` | Total savings (self-consumption + feed-in), in **cents** | | `pvSavings` | `decimal` | PV self-consumption savings, in **cents** | ### Daily interval (5-minute resolution) Each interval represents a 5-minute window. Power values are in **Watt** (instantaneous readings). | Field | Type | Description | | ----------------------------- | -------- | ---------------------------------------- | | `timestamp` | `string` | ISO 8601 UTC timestamp | | `gridConsumptionWatt` | `int` | Grid power draw in Watt | | `productionWatt` | `int?` | Solar production in Watt | | `batteryStateOfChargePercent` | `int?` | Battery state of charge (0–100%) | | `batteryChargingWatt` | `int?` | Battery charging power in Watt | | `batteryDischargingWatt` | `int?` | Battery discharging power in Watt | | `devices` | `array` | Per-device consumption for this interval | #### Device interval | Field | Type | Description | | --------------- | --------- | ------------------------------------------------------- | | `deviceId` | `string` | Device ID (GUID) | | `name` | `string` | Device display name | | `category` | `string` | Device category (e.g., `Wallbox`, `HeatPump`, `Switch`) | | `consumptionWh` | `decimal` | Device consumption in Wh for this interval | ### Consolidated interval (period aggregates) Each interval represents one sub-period (e.g., a single day within a weekly view). All energy values are in **watt-hours (Wh)**. | Field | Type | Description | | --------------------- | ---------- | ------------------------------------------------------------------------------ | | `date` | `string` | Date of this sub-period (`yyyy-MM-dd`) | | `gridConsumptionWh` | `decimal` | Grid consumption in Wh | | `gridFeedInWh` | `decimal` | Grid feed-in in Wh | | `productionWh` | `decimal?` | Solar production in Wh | | `batteryChargedWh` | `decimal` | Battery charged in Wh | | `batteryDischargedWh` | `decimal` | Battery discharged in Wh | | `costs` | `object` | Cost breakdown for this sub-period (amounts in **cents**, see [Costs](#costs)) | ### Device Per-device energy summary for the requested period. | Field | Type | Description | | -------------------- | ---------- | --------------------------------------------------------- | | `deviceId` | `string` | Device ID (GUID) | | `name` | `string` | Device display name | | `category` | `string` | Device category (e.g., `Wallbox`, `HeatPump`, `Switch`) | | `totalConsumptionWh` | `decimal` | Total energy consumed by this device | | `costs` | `decimal?` | Cost attributed to this device, in **cents** | | `savings` | `decimal?` | Savings attributed to this device, in **cents** | | `activities` | `array?` | Activity windows (daily endpoint only, `null` for period) | ### Device activity (daily only) Activity windows show when a device was actively consuming energy during the day. | Field | Type | Description | | ----------------- | ---------- | ------------------------------------------------------ | | `startedAt` | `string` | Activity start time (ISO 8601 UTC) | | `endedAt` | `string` | Activity end time (ISO 8601 UTC) | | `consumptionWh` | `decimal` | Total consumption during this activity | | `durationMinutes` | `int` | Duration in minutes | | `costs` | `decimal?` | Calculated cost for this activity window, in **cents** | --- ## Code examples ### C# (.NET) ```csharp using System.Net.Http.Headers; using System.Text.Json; var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "YOUR_ACCESS_TOKEN"); var baseUrl = "https://connect.clever-pv.com"; var userId = "your-external-user-id"; // Daily energy data var dailyResponse = await client.GetAsync( $"{baseUrl}/v1/users/{userId}/energy-data/daily?date=2025-03-15"); var dailyJson = await dailyResponse.Content.ReadAsStringAsync(); Console.WriteLine(dailyJson); // Weekly energy data var weeklyResponse = await client.GetAsync( $"{baseUrl}/v1/users/{userId}/energy-data/period?date=2025-03-15&period=weekly"); var weeklyJson = await weeklyResponse.Content.ReadAsStringAsync(); Console.WriteLine(weeklyJson); ``` ### Python ```python import requests base_url = "https://connect.clever-pv.com" user_id = "your-external-user-id" headers = {"Authorization": "Bearer YOUR_ACCESS_TOKEN"} # Daily energy data daily = requests.get( f"{base_url}/v1/users/{user_id}/energy-data/daily", params={"date": "2025-03-15"}, headers=headers, ) print(daily.json()) # Monthly energy data monthly = requests.get( f"{base_url}/v1/users/{user_id}/energy-data/period", params={"date": "2025-03-15", "period": "monthly"}, headers=headers, ) print(monthly.json()) ``` ### JavaScript / Node.js ```javascript const baseUrl = "https://connect.clever-pv.com"; const userId = "your-external-user-id"; const headers = { Authorization: "Bearer YOUR_ACCESS_TOKEN" }; // Daily energy data const dailyRes = await fetch( `${baseUrl}/v1/users/${userId}/energy-data/daily?date=2025-03-15`, { headers }, ); const daily = await dailyRes.json(); console.log("Summary:", daily.summary); console.log("Intervals:", daily.intervals.length); // Yearly energy data const yearlyRes = await fetch( `${baseUrl}/v1/users/${userId}/energy-data/period?date=2025-03-15&period=yearly`, { headers }, ); const yearly = await yearlyRes.json(); console.log("Yearly summary:", yearly.summary); ``` --- ## Important notes - **Units**: Summary and interval energy values are in **watt-hours (Wh)**. Daily interval power readings (`gridConsumptionWatt`, `productionWatt`, etc.) are in **Watt** (instantaneous power, not energy). All **cost and savings** fields (`summary.costs`, `consolidatedIntervals[].costs`, device `costs` / `savings`, activity `costs`) are **in cents** (minor unit of the tariff currency). - **Displaying money**: Divide cent values by 100 to show major units (e.g. euros) in your UI. - **Timestamps are UTC**: All `timestamp`, `startedAt`, and `endedAt` fields are the true UTC instant in ISO 8601 with a trailing `Z`. Convert to the user's local timezone in your application as needed. The requested `date`, however, is sliced by the **home's timezone** (not UTC) — so a day's intervals do not start at `00:00Z`. See [Timezone behaviour](#timezone-behaviour). - **Nullable fields**: Fields like `productionWh`, `batteryStateOfChargePercent`, and `costs` may be `null` when no solar system or battery is connected, or when cost data is unavailable. Always handle `null` values gracefully. - **Empty responses**: If no data is available for the requested date, the endpoint returns `200 OK` with zero values in the summary and empty arrays for intervals and devices — not `404`. - **Device activities**: Activity windows are only included in the **daily** endpoint response. The **period** endpoint returns `null` for `activities` on each device. - **Producer devices excluded**: Only consumer devices (wallboxes, heat pumps, switches, etc.) appear in the `devices` array. Producer devices like solar inverters are not listed as separate devices — their production is reflected in the `productionWh` summary field. - **Period behavior**: The `date` parameter serves as an anchor date. For `weekly`, data covers the week containing that date. For `monthly`, the full month. For `yearly`, the full year. - **Correlating with Live Telemetry**: The `deviceId` values match between Historical Energy Data and [Live Telemetry](/live-telemetry) events. Use Historical Energy Data for daily/weekly/monthly reports and Live Telemetry for real-time dashboards. --- # Interventions *(source: `pages/interventions.mdx`)* ## TL;DR - `interventions[]` on a capability = it's **blocked**. The capability exists but can't be used until the user takes action - Most common: `authentication-required` → resolve with `PUT /devices/{id}/connect` (re-auth flow) - `subscription-upgrade-required` → user needs a plan upgrade, you can't resolve this via API - Always iterate `interventions[]` and drive your UI from the `id` — this table shows every possible value and how to fix it Quick reference of intervention codes you may encounter and how to resolve them. ## Vehicles ### General (any car you connect) | Intervention | When you see it | Resolution | | --- | --- | --- | | `home-location-required` | `home-location` capability is not set. | Set it via `PUT /v1/users/{userId}/devices/{deviceId}/home-location`. **Send real `latitude`/`longitude`** — they drive at-home detection and gate solar-aware charging. Omitting them defaults to `0,0`, which clears this intervention but leaves the vehicle permanently "not at home". Address fields (`city`/`postalCode`/`street`) are informational only. Details: [`home-location` capability](./device-capabilities#cap-home-location). | | `connector-type-required` | 'connector-type" capability is not set. | Set it via `PUT /v1/users/{userId}/devices/{deviceId}/connector-type`. | | `location-not-available` | Vehicle location data is not available (e.g., vendor does not provide location). Shown on the `location-detection` capability. | Informational only — no action required. This intervention does not block device control. Note: without location data the system cannot determine whether the vehicle is at its home location, so the vehicle may be controlled even when away from home (e.g., at a public charging station). | ### VW CCO | Intervention | When you see it | Resolution | | --- | --- | --- | | `add-charging-location-required` | Charging location not set. | Set it via `PUT /v1/users/{userId}/devices/{deviceId}/add-charging-location` | ### Tesla Official | Intervention | When you see it | Resolution | | --- | --- | --- | | `certificate-required` | Certificate is not installed. | Resolve via `PUT /v1/users/{userId}/devices/{deviceId}/install-certificate`. | | `authentication-required` | OAuth/auth fails or needs re-authentication. | Resolve via `PUT /v1/users/{userId}/devices/{deviceId}/connect`. | | `missing-scopes-required` | Required scopes are missing. | Resolve via `PUT /v1/users/{userId}/devices/{deviceId}/set-scopes`. | ## Heat Pumps ### Bosch/Buderus | Intervention | When you see it | Resolution | | --- | --- | --- | | `approval-required` | Contract status is not active during onboarding. | Resolves automatically, once the contract is active; the intervention is removed automatically. | ### Offline >= 5 days | Intervention | When you see it | Resolution | | --- | --- | --- | | `authentication-required` | Device is offline >= 5 days. | Resolve by re-authenticating (`PUT /v1/users/{userId}/devices/{deviceId}/connect`). | ## Electric Meters ### Shelly (legacy) | Intervention | When you see it | Resolution | | --- | --- | --- | | `measurement-purpose-required` | Measurement purpose is not confirmed.[component: br][component: br]Setup stays incomplete on creation until you confirm it. | Resolve via `PUT /v1/users/{userId}/devices/{deviceId}/measurement-purpose` with `purpose` set to `gridMeter` or `pvProduction`. | ### Battery operating mode | Intervention | When you see it | Resolution | | --- | --- | --- | | `smart-price-control-enabled` | Shown on the `operation-mode` capability when the home's battery Smart Price Control automation is active. Manual mode changes are blocked while Smart Price Control is running. | Disable battery Smart Price Control for the home (via automation settings / vendor app), then retry the PUT. | | `control-permission-required` | Shown on the `operation-mode` capability when clever-PV is not (yet) permitted to control the battery — the [`control-permission`](./device-capabilities#cap-control-permission) state is `forbidden` or `pending`. PUTs to `operation-mode` fail with `400` while this intervention is present. | Resolve via `PUT /v1/users/{userId}/devices/{deviceId}/control-permission` with `{ "request": "allow" }`. SigEnergy: takes effect immediately. Fronius: the user receives a verification e-mail and must confirm it — the state stays `pending` (and the intervention remains) until confirmed. | ## Target Charging (Cars & Wallboxes) | Intervention | When you see it | Resolution | | --- | --- | --- | | `target-charging-setup-required` | Shown on the [`target-charging`](./device-capabilities#cap-target-charging) capability when the EV's **battery capacity** or **consumption** is not configured (`null` or `<= 0`). Both values are required before target charging can be enabled. | Set both values: `PUT /v1/users/{userId}/devices/{deviceId}/battery-capacity` with `{ "batteryCapacity": }` and `PUT /v1/users/{userId}/devices/{deviceId}/consumption` with `{ "consumption": }`. The intervention clears automatically on the next read once both values are positive. | ### How `target-charging-setup-required` works **Why it exists.** Target charging plans how much energy must flow into the vehicle by a given time. To translate the configured target (`targetWh`, `targetSoC`) into a concrete charge plan, the system needs the EV's battery capacity (Wh) and its energy consumption. Without both values a plan cannot be computed, so the capability is blocked. **Where the values live.** Both values are stored **per device**: for a car on its charging settings, for a wallbox on the wallbox itself. They are set via the [`battery-capacity`](./device-capabilities#cap-battery-capacity) and [`consumption`](./device-capabilities#cap-consumption) capabilities. A value counts as "set" only if it is greater than `0`. **What is blocked — and what is not.** The intervention only blocks **enabling** target charging, i.e. a `PUT /v1/users/{userId}/devices/{deviceId}/target-charging` with `"enabled": true` while it is currently disabled. That request fails with `422` (`ConnectApiProblemDetails`, `errorCode: "interventionFound"`; the `detail` text names the intervention id). Everything else still works while the intervention is present: - `GET` still returns the capability with the full plan data. - Disabling (`"enabled": false`) is never blocked. - Updating plan parameters (`targetWh`, `executionTime`, `weekDays`, …) without flipping `enabled` from `false` to `true` is not blocked either. If the home additionally lacks the required subscription, `subscription-upgrade-required` takes precedence and the enable attempt fails with `402` instead. **Lifecycle.** The intervention is computed at read time on every request — it is never persisted. There is no separate "resolve" call: once both values are positive, the next `GET` no longer includes it and enabling succeeds. **Diagnosing which value is missing.** The `GET` response of `target-charging` includes the current `batteryCapacity` and `consumption` as nullable fields in `data`, so you can tell which of the two still needs to be set. ## General Onboarding | Intervention | When you see it | Resolution | | --- | --- | --- | | `authentication-required` | Authentication issues with the device. | Resolve by re-authenticating (`PUT /v1/users/{userId}/devices/{deviceId}/connect`). | | `email-verification-required` | The `email-verification` capability is present | Resolve via `PUT /v1/users/{userId}/devices/{deviceId}/email-verification`. | ## Subscription / Automation ### Smart mode | Intervention | When you see it | Resolution | | --- | --- | --- | | `subscription-upgrade-required` | Your plan is free or expired. On a starter plan, only if another device already has smart mode enabled. | Not resolvable via the Connect API. Direct the user to upgrade their clever-PV plan in the customer portal; the intervention clears automatically once the new plan is active. | ### Scheduling | Intervention | When you see it | Resolution | | --- | --- | --- | | `subscription-upgrade-required` | Your plan does not include the Schedule feature. | Not resolvable via the Connect API. Direct the user to upgrade to a plan that includes scheduling; the intervention clears automatically once the new plan is active. | ### Battery automation | Intervention | When you see it | Resolution | | --- | --- | --- | | `solar-forecast-setup-required` | Solar forecast is not enabled for the home. | Resolve by enabling Solar Forecast for the home. | | `battery-capacity-required` | Battery capacity is missing on a device with an attached battery (the intervention is reported on the **electric meter** the battery is attached to). | Resolve by writing the [`battery-capacity`](/device-capabilities#cap-battery-capacity) capability on that device: `PUT /v1/users/{userId}/devices/{deviceId}/battery-capacity` with `{"batteryCapacity": }` (nominal capacity in Wh). | | `battery-software-update-required` | Vendor reports outdated battery software. | Resolve by updating the battery firmware in the vendor app/portal. | | `wrong-operating-mode` | Battery is in a mode that blocks smart control. | Resolve by switching the battery to an allowed/automatic operating mode in the vendor app. | | `email-verification-required` | Vendor email consent is missing (e.g., Fronius registration not verified). | Resolve by verifying the email/consent in the vendor portal; it clears after verification. | --- # Surplus Charging Control *(source: `pages/surplus-charging-control.mdx`)* ## TL;DR - Surplus/excess charging decisions are driven by one device per home — the **main device** - The main device is **selected automatically**, preferring whichever measurement source delivers the freshest values - Control is **event-driven**: every main-device measurement triggers a decision pass - Battery state is factored in across **all connected batteries** in the home, not only the one reported by the main device Surplus charging control turns the home's PV surplus into real-world action — adjusting controlled loads so they consume excess solar power instead of feeding it into the grid. To do that, the control loop needs a single, authoritative view of the home's current energy balance. That view comes from the **main device**. ## How surplus control works One device per home acts as the **measurement source** for all surplus-control decisions — this device is referred to as the **main device**. Its readings (current grid power, solar production, and related values) are what the control loop evaluates on each cycle. Control is **event-driven**: every time the main device reports a new measurement, the system runs a decision pass that: - Compares current PV surplus against the targets of the home's controllable devices - Turns controlled loads on or off, or adjusts their power, based on the surplus available There is no background polling loop independent of the main device's readings — the control loop advances with each incoming main-device measurement. Alongside the main device's power measurements, the decision pass also factors in the state of **all connected batteries** in the home — not only the battery reported by the main device. State of charge, charging/discharging power, and available capacity are combined across batteries (capacity-weighted state of charge, summed power) so the control loop treats the home's total battery state as a single aggregated view. ## Main device selection The main device is chosen **automatically**. Among the devices that produce power measurements for the home, the system prefers the one that delivers the **freshest values** — that is, the shortest update interval between measurements. This keeps the control loop responsive to real-world conditions. Selection is re-evaluated whenever the set of measurement-producing devices changes — for example, when a device is added to the home, removed, or changes state. Any device that reports current power values for the home can qualify as the main device, regardless of its specific role as a grid-side meter, a production-side sensor, or an integrated meter reported by another device. ## Identifying the main device from the API The main device is also surfaced through the device list: the home's main electric meter is returned as the **first entry** in `GET /v1/users/{userId}/devices`. The entry's `model.category` is `electricMeter`, so clients can highlight the main meter in their UI directly from the list response without issuing a separate query. If no electric meter has been picked as main yet — for example, immediately after onboarding before backend selection has run — the list still uses a **deterministic fallback order**: electric meters are sorted by creation time, then by ID. The first eMeter you see is therefore stable across calls. If the home has no electric meters at all, the response simply contains no eMeter entry and this ordering rule does not apply. See [Main device selection](#main-device-selection) for how the backend picks which meter is main. --- # Electricity Tariffs *(source: `pages/electricity-tariffs.mdx`)* ## TL;DR - `GET /electricity-tariffs` — list all tariffs for a user - `POST /electricity-tariffs` — create a tariff; `PUT .../{id}` — update; `DELETE .../{id}` — remove - Three combinations: **buy + fix** (fixed ct/kWh), **buy + dynamic** (stock price + grid fees), **sell** (feed-in) - For fix and sell tariffs, `variablePriceComponents` are **auto-generated** from `price` — sending them in the request has no effect - dynamic tariffs support time-of-use grid fees via `variablePriceComponents` (must cover all 7 days × 96 quarter-hours) Electricity tariffs define the pricing terms under which a user buys energy from and sells energy back to the grid. The API supports fixed-price purchase tariffs, dynamic/spot-price purchase tariffs linked to an energy exchange, and fixed-price feed-in (sell) tariffs. Tariff data drives the cost and savings calculations in [Historical Energy Data](/historical-energy-data). --- ## Endpoints overview | Method | Endpoint | Description | | -------- | ------------------------------------------------- | ------------------------------------ | | `GET` | `/v1/users/{userId}/electricity-tariffs` | List all tariffs for the user | | `POST` | `/v1/users/{userId}/electricity-tariffs` | Create a new tariff | | `PUT` | `/v1/users/{userId}/electricity-tariffs/{id}` | Update an existing tariff | | `DELETE` | `/v1/users/{userId}/electricity-tariffs/{id}` | Delete a tariff | --- ## Authentication These endpoints use the same authentication as all Connect API endpoints: - **Scheme**: Bearer token (JWT) - **Scope**: `connect_api` - **User context**: The `{userId}` path parameter is your external user ID (the same ID you use when provisioning users) See [Authentication](/api-authentication) for details on obtaining a token. --- ## Enums | Enum | Values | | ----------- | -------------------------- | | `priceType` | `fix`, `dynamic` | | `type` | `buy`, `sell` | --- ## Tariff types There are three valid combinations of `type` and `priceType`. Each combination determines which fields are required and which are ignored. ### buy + fix (fixed-price purchase tariff) The most common tariff. The user pays a fixed ct/kWh price. | Field | Required | Notes | | -------------------------- | -------- | ------------------------------------------------------ | | `from` | Yes | Start date (time is ignored, truncated to start of day in user's timezone) | | `to` | No | End date (time is ignored, truncated to start of day in user's timezone). `null` = open-ended | | `price` | Yes | Price in ct/kWh | | `baseFee` | No | Monthly base fee in EUR | | `priceType` | Yes | Must be `fix` | | `type` | Yes | Must be `buy` | | `bettingZone` | No | Ignored for fix tariffs | | `gridFee` | No | **Ignored** — `price` is used instead | | `variablePriceComponents` | No | **Ignored** — auto-generated from `price` | **Backend behavior:** `variablePriceComponents` and `gridFee` are ignored. The system creates a static gridFee component covering all days/hours with the `price` value. ### buy + dynamic (dynamic/spot-price purchase tariff) Tariff linked to energy stock exchange prices. The total cost = stock price + grid fees. | Field | Required | Notes | | -------------------------- | -------- | ----------------------------------------------------------------------------------------- | | `from` | Yes | Start date (time is ignored, truncated to start of day in user's timezone) | | `to` | No | End date (time is ignored, truncated to start of day in user's timezone). `null` = open-ended | | `price` | No | Not used for dynamic tariffs | | `baseFee` | No | Monthly base fee in EUR | | `priceType` | Yes | Must be `dynamic` | | `type` | Yes | Must be `buy` | | `bettingZone` | Yes | Energy market bidding zone (e.g. `DE-LU`) | | `gridFee` | No | Simple static grid fee in ct/kWh (legacy). Use `variablePriceComponents` for time-of-use | | `variablePriceComponents` | No | Grid fee schedule with time-of-use pricing. Takes precedence over `gridFee` | **Backend behavior:** If `variablePriceComponents` is provided, they are used directly (validated for full coverage). Otherwise, if `gridFee` is provided, a static component is created from it. If neither is provided, no grid fee is applied. ### sell (feed-in tariff) What the user earns for feeding solar energy back to the grid. Always uses a fixed price. | Field | Required | Notes | | -------------------------- | -------- | ------------------------------------------------------ | | `from` | Yes | Start date (time is ignored, truncated to start of day in user's timezone) | | `to` | No | End date (time is ignored, truncated to start of day in user's timezone). `null` = open-ended | | `price` | Yes | Feed-in price in ct/kWh | | `baseFee` | No | **Ignored** for sell tariffs | | `priceType` | Yes | Must be `fix` | | `type` | Yes | Must be `sell` | | `bettingZone` | No | Ignored | | `gridFee` | No | **Ignored** | | `variablePriceComponents` | No | **Ignored** — auto-generated from `price` | **Backend behavior:** Same as buy + fix. `variablePriceComponents` and `gridFee` are ignored. A static component is created from `price`. --- ## Summary matrix | | buy + fix | buy + dynamic | sell | | -------------------------------------- | -------------- | ----------------------- | -------------- | | `price` | **Required** | Ignored | **Required** | | `baseFee` | Optional | Optional | Ignored | | `bettingZone` | Ignored | **Required** | Ignored | | `gridFee` | Ignored | Optional (fallback) | Ignored | | `variablePriceComponents` | Ignored | Optional (priority) | Ignored | | Auto-generated `variablePriceComponents` | Yes (from price) | Only if neither provided | Yes (from price) | --- ## List tariffs ``` GET /v1/users/{userId}/electricity-tariffs ``` Returns all tariffs for the user. ### Response: `200 OK` ```json [ { "id": "a1b2c3d4-0000-0000-0000-000000000001", "from": "2024-01-01T00:00:00Z", "to": null, "price": 28.5, "baseFee": 9.90, "gridFee": 28.5, "bettingZone": null, "priceType": "fix", "type": "buy", "variablePriceComponents": [ { "type": "gridFee", "rules": [ { "daysOfWeek": ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"], "timeWindows": [ { "start": "00:00", "end": "24:00", "priceCtPerKwh": 28.5 } ] } ] } ] } ] ``` --- ## Create tariff ``` POST /v1/users/{userId}/electricity-tariffs ``` Creates a new tariff. Returns the full updated list of tariffs. ### Response: `201 Created` ### Example: buy + fix ```json { "from": "2024-01-01T00:00:00Z", "to": null, "price": 28.5, "baseFee": 9.90, "priceType": "fix", "type": "buy" } ``` The response will contain `variablePriceComponents` auto-generated from `price`: ```json { "id": "...", "from": "2024-01-01T00:00:00Z", "to": null, "price": 28.5, "baseFee": 9.90, "gridFee": 28.5, "priceType": "fix", "type": "buy", "variablePriceComponents": [ { "type": "gridFee", "rules": [ { "daysOfWeek": ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"], "timeWindows": [ { "start": "00:00", "end": "24:00", "priceCtPerKwh": 28.5 } ] } ] } ] } ``` > Sending `gridFee` or `variablePriceComponents` in the request has no effect for fix tariffs. They are always derived from `price`. ### Example: buy + dynamic (simple grid fee) ```json { "from": "2024-06-01T00:00:00Z", "to": null, "baseFee": 12.50, "gridFee": 16.0, "bettingZone": "DE-LU", "priceType": "dynamic", "type": "buy" } ``` Response: `gridFee` is converted to a static `variablePriceComponents` entry covering all days/hours at 16.0 ct/kWh. ### Example: buy + dynamic (time-of-use grid fee) Different grid fees for day/night, weekdays/weekends. Rules must cover all 7 days × 24 hours without gaps or overlaps. Times must use 15-minute intervals (00, 15, 30, 45). ```json { "from": "2024-06-01T00:00:00Z", "to": null, "baseFee": 12.50, "bettingZone": "DE-LU", "priceType": "dynamic", "type": "buy", "variablePriceComponents": [ { "type": "gridFee", "rules": [ { "daysOfWeek": ["monday", "tuesday", "wednesday", "thursday", "friday"], "timeWindows": [ { "start": "00:00", "end": "06:00", "priceCtPerKwh": 12.0 }, { "start": "06:00", "end": "22:00", "priceCtPerKwh": 20.0 }, { "start": "22:00", "end": "24:00", "priceCtPerKwh": 12.0 } ] }, { "daysOfWeek": ["saturday", "sunday"], "timeWindows": [ { "start": "00:00", "end": "24:00", "priceCtPerKwh": 14.0 } ] } ] } ] } ``` **Validation rules for `variablePriceComponents`:** - Times must use 15-minute intervals (`06:00`, `06:15`, `06:30`, `06:45` — not `06:10`) - Time windows must not overlap within the same rule - Time windows must have a minimum duration of 15 minutes (start ≠ end) - All 7 days and all 96 quarter-hours per day must be covered (no gaps) - Use `"24:00"` to represent end of day (not `"00:00"` of the next day) > When `variablePriceComponents` is provided, `gridFee` is ignored. ### Example: sell (feed-in tariff) ```json { "from": "2024-01-01T00:00:00Z", "to": "2044-12-31T00:00:00Z", "price": 8.2, "priceType": "fix", "type": "sell" } ``` Response: `variablePriceComponents` is auto-generated from `price` (8.2 ct/kWh static). `baseFee`, `gridFee`, and `variablePriceComponents` from the request are all ignored. --- ## Update tariff ``` PUT /v1/users/{userId}/electricity-tariffs/{id} ``` Updates an existing tariff. Same request body format as POST. Returns the full updated list of tariffs. ### Constraints - `type` (buy/sell) **cannot be changed** - `priceType` (fix/dynamic) **cannot be changed** - Tariffs cannot be updated while initial savings are still being calculated ### Response: `200 OK` ### Example: Update a buy + fix tariff price ```json { "from": "2024-01-01T00:00:00Z", "to": "2025-06-30T00:00:00Z", "price": 32.0, "baseFee": 10.50, "priceType": "fix", "type": "buy" } ``` --- ## Delete tariff ``` DELETE /v1/users/{userId}/electricity-tariffs/{id} ``` Deletes a tariff. ### Response: `204 No Content` --- ## Field reference | Field | Type | Description | | -------------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------- | | `id` | `string` | Tariff ID (GUID), assigned by the server | | `from` | `DateTime` | Tariff start date. Time portion is ignored — truncated to start of day in the user's timezone | | `to` | `DateTime?` | Tariff end date. Time portion is ignored — truncated to start of day in the user's timezone. `null` = no end date | | `price` | `decimal?` | Price in ct/kWh. Required for fix and sell tariffs | | `baseFee` | `decimal?` | Monthly base fee in EUR. Only applied for buy tariffs | | `gridFee` | `decimal?` | Static grid fee in ct/kWh. Only used for dynamic buy tariffs as a simple alternative to `variablePriceComponents` | | `bettingZone` | `string?` | Energy market bidding zone (e.g. `DE-LU`). Required for dynamic tariffs | | `priceType` | `TariffPriceType` | `fix` or `dynamic` | | `type` | `TariffType` | `buy` or `sell` | | `variablePriceComponents` | `VariablePriceComponent[]?` | Time-of-use grid fee schedule. Only meaningful for dynamic buy tariffs | ### VariablePriceComponent | Field | Type | Description | | ------- | -------- | --------------------------------------------- | | `type` | `string` | Component type (currently always `"gridFee"`) | | `rules` | `array` | List of day-of-week / time-window pricing rules | ### Rule | Field | Type | Description | | ------------- | ---------- | --------------------------------------------------------------------------- | | `daysOfWeek` | `string[]` | Days this rule applies to (e.g. `["monday", "tuesday"]`) | | `timeWindows` | `array` | Time windows with pricing for the specified days | ### TimeWindow | Field | Type | Description | | ---------------- | --------- | --------------------------------------------------------------- | | `start` | `string` | Start time in `HH:mm` format (e.g. `"06:00"`) | | `end` | `string` | End time in `HH:mm` format (use `"24:00"` for end of day) | | `priceCtPerKwh` | `decimal` | Grid fee price in ct/kWh for this time window | --- ## Important notes - **Prices in ct/kWh**: The `price`, `gridFee`, and `priceCtPerKwh` fields are all in **cents per kilowatt-hour**. The `baseFee` field is in **EUR per month**. - **Dates are start-of-day in user timezone**: The time component of `from` and `to` has no effect. The system always truncates both values to the start of day (midnight) in the user's configured timezone, regardless of the time you send. Only the date portion matters. - **Open-ended tariffs**: Set `to` to `null` if the tariff has no known end date. - **Immutable fields on update**: You cannot change `type` (buy/sell) or `priceType` (fix/dynamic) when updating a tariff. Delete and recreate instead. - **Auto-generated components**: For fix and sell tariffs, any `variablePriceComponents` or `gridFee` values you send in the request body are silently ignored. The system always generates `variablePriceComponents` from `price`. - **Grid fee precedence for dynamic tariffs**: When both `variablePriceComponents` and `gridFee` are provided, `variablePriceComponents` takes precedence and `gridFee` is ignored. - **Response format**: POST returns `201 Created` with the full list of tariffs. PUT returns `200 OK` with the full list. DELETE returns `204 No Content` with no body. --- # Energy Prices *(source: `pages/energy-prices.mdx`)* ## TL;DR - `GET /v1/users/{userId}/energy-prices?from=...&to=...&resolution=...` — raw EPEX spot prices per time slot - `stockPrice` is in **ct/kWh**, **raw** (no grid fees, no taxes, no margin) - Selected day is interpreted in the **user's timezone**; `time` values are returned as **UTC** (`...Z`) - `null` `stockPrice` is legitimate (e.g. day-ahead not yet published) — not an error - Hard cap: **3-day window**, bounded by yesterday → tomorrow - Resolutions: `hourly` (default) or `quarterHourly` - Bidding zone is taken from the user's dynamic tariff if one exists; otherwise it defaults based on the user's timezone (e.g. `Europe/Berlin` → `DE-LU`) The Energy Prices endpoint returns dynamic electricity **stock prices** (raw EPEX, before grid fees and taxes) for a user's home, broken down per time slot. It's intended for partner integrations that need short-term spot price exposure — e.g. to display a price curve to the user or align scheduled loads with cheap hours. This endpoint does **not** return the user's contractual energy price. For the user's tariff configuration (including grid fees), see [Electricity Tariffs](/electricity-tariffs). --- ## Endpoint | Method | Endpoint | Description | | ------ | ----------------------------------------- | ----------------------------------------------- | | `GET` | `/v1/users/{userId}/energy-prices` | Raw stock prices per time slot for the user's home | The home is resolved server-side from the authenticated end-user. The `{userId}` path segment is required for routing and auditing, but the home is **not** chosen by the caller. --- ## Authentication This endpoint uses the same authentication as all Connect API endpoints: - **Scheme**: Bearer token (JWT), OAuth2 `client_credentials` - **Scope**: `connect_api` - **User context**: The `{userId}` path parameter is your external user ID (the same ID you use when provisioning users) See [Authentication](/api-authentication) for details on obtaining a token. --- ## Request ### Query parameters | Name | Type | Required | Notes | | ------------ | --------------------- | -------- | -------------------------------------------------------------------------------------- | | `from` | `date` (`yyyy-MM-dd`) | yes | Inclusive start. Must be **on or after yesterday**. | | `to` | `date` (`yyyy-MM-dd`) | yes | Inclusive end. Must be **on or before tomorrow**. `from <= to`. | | `resolution` | `string` | no | `hourly` (default) or `quarterHourly`. Case-insensitive. | **Maximum window**: 3 days (yesterday → tomorrow). This is a hard cap — data outside this window is not available from this endpoint. Partners that need a longer history must collect and store the slots day by day as they are published. ### Example request ```http GET /v1/users/3f9b1c7a-.../energy-prices?from=2026-06-10&to=2026-06-10&resolution=hourly Authorization: Bearer eyJhbGciOi... ``` --- ## Response ### `200 OK` A JSON array of slot objects. ```json [ { "time": "2026-06-09T22:00:00Z", "stockPrice": 8.42 }, { "time": "2026-06-09T23:00:00Z", "stockPrice": 7.95 }, { "time": "2026-06-10T00:00:00Z", "stockPrice": 6.10 }, { "time": "2026-06-10T01:00:00Z", "stockPrice": null } ] ``` ### Fields | Field | Type | Description | | ------------ | ---------- | ------------------------------------------------------------------------------------------------------------- | | `time` | `string` | UTC ISO-8601 (`...Z`). Start of the slot. `hourly` → top of the hour. `quarterHourly` → `:00`, `:15`, `:30`, `:45`. | | `stockPrice` | `decimal?` | Raw stock price in **ct/kWh**, **before** grid fees, taxes, and margin. `null` when no price is available for that slot. | > `stockPrice` is **not** the price the user pays. It's the EPEX reference price for the user's effective bidding zone. To compute what the user actually pays, add the grid fees from the user's tariff (see [Electricity Tariffs](/electricity-tariffs)) plus the applicable taxes. --- ## Timezone behaviour The `from` and `to` query parameters select a **day in the user's timezone**. The endpoint internally maps that day to midnight-to-midnight in the user's home timezone and converts to UTC for the underlying lookup. For example, `?from=2026-06-10&to=2026-06-10` for a user in `Europe/Berlin` covers `2026-06-09T22:00Z → 2026-06-10T22:00Z` during summer (CEST, UTC+2). Slot timestamps in the response are always **UTC** (`...Z`). No localisation is applied server-side — clients should convert to whatever timezone they need to display. --- ## Bidding zone selection The bidding zone used to source the stock prices is determined as follows: 1. **From the user's tariff** — if the user has an active dynamic tariff with a real bidding zone (e.g. `DE-LU`), prices for that zone are returned. 2. **Default by timezone** — if the user has no dynamic tariff, or the tariff is on a `flatline` bidding zone (a fixed-rate tariff with no real spot exposure), the bidding zone is defaulted based on the user's timezone (e.g. `Europe/Berlin` → `DE-LU`). Per-slot fallback: when the defaulted zone has no value for a given timestamp, the original value from the user's own zone is returned for that slot (which may be `null` or `0`). **Implication for partners**: `stockPrice` is the EPEX reference price for the user's **effective** exchange zone — possibly substituted via the defaulting rule above. Do not conflate it with the user's contractual energy price. --- ## Error responses All errors follow [RFC 7807](https://datatracker.ietf.org/doc/html/rfc7807) (`application/problem+json`). | Status | Meaning | | ------ | ---------------------------------------------------------------------------------------------------- | | `400` | Validation failure. `errors` keyed by `from` / `to` / `resolution` (e.g. missing, out of range, unknown resolution). | | `401` | Missing or invalid bearer token. | | `403` | Token lacks the `connect_api` scope. | | `404` | The authenticated user has no home. | | `429` | Global IP rate limit (100 requests / 10 seconds). | | `500` | Unhandled server error. | ### Example `400` ```json { "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "errors": { "from": ["'from' must be on or after 2026-06-09."] } } ``` --- ## Important notes - **Units**: `stockPrice` is in **ct/kWh** — raw spot price, no fees, no taxes, no margin. - **UTC only**: Slot timestamps are always UTC. Convert client-side for display. - **`null` is valid**: A `null` `stockPrice` means no value is available for that slot (e.g. day-ahead not yet published). It is not an error condition. - **3-day cap is hard**: Data outside yesterday → tomorrow is not available from this endpoint. Partners that need a longer history must collect and store the slots day by day as they are published. - **No metadata fields**: The endpoint deliberately returns only the per-slot list — no current price, no 14-day average, no "has dynamic tariff" flag, no push-enabled flag. Compute what you need from the slot data. - **`stockPrice` is not the user's price**: It's the EPEX reference for the user's effective bidding zone — possibly substituted via the [bidding zone selection](#bidding-zone-selection) rules. The user's actual price requires adding grid fees and taxes. --- # Connect API - Shelly Device Onboarding *(source: `pages/shelly-onboarding.mdx`)* ## TL;DR - Shelly uses the `form` flow — collect the user's **Shelly Cloud auth key** and POST it as connection data - A Shelly device's `model.category` determines its type: **smart plugs / relays** (e.g. Plug S, 1PM, 2PM) are `switch` and controllable via `on-off`; **EM / 3EM energy meters** (e.g. Shelly EM, 3EM, Pro 3EM, EM Gen3) are `electricMeter` and used for metering only (no `on-off`) - Only the `electricMeter` (EM / 3EM) variants require `measurement-purpose` via `PUT /devices/{id}/measurement-purpose` (`gridMeter` or `pvProduction`) — otherwise setup stays incomplete. `pvProduction` re-creates the device as a *producing* `switch` that reports PV output but is still **not** `on-off`-controllable - One Shelly Cloud account = one integrator = one connection. All Shelly devices under that account appear in the device list Complete flow for connecting Shelly devices via the Connect API. --- ## Step 1: List Vendors ```http GET /v1/users/{userId}/vendors ``` **Response:** ```json { "data": [ { "id": "85aa4cd1-2949-41be-a227-ce4bfe281e24", "name": "BMW" }, { "id": "ce1332aa-5d51-4222-8b4a-f759862b3ced", "name": "Home Connect" }, { "id": "f8c2e4a1-9b3d-4f7c-8a1e-2d5f6c9b4a3e", "name": "Shelly" }, { "id": "c36018d3-334e-4d44-82b9-324f2ff7de41", "name": "Solvis" }, { "id": "ae1b4468-217e-440d-a2c7-3c44d283a6c1", "name": "Tesla" }, { "id": "09527b83-6665-4202-9666-a59840ef3a27", "name": "Vaillant" }, { "id": "e591b6fb-9fdc-4283-ade9-d16266572882", "name": "Viessmann" }, { "id": "32630d0a-467b-4f8f-8dd0-b417e78a0b70", "name": "Volkswagen" } ] } ``` --- ## Step 2: Get Connection Options ```http GET /v1/users/{userId}/vendors/{vendorId}/connection-options ``` For Shelly (vendorId: `f8c2e4a1-9b3d-4f7c-8a1e-2d5f6c9b4a3e`): **Response:** ```json { "flow": "form", "formOptions": [ { "type": "login", "data": { "url": "https://my.shelly.cloud/integrator.html?itg=ITG_CPV1&cb=https%3a%2f%2ffa-cpv-shelly-itg-beta.azurewebsites.net%2fapi%2fShellyIntegrator%3fcode%3d6eUTCK1JBN8PA6khvwwagXb_27G6g0gd9O1raM4wcNEVAzFumTTRvg%3d%3d%26userid%3d76508964-f720-479d-9c6e-5f1c51b1d6bc" } } ], "docUrl": null } ``` The Shelly integration uses a `"form"` flow with a `"login"` type. This means you'll need to open the provided URL in a browser or WebView for user authentication. --- ## Step 3: Create Connection ```http POST /v1/users/{userId}/vendors/{vendorId}/connections ``` **Request:** ```json { "type": "form", "data": { } } ``` **Response:** ```json { "connectionId": "", "vendorId": "f8c2e4a1-9b3d-4f7c-8a1e-2d5f6c9b4a3e" } ``` --- ## Step 4: User Interaction After creating the connection, guide the user through the Shelly integrator: 1. Open browser/WebView to the URL from Step 2: `https://my.shelly.cloud/integrator.html?itg=ITG_CPV1&cb=https%3a%2f%2ffa-cpv-shelly-itg-beta.azurewebsites.net%2fapi%2fShellyIntegrator%3fcode%3d6eUTCK1JBN8PA6khvwwagXb_27G6g0gd9O1raM4wcNEVAzFumTTRvg%3d%3d%26userid%3d76508964-f720-479d-9c6e-5f1c51b1d6bc` 2. User logs in with their Shelly account. 3. User clicks the device to pair from the device list. 4. Authorize the integrator on the device-detail screen by performing **all three** of these actions: 1. Under the label **"ENABLE INTEGRATOR FOR THIS DEVICE"**, tap the **"YES"** button. 2. Under the label **"ALLOW DEVICE CONTROL"**, tap the **"YES"** button. 3. Tap the **"SAVE"** button at the bottom of the screen. 5. On success, the Shelly UI shows a **"Done"** alert. The user can close the tab afterwards. The devices are then pushed by Shelly to our backend and will appear in the next step. [image: Screenshot of the Shelly integrator device-detail screen showing two YES toggles labeled 'ENABLE INTEGRATOR FOR THIS DEVICE' and 'ALLOW DEVICE CONTROL', and a SAVE button at the bottom.] --- ## Step 5: List Devices from Vendor ```http GET /v1/users/{userId}/vendors/{vendorId}/devices?connectionId={connectionId} ``` **Response:** ```json { "data": [ { "name": "Office", "text": "", "model": { "id": "a5bcf4ef-9ad9-4183-9c60-5a1dcae3917b", "name": "Shelly Plus Plug S", "category": "switch" }, "externalId": "", "available": true, "connectionId": "" } ] } ``` --- ## Step 6: Create Device in Connect API ```http POST /v1/users/{userId}/devices ``` **Request:** ```json { "name": "Shelly Office in clever-PV", "externalId": "", "modelId": "a5bcf4ef-9ad9-4183-9c60-5a1dcae3917b", "connectionId": "" } ``` **Response:** ```json { "id": "", "name": "Shelly Office in clever-PV", "externalId": "", "model": { "id": "a5bcf4ef-9ad9-4183-9c60-5a1dcae3917b", "name": "ShellyPlusPlugS", "category": "switch", "vendor": { "id": "f8c2e4a1-9b3d-4f7c-8a1e-2d5f6c9b4a3e", "name": "Shelly" } }, "capabilities": [ { "name": "core", "data": { "name": "Office in clever-PV", "connectivityStatus": "online" }, "options": null, "interventions": [] }, { "name": "display-power", "data": { "power": null, "lastUpdate": null }, "options": null, "interventions": [] }, { "name": "on-off", "data": { "state": "off" }, "options": null, "interventions": [] }, { "name": "preferred-operating-mode", "data": { "mode": "smartControl" }, "options": null, "interventions": [] }, { "name": "price-control", "data": { "enabled": false, "threshold": 25 }, "options": null, "interventions": [] }, { "name": "priority", "data": { "priority": 1 }, "options": null, "interventions": [] }, { "name": "rated-capacity", "data": { "ratedCapacity": 100 }, "options": null, "interventions": [] }, { "name": "smart-mode", "data": { "enabled": false }, "options": null, "interventions": [] }, { "name": "solar-optimization", "data": { "enabled": false, "ratedCapacity": 100, "sunShare": 100 }, "options": null, "interventions": [] }, { "name": "switch-delay", "data": { "switchDelayOn": 0, "switchDelayOff": 0 }, "options": null, "interventions": [] }, { "name": "target-control", "data": { "enabled": false, "targetControlLock": null, "currentRuntime": 0, "hasReachedTarget": false, "lastTargetControl": null, "startTime": null, "endTime": null, "targetRuntime": null, "startDate": null, "endDate": null, "weekDays": null, "type": null, "useSmartChargePlan": null, "sendPushNotification": null, "enableTargetControlLimit": null, "lastUpdate": null }, "options": null, "interventions": [] }, { "name": "time-control", "data": { "nextSchedule": null }, "options": null, "interventions": [] } ] } ``` --- ## Step 7: Control Device (Turn Switch On/Off) ```http PUT /v1/users/{userId}/devices/{deviceId}/on-off ``` **Request (Turn On):** ```json { "state": "on" } ``` **Request (Turn Off):** ```json { "state": "off" } ``` **Response:** `202` --- ## Step 8: Get Device Status Retrieve the device with current power load and state: ```http GET /v1/users/{userId}/devices/{deviceId} ``` **Response:** ```json { "id": "", "name": "Office in clever-PV", "externalId": "", "model": { "id": "a5bcf4ef-9ad9-4183-9c60-5a1dcae3917b", "name": "ShellyPlusPlugS", "category": "switch", "vendor": { "id": "f8c2e4a1-9b3d-4f7c-8a1e-2d5f6c9b4a3e", "name": "Shelly" } }, "capabilities": [ { "name": "core", "data": { "name": "Office in clever-PV 3", "connectivityStatus": "online" }, "options": null, "interventions": [] }, { "name": "display-power", "data": { "power": 912, "lastUpdate": "2025-12-11T13:12:24Z" }, "options": null, "interventions": [] }, { "name": "on-off", "data": { "state": "on" }, "options": null, "interventions": [] }, { "name": "preferred-operating-mode", "data": { "mode": "smartControl" }, "options": null, "interventions": [] }, { "name": "price-control", "data": { "enabled": false, "threshold": 25 }, "options": null, "interventions": [] }, { "name": "priority", "data": { "priority": 1 }, "options": null, "interventions": [] }, { "name": "rated-capacity", "data": { "ratedCapacity": 100 }, "options": null, "interventions": [] }, { "name": "smart-mode", "data": { "enabled": false }, "options": null, "interventions": [] }, { "name": "solar-optimization", "data": { "enabled": false, "ratedCapacity": 100, "sunShare": 100 }, "options": null, "interventions": [] }, { "name": "switch-delay", "data": { "switchDelayOn": 0, "switchDelayOff": 0 }, "options": null, "interventions": [] }, { "name": "target-control", "data": { "enabled": false, "targetControlLock": null, "currentRuntime": 0, "hasReachedTarget": false, "lastTargetControl": null, "startTime": null, "endTime": null, "targetRuntime": null, "startDate": null, "endDate": null, "weekDays": null, "type": null, "useSmartChargePlan": null, "sendPushNotification": null, "enableTargetControlLimit": null, "lastUpdate": null }, "options": null, "interventions": [] }, { "name": "time-control", "data": { "nextSchedule": null }, "options": null, "interventions": [] } ] } ``` Note the `display-power` capability now shows the current power consumption (`power: 912` watts) and when it was last updated. --- # Connect API - OCPP Wallbox Onboarding *(source: `pages/ocpp-wallbox-onboarding.mdx`)* ## TL;DR - `POST /vendors/{id}/connections` with `type: "ocpp"` → read `flowOptions[name="ocppUrl"]` from the response → display the `wss://` URL to the user → user enters it in their wallbox config - The `ocppUrl` from `flowOptions` is **stable from the moment the connection is created** — safe to display, print, or store immediately. Do not use the URL from `GET /connection-options`; it is a randomized preview - The wallbox won't show up in `GET /vendors/{id}/devices` until it actually connects to the OCPP endpoint — tell users to save & restart - Only **one pending connection** allowed — if you need a new one, `DELETE` the old connection first (devices must be unlinked first) Complete flow for connecting OCPP wallboxes via the Connect API. --- ## Notes - Vendor/Model is a 1 to 1 relation (one Vendor per Model) - Only one pending connection at a time (this constraint might be removed in future) - Similar to form flow --- ## Step 1: List Vendors ```http GET /v1/users/{userId}/vendors?category=wallbox ``` **Response:** ```json { "data": [ { "id": "{vendorId}", "name": "go-e V3/Gemini/Pro via OCPP" } ] } ``` --- ## Step 2: Get Connection Options ```http GET /v1/users/{userId}/vendors/{vendorId}/connection-options ``` **Response:** ```json { "flow": "ocpp", "formOptions": [ { "type": "text", "data": { "value": "wss://ocpp.clever-pv.com//", "readonly": true, "label": "OCPP Connection URL", "description": "Enter this URL in your wallbox configuration" } } ], "docUrl": null } ``` - `OCPP-HOME-ID`: unique OCPP identifier per home/user, automatically created by the Connect API - `DEVICE-TAG`: Some devices require this id, but it's optional for others. In case the device requires the ID, it will be added by the Connect API. If not required, this part of the URL will not be provided > **Warning — do not display this URL to the user.** The `DEVICE-TAG` segment in this pre-creation response is freshly randomized on **every read** and is never persisted. Use this endpoint only to discover the flow type and field layout. The URL to give to the user is the one returned as `flowOptions[name="ocppUrl"]` after you create the connection in [Step 3](#step-3-create-connection) — that one is stable. --- ## Step 3: Create Connection ```http POST /v1/users/{userId}/vendors/{vendorId}/connections ``` **Request:** ```json { "type": "ocpp", "data": {} } ``` **Response:** ```json { "connectionId": "2c659624-ea66-49db-ae98-716b2e73fb0d", "vendorId": "{vendorId}", "flowOptions": [ { "name": "ocppUrl", "data": "wss://ocpp.clever-pv.com//" } ] } ``` > **Note:** Read the URL from `flowOptions[name="ocppUrl"].data`. Match by `name`, not by index — order is not guaranteed. The device tag in the URL is minted and persisted when the connection is created, so the URL is **stable from this point on**. The same array (with the identical URL) is re-surfaced on every `GET /v1/users/{userId}/vendors/connections?vendorId=...` call, so you can recover the URL later without re-creating the connection. --- ## Step 4: User Interaction required: Enter OCPP URL in Wallbox Ask the wallbox owner to: 1. Open the configuration interface of their wallbox (web UI or app). 2. Locate the setting for the **OCPP server URL** (sometimes called backend URL or charge point management URL). 3. Use the `ocppUrl` value from the `flowOptions` array returned in [**Step 3: Create Connection**](#step-3-create-connection) (or re-fetched via `GET /v1/users/{userId}/vendors/connections?vendorId={vendorId}`) in the wallbox device. 4. Save/apply the configuration and, if required by the device, restart the wallbox. > **URL stability:** The device tag is generated and persisted when the connection is created, so the `flowOptions` URL is stable from creation onward. Once the wallbox has booted, its persisted device tag continues to be used — the URL never changes between reads. > **Order doesn't break onboarding:** If the wallbox was already configured with an OCPP URL *before* the connection was created (e.g. from an earlier attempt), creating the connection triggers a server-side re-match of already-connected wallboxes. In that case the device typically appears in `GET /devices` within ~10 seconds without restarting the wallbox. As soon as the wallbox connects to the OCPP endpoint, it will appear in **Step 6: List Devices from Vendor**. --- ## Step 5: Delete Connection (Optional) In case the connection is not established, the connection should be deleted as there may only be one connection with unassigned OCPP devices at a time per userId. ```http DELETE /v1/users/{userId}/vendors/{vendorId}/connections/cfb0103c-a6be-4673-b926-f6dcb718a0a0 ``` **Response:** `204` on success > **Important:** All devices attached to the connection must be deleted first (via `DELETE /v1/users/{userId}/devices/{deviceId}`). If you attempt to delete a connection with linked devices, the endpoint will return an error. --- ## Step 6: List Devices from Vendor ```http GET /v1/users/{userId}/vendors/{vendorId}/devices?connectionId=2c659624-ea66-49db-ae98-716b2e73fb0d ``` **Response:** ```json { "data": [ { "name": "go-e V3/Gemini via OCPP", "text": "OCPP Wallbox - 85034558CP001 - Connector 1", "model": { "id": "0b1c2d3e-4f5a-4b6c-7d8e-9f0a1b2c3d4e", "name": "goe-v3-ocpp", "category": "wallbox" }, "externalId": "EXTERNAL_ID_OF_DEVICE", "available": true, "connectionId": "76979136-08ea-41e3-9aa3-1ec6fe162bd4" } ] } ``` --- ## Step 7: Create Device in Connect API ```http POST /v1/users/{userId}/devices ``` **Request:** ```json { "name": "Arbitrary display name for wallbox", "externalId": "EXTERNAL_ID_OF_DEVICE", "modelId": "0b1c2d3e-4f5a-4b6c-7d8e-9f0a1b2c3d4e", "connectionId": "2c659624-ea66-49db-ae98-716b2e73fb0d" } ``` **Response:** ```json { "data": [ { "id": "d43129c3-84f1-484d-b2a4-ef60d6a3b485", "name": "Arbitrary display name for wallbox", "externalId": "EXTERNAL_ID_OF_DEVICE", "model": { "id": "e9a3d7c4-2b8f-4a6d-c5f1-7b3e9a2d8c4f", "name": "go-e V3/Gemini via OCPP", "category": "wallbox", "vendor": { "id": "{vendorId}", "name": "go-e OCPP" } }, "capabilities": [ { "name": "core", "data": { "name": "Arbitrary display name for wallbox", "connectivityStatus": "connecting" }, "options": null, "interventions": [] }, { "name": "on-off", "data": { "state": "off" }, "options": null, "interventions": [] }, { "name": "display-power", "data": { "power": 0, "lastUpdate": null }, "options": null, "interventions": [] }, { "name": "display-charging-state", "data": { "chargingState": "waitingForStatus" }, "options": { "chargingState": [ "noCarConnected", "charging", "waitingOnCar", "completed", "error", "awaitingStart", "waitingForStatus" ] }, "interventions": [] }, { "name": "display-charging-current", "data": { "amperage": 0 }, "options": null, "interventions": [] }, { "name": "display-charging-phases", "data": { "phases": 0 }, "options": null, "interventions": [] }, { "name": "set-charging-power", "data": { "desiredAmperage": 6 }, "options": { "minAmperage": 6, "maxAmperage": 32 }, "interventions": [] }, { "name": "min-max-amperage", "data": { "minAmperage": 6, "maxAmperage": 32 }, "options": { "absoluteMinAmperage": 6, "absoluteMaxAmperage": 32 }, "interventions": [] }, { "name": "phase-mode", "data": { "phaseMode": "automatic3Phases" }, "options": { "availablePhaseModes": [ "singlePhase", "twoPhases", "threePhases", "automatic2Phases", "automatic3Phases" ] }, "interventions": [] }, { "name": "phase-switching", "data": { "supported": true }, "options": null, "interventions": [] }, { "name": "charging-mode", "data": { "chargingMode": "clever" }, "options": { "chargingMode": [ "clever", "casual", "chargeAlways" ] }, "interventions": [] }, { "name": "boost", "data": { "enabled": false }, "options": null, "interventions": [] }, { "name": "smart-mode", "data": { "enabled": false }, "options": null, "interventions": [] }, { "name": "solar-optimization", "data": { "enabled": true, "ratedCapacity": 1380, "sunShare": 100 }, "options": null, "interventions": [] }, { "name": "priority", "data": { "priority": 1 }, "options": null, "interventions": [] }, { "name": "rated-capacity", "data": { "ratedCapacity": 1380 }, "options": null, "interventions": [] }, { "name": "target-charging", "data": { "enabled": false, "targetControlLock": null, "currentRuntime": 0, "hasReachedTarget": false, "lastTargetControl": null, "executionTime": null, "targetWh": null, "targetSoC": null, "unitType": null, "targetDate": null, "weekDays": null, "type": null, "useSmartChargePlan": null, "sendPushNotification": null, "enableTargetControlLimit": null, "chargedWh": 0, "consumption": null, "batteryCapacity": null, "currentSoC": null, "socLimit": null, "lastUpdate": null }, "options": null, "interventions": [] }, { "name": "price-control", "data": { "enabled": false, "threshold": 25 }, "options": null, "interventions": [] }, { "name": "time-control", "data": { "nextSchedule": null }, "options": null, "interventions": [] } ] } ] } ``` --- # API Reference *(source: `pages/api-reference.mdx`)* The full interactive API documentation is available on our Swagger UI: **[Open API Reference](https://api.clever-pv.com/connect/swagger/index.html)** ## Authentication To use the API, you need to obtain an access token first. See the [Authentication](/api-authentication) page for details on how to get your credentials and request a token. --- # API Authentication *(source: `pages/api-authentication.mdx`)* ## TL;DR - `POST https://auth.clever-pv.com/connect/token` with `grant_type=client_credentials` and `scope=connect_api` - Use the returned `access_token` as `Authorization: Bearer {token}` on all requests - Tokens expire — check `expires_in` in the response and refresh before it runs out ## Base URLs [component: BaseUrls] ## Token Details The authentication uses OAuth2 Client Credentials flow: - **Token URL**: {tokenUrl} - **Grant Type**: `client_credentials` - **Scope**: `connect_api` Tokens are automatically cached and refreshed as needed. ## Example: Fetch Token with curl
curl -X POST "{tokenUrl}" 
  -H "Content-Type: application/x-www-form-urlencoded"
  -d "grant_type=client_credentials"
  -d "client_id=CLIENT_ID"
  -d "client_secret=CLIENT_SECRET"
  -d "scope=connect_api"
Replace `CLIENT_ID` and `CLIENT_SECRET` with your actual credentials. --- # API Credentials Setup *(source: `pages/api-credentials-setup.mdx`)* ## TL;DR - This page is only for the **docs playground** — enter your `client_id` + `client_secret` here to try endpoints interactively - Credentials stay in your browser session — we never see or store them To test API endpoints in the playground, you need to configure your API credentials. Your credentials are stored securely in your browser session only and are never sent to our documentation servers. [component: CredentialsForm] ## How it works 1. Enter your `client_id` and `client_secret` above 2. Click "Save Credentials" 3. Navigate to any API endpoint in the [API Reference](/api) 4. The playground will automatically obtain an access token and include it in your requests --- # OpenAPI Specification (live snapshot) Fetched from https://api.clever-pv.com/connect/swagger/v1/swagger.json at build time. The live spec at this URL is the source of truth. ```json { "openapi": "3.0.4", "info": { "title": "CleverPV.Connect.API", "version": "v1" }, "servers": [ { "url": "/connect" } ], "paths": { "/": { "get": { "tags": [ "CleverPV.Connect.API" ], "parameters": [ { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "200": { "description": "OK" } } } }, "/v1/users/{userId}/devices": { "post": { "tags": [ "Device 2.0" ], "summary": "Create a new device", "description": "Each devices should have a \"core\" capability.\nThe core capability determines if a device is usable or not.", "parameters": [ { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "requestBody": { "description": "Name, External ID and Model ID of the device", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateDeviceCommandDto" } }, "text/json": { "schema": { "$ref": "#/components/schemas/CreateDeviceCommandDto" } }, "application/*+json": { "schema": { "$ref": "#/components/schemas/CreateDeviceCommandDto" } } } }, "responses": { "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeviceDto" } } } }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "403": { "description": "Forbidden", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "404": { "description": "Not Found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "409": { "description": "Conflict", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConnectApiProblemDetails" } } } }, "500": { "description": "Internal Server Error" }, "200": { "description": "Returns the newly created device" } } }, "get": { "tags": [ "Device 2.0" ], "summary": "Get list of all devices", "parameters": [ { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "200": { "description": "List of Devices", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeviceDtoPaginatedResult" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "500": { "description": "Internal Server Error" } } } }, "/v1/users/{userId}/devices/{deviceId}": { "delete": { "tags": [ "Device 2.0" ], "summary": "Deletes a device. If vendor reset fails and force=false, returns 409 Conflict.", "parameters": [ { "name": "deviceId", "in": "path", "description": "ID of the device", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "force", "in": "query", "description": "Skip vendor reset and delete anyway", "schema": { "type": "boolean", "default": false } }, { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "204": { "description": "Successfully deleted device" }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "403": { "description": "Forbidden", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "404": { "description": "Device not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "409": { "description": "Vendor reset failed. Use force=true to delete anyway.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConnectApiProblemDetails" } } } }, "500": { "description": "Internal Server Error" } } }, "get": { "tags": [ "Device 2.0" ], "summary": "Get a device by ID", "parameters": [ { "name": "deviceId", "in": "path", "description": "ID of the device", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "200": { "description": "Device", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeviceDto" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "404": { "description": "Device not Found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "500": { "description": "Internal Server Error" } } }, "patch": { "tags": [ "Device 2.0" ], "summary": "Update a device", "parameters": [ { "name": "deviceId", "in": "path", "description": "ID of the device", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "requestBody": { "description": "The device properties to update", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateDeviceCommandDto" } }, "text/json": { "schema": { "$ref": "#/components/schemas/UpdateDeviceCommandDto" } }, "application/*+json": { "schema": { "$ref": "#/components/schemas/UpdateDeviceCommandDto" } } } }, "responses": { "204": { "description": "Device updated successfully" }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "403": { "description": "Forbidden", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "404": { "description": "Device not Found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "500": { "description": "Internal Server Error" } } } }, "/v1/users/{userId}/devices/{deviceId}/{capability}": { "put": { "tags": [ "Device 2.0" ], "summary": "Execute a capability for a device by id.", "description": "Request body must be the capability update DTO at the JSON root (camelCase property names),\ne.g. `{\"phaseMode\":\"threePhases\"}` for phase-mode. Wrapping fields in an extra object\nsuch as `{\"data\":{...}}` is not supported and returns 400 Bad Request.", "parameters": [ { "name": "deviceId", "in": "path", "description": "ID of the device", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "capability", "in": "path", "description": "The capability name to execute", "required": true, "schema": { "type": "string" } }, { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "requestBody": { "description": "Capability-specific update payload (root object, not wrapped)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ICapabilityData" } }, "text/json": { "schema": { "$ref": "#/components/schemas/ICapabilityData" } }, "application/*+json": { "schema": { "$ref": "#/components/schemas/ICapabilityData" } } } }, "responses": { "202": { "description": "Capability successfully executed" }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "402": { "description": "Payment Required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "404": { "description": "Device not Found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "412": { "description": "Precondition Failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "422": { "description": "Cannot execute Capability", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "500": { "description": "Internal Server Error" } } } }, "/v1/users/{userId}/electricity-tariffs": { "get": { "tags": [ "Electricity Tariffs" ], "summary": "Get all electricity tariffs for the user", "parameters": [ { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ElectricityTariffResponseDto" } } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "403": { "description": "Forbidden", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "500": { "description": "Internal Server Error" } } }, "post": { "tags": [ "Electricity Tariffs" ], "summary": "Create a new electricity tariff", "parameters": [ { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "requestBody": { "description": "The tariff to create", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateElectricityTariffRequestDto" } }, "text/json": { "schema": { "$ref": "#/components/schemas/CreateElectricityTariffRequestDto" } }, "application/*+json": { "schema": { "$ref": "#/components/schemas/CreateElectricityTariffRequestDto" } } } }, "responses": { "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ElectricityTariffResponseDto" } } } } }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConnectApiProblemDetails" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "403": { "description": "Forbidden", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "500": { "description": "Internal Server Error" } } } }, "/v1/users/{userId}/electricity-tariffs/{id}": { "put": { "tags": [ "Electricity Tariffs" ], "summary": "Update an existing electricity tariff", "parameters": [ { "name": "id", "in": "path", "description": "ID of the tariff to update", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "requestBody": { "description": "The updated tariff data", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateElectricityTariffRequestDto" } }, "text/json": { "schema": { "$ref": "#/components/schemas/UpdateElectricityTariffRequestDto" } }, "application/*+json": { "schema": { "$ref": "#/components/schemas/UpdateElectricityTariffRequestDto" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ElectricityTariffResponseDto" } } } } }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConnectApiProblemDetails" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "403": { "description": "Forbidden", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "500": { "description": "Internal Server Error" } } }, "delete": { "tags": [ "Electricity Tariffs" ], "summary": "Delete an electricity tariff", "parameters": [ { "name": "id", "in": "path", "description": "ID of the tariff to delete", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "204": { "description": "No Content" }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConnectApiProblemDetails" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "403": { "description": "Forbidden", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "500": { "description": "Internal Server Error" } } } }, "/v1/users/{userId}/energy-data/daily": { "get": { "tags": [ "Energy Data" ], "summary": "Get daily energy data (5-minute resolution) for a user", "parameters": [ { "name": "date", "in": "query", "description": "Date in yyyy-MM-dd format", "schema": { "type": "string", "format": "date" } }, { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EnergyDataDailyResponseDto" } } } }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "403": { "description": "Forbidden", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "404": { "description": "Not Found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "500": { "description": "Internal Server Error" } } } }, "/v1/users/{userId}/energy-data/period": { "get": { "tags": [ "Energy Data" ], "summary": "Get consolidated energy data (weekly, monthly, yearly aggregates) for a user", "parameters": [ { "name": "date", "in": "query", "description": "Date in yyyy-MM-dd format", "schema": { "type": "string", "format": "date" } }, { "name": "period", "in": "query", "description": "Resolution: weekly, monthly, yearly", "schema": { "$ref": "#/components/schemas/EnergyDataPeriod" } }, { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EnergyDataConsolidatedResponseDto" } } } }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "403": { "description": "Forbidden", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "404": { "description": "Not Found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "500": { "description": "Internal Server Error" } } } }, "/v1/users/{userId}/energy-prices": { "get": { "tags": [ "Energy Prices" ], "summary": "Get dynamic electricity stock prices for the user's home over a 3-day window\n(yesterday, today, tomorrow). Both bounds are inclusive.", "parameters": [ { "name": "From", "in": "query", "description": "Start date (inclusive) in the partner's timezone. Format: `yyyy-MM-dd`.", "schema": { "type": "string", "format": "date" } }, { "name": "To", "in": "query", "description": "End date (inclusive) in the partner's timezone. Format: `yyyy-MM-dd`.", "schema": { "type": "string", "format": "date" } }, { "name": "Resolution", "in": "query", "description": "Slot resolution. `hourly` (default) or `quarterHourly`. Case-insensitive.", "schema": { "$ref": "#/components/schemas/EnergyPriceResolution" } }, { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/EnergyPriceSlotDto" } } } } }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConnectApiProblemDetails" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "403": { "description": "Forbidden", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "404": { "description": "Not Found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConnectApiProblemDetails" } } } }, "500": { "description": "Internal Server Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConnectApiProblemDetails" } } } } } } }, "/v1/users/{userId}/home/location": { "put": { "tags": [ "Home" ], "summary": "Set the geographic location of the user's home. Required before a solar forecast can be generated.", "parameters": [ { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SetHomeLocationRequestDto" } }, "text/json": { "schema": { "$ref": "#/components/schemas/SetHomeLocationRequestDto" } }, "application/*+json": { "schema": { "$ref": "#/components/schemas/SetHomeLocationRequestDto" } } } }, "responses": { "204": { "description": "No Content" }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "404": { "description": "Not Found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } } } } }, "/v1/users/{userId}/home/solar-forecast": { "put": { "tags": [ "Home" ], "summary": "Enable or disable the solar forecast for the user's home.\nEnabling also restarts forecast generation from the stored home location.", "parameters": [ { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SetSolarForecastEnabledRequestDto" } }, "text/json": { "schema": { "$ref": "#/components/schemas/SetSolarForecastEnabledRequestDto" } }, "application/*+json": { "schema": { "$ref": "#/components/schemas/SetSolarForecastEnabledRequestDto" } } } }, "responses": { "204": { "description": "No Content" }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "404": { "description": "Not Found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } } } }, "get": { "tags": [ "Home" ], "summary": "Get the solar production forecast for the user's home.", "parameters": [ { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SolarForecastResponseDto" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "404": { "description": "Not Found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "409": { "description": "Conflict", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } } } } }, "/v1/users/{userId}/home/energy-forecast": { "get": { "tags": [ "Home" ], "summary": "Get the quarter-hourly energy forecast (price, consumption, PV production, battery state and\ngrid flows) for the user's home.", "parameters": [ { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EnergyForecastResponseDto" } } } }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "404": { "description": "Not Found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "409": { "description": "Conflict", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } } } } }, "/v1/users": { "get": { "tags": [ "Management" ], "summary": "List all users provisioned via this Connect API client.", "parameters": [ { "name": "size", "in": "query", "description": "Number of items per page (1–100, default 25)", "schema": { "type": "integer", "format": "int32", "default": 25 } }, { "name": "offset", "in": "query", "description": "Number of items to skip for pagination (default 0)", "schema": { "type": "integer", "format": "int32", "default": 0 } }, { "name": "search", "in": "query", "description": "Filter users by external user ID (case-insensitive substring match)", "schema": { "type": "string" } }, { "name": "sortBy", "in": "query", "description": "Field to sort by. Allowed values:\n`externaluserid`, `devicecount`, `clientname`, `created` (default)", "schema": { "type": "string" } }, { "name": "sortDir", "in": "query", "description": "Sort direction: `asc` or `desc` (default)", "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "200": { "description": "Returns the paginated list of users", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConnectApiUserResponsePaginatedResult" } } } }, "401": { "description": "Authentication token is missing or invalid", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } } } } }, "/v1/users/{userId}": { "delete": { "tags": [ "Management" ], "summary": "Delete a user provisioned via this Connect API client.", "description": "Permanently removes the user together with its home, all devices, vendor connections and the\nConnect-API-managed subscription. This is a hard delete and cannot be undone.\n \nThe user is identified by the same `userId` used on every other endpoint. Unlike those\nendpoints, this one never creates the user on the fly: if no user exists for the given\n`userId`, the request returns 404 instead of provisioning one.\n \nDeletion is scoped to the calling client — you can only delete users that were provisioned\nthrough your own Connect API credentials.", "parameters": [ { "name": "userId", "in": "path", "description": "The partner-defined user identifier (minimum 5 characters).", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "204": { "description": "User successfully deleted" }, "401": { "description": "Authentication token is missing or invalid", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "404": { "description": "No user exists for the given userId under this client", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConnectApiProblemDetails" } } } } } } }, "/v1/devices": { "get": { "tags": [ "Management" ], "summary": "List all devices across all users provisioned via this Connect API client.", "parameters": [ { "name": "size", "in": "query", "description": "Number of items per page (1–100, default 25)", "schema": { "type": "integer", "format": "int32", "default": 25 } }, { "name": "offset", "in": "query", "description": "Number of items to skip for pagination (default 0)", "schema": { "type": "integer", "format": "int32", "default": 0 } }, { "name": "category", "in": "query", "description": "Filter by device category (e.g. ElectricMeter, Wallbox, Switch, PowerStorage)", "schema": { "$ref": "#/components/schemas/DeviceCategory" } }, { "name": "search", "in": "query", "description": "Search by device name, external ID, or internal ID (case-insensitive substring match)", "schema": { "type": "string" } }, { "name": "userId", "in": "query", "description": "Filter devices by the user ID of their owner (exact match)", "schema": { "type": "string" } }, { "name": "vendorName", "in": "query", "description": "Filter by vendor/manufacturer name (exact match, case-insensitive)", "schema": { "type": "string" } }, { "name": "sortBy", "in": "query", "description": "Field to sort by. Allowed values:\n`name`, `category`, `vendorname`, `created` (default)", "schema": { "type": "string" } }, { "name": "sortDir", "in": "query", "description": "Sort direction: `asc` or `desc` (default)", "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "200": { "description": "Returns the paginated list of devices", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConnectApiDeviceResponsePaginatedResult" } } } }, "401": { "description": "Authentication token is missing or invalid", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } } } } }, "/v1/catalog/vendors": { "get": { "tags": [ "PublicCatalog" ], "summary": "Get the supported vendors (app-scoped when authenticated, otherwise the clever-PV catalog)", "parameters": [ { "name": "category", "in": "query", "description": "The category to filter the vendors by.", "schema": { "$ref": "#/components/schemas/DeviceCategory" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "200": { "description": "Returns the vendors", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VendorDtoPaginatedResult" } } } } } } }, "/v1/catalog/vendors/{vendorId}": { "get": { "tags": [ "PublicCatalog" ], "summary": "Get a supported vendor by id (app-scoped when authenticated, otherwise the clever-PV catalog)", "parameters": [ { "name": "vendorId", "in": "path", "description": "The vendor id", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "200": { "description": "Returns the vendor", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VendorDto" } } } }, "404": { "description": "Vendor not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } } } } }, "/v1/catalog/vendors/{vendorId}/connection-options": { "get": { "tags": [ "PublicCatalog" ], "summary": "Get the connection options for a supported vendor (clever-PV catalog)", "description": "Always resolved in CleverPV.Application.Vendors.Queries.VendorCatalogResolveMode.Public, even for authenticated\ncallers: this catalog route carries no user/home, so placeholders cannot be resolved against\nlive state and are returned as readable documentation tokens (e.g. `HOME_OCPP_ID`). The\nmodel lookup itself is not AppId-scoped, so there is nothing app-specific to resolve here.", "parameters": [ { "name": "vendorId", "in": "path", "description": "The vendor id", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "200": { "description": "Returns the connection options", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VendorConnectionOptionsResponse" } } } }, "404": { "description": "Vendor not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } } } } }, "/v1/users/{userId}/vendors": { "get": { "tags": [ "Vendors" ], "summary": "Get available vendors", "parameters": [ { "name": "category", "in": "query", "description": "The category to filter the vendors by.", "schema": { "$ref": "#/components/schemas/DeviceCategory" } }, { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "200": { "description": "Returns the vendors", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VendorsDtoPaginatedResult" } } } } } } }, "/v1/users/{userId}/vendors/{vendorId}": { "get": { "tags": [ "Vendors" ], "summary": "Get vendor by id", "parameters": [ { "name": "vendorId", "in": "path", "description": "The vendor id", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "200": { "description": "Returns the vendor", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VendorsDto" } } } }, "404": { "description": "Vendor not found" } } } }, "/v1/users/{userId}/vendors/{vendorId}/connection-options": { "get": { "tags": [ "Vendors" ], "summary": "Get connection options for vendor", "parameters": [ { "name": "vendorId", "in": "path", "description": "The vendor id", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "200": { "description": "Returns the connections options", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VendorConnectionOptionsDtoPaginatedResult" } } } } } } }, "/v1/users/{userId}/vendors/{vendorId}/connections": { "post": { "tags": [ "Vendors" ], "summary": "Establish a persistent connection to the vendor", "parameters": [ { "name": "vendorId", "in": "path", "description": "The vendor id", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "requestBody": { "description": "The connection details", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateConnectionDto" } }, "text/json": { "schema": { "$ref": "#/components/schemas/CreateConnectionDto" } }, "application/*+json": { "schema": { "$ref": "#/components/schemas/CreateConnectionDto" } } } }, "responses": { "200": { "description": "Returns connection ID", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GetVendorConnectionDto" } } } }, "400": { "description": "Invalid onboarding flow type provided", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "403": { "description": "Unable to verify connection to vendor", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "404": { "description": "Provided connection method not found\nNo home found for requester", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "422": { "description": "Vendor does not support the requested onboarding flow", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } } } } }, "/v1/users/{userId}/vendors/{vendorId}/connections/{connectionId}": { "delete": { "tags": [ "Vendors" ], "summary": "Delete a connection for a vendor", "parameters": [ { "name": "vendorId", "in": "path", "description": "The vendor id", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "connectionId", "in": "path", "description": "The connection id to delete", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "204": { "description": "Connection successfully deleted" }, "404": { "description": "Connection not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "409": { "description": "Connection has associated devices that must be deleted first", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConnectApiProblemDetails" } } } } } } }, "/v1/users/{userId}/vendors/{vendorId}/devices": { "get": { "tags": [ "Vendors" ], "summary": "Get devices from connected vendor", "parameters": [ { "name": "vendorId", "in": "path", "description": "The vendor id", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "connectionId", "in": "query", "description": "The connection id to use", "schema": { "type": "string", "format": "uuid" } }, { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "403": { "description": "Unable to verify connection to vendor", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "404": { "description": "No connection for vendor found\nNo home found for requester", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "409": { "description": "User tries to add more than one pending OCPP connection\nUser tries to add more than one pending OCPP connection", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "200": { "description": "List of (supported) devices", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VendorDeviceDtoPaginatedResult" } } } }, "206": { "description": "List of (supported) devices - Unsupported models found.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VendorDeviceDtoPaginatedResult" } } } } } } }, "/v1/users/{userId}/vendors/connections": { "get": { "tags": [ "Vendors" ], "summary": "Get usable connections for vendors", "parameters": [ { "name": "vendorId", "in": "query", "description": "The vendor id", "schema": { "type": "string", "format": "uuid" } }, { "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Connect-TimeZone", "in": "header", "description": "IANA timezone identifier for the user (e.g., 'Europe/Berlin', 'America/New_York', 'UTC'). Defaults to 'Europe/Berlin' if not provided.", "schema": { "type": "string", "default": "Europe/Berlin", "example": "Europe/Berlin" } } ], "responses": { "200": { "description": "List of connections", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GetVendorConnectionDtoPaginatedResult" } } } } } } } }, "components": { "schemas": { "AddChargingLocationCapabilityData": { "type": "object", "properties": { "chargingLocationId": { "type": "string", "nullable": true } }, "additionalProperties": false }, "AddChargingLocationCapabilityUpdateData": { "type": "object", "additionalProperties": false }, "AiRecommendationMode": { "enum": [ "disabled", "manualAccept", "autoAccept" ], "type": "string" }, "AiRecommendationModeCapabilityData": { "type": "object", "properties": { "mode": { "$ref": "#/components/schemas/AiRecommendationMode" } }, "additionalProperties": false }, "AndroidModelAppUrlCapabilityData": { "type": "object", "properties": { "url": { "type": "string", "nullable": true } }, "additionalProperties": false }, "AndroidModelAppUrlCapabilityUpdateData": { "type": "object", "additionalProperties": false }, "BatteryCapacityCapabilityData": { "type": "object", "properties": { "batteryCapacity": { "type": "integer", "format": "int32", "nullable": true } }, "additionalProperties": false }, "BatteryCapacityCapabilityUpdateData": { "type": "object", "properties": { "batteryCapacity": { "type": "integer", "format": "int32" } }, "additionalProperties": false }, "BoostCapabilityData": { "type": "object", "properties": { "enabled": { "type": "boolean" } }, "additionalProperties": false }, "ChargeConnectorType": { "enum": [ "wallPlug", "unknownWallbox", "cpvWallbox" ], "type": "string" }, "ChargingMode": { "enum": [ "clever", "casual", "chargeAlways" ], "type": "string" }, "ChargingModeCapabilityData": { "type": "object", "properties": { "chargingMode": { "$ref": "#/components/schemas/ChargingMode" } }, "additionalProperties": false }, "ChargingModeCapabilityUpdateData": { "type": "object", "properties": { "chargingMode": { "$ref": "#/components/schemas/ChargingMode" } }, "additionalProperties": false }, "ChargingState": { "enum": [ "noCarConnected", "charging", "waitingOnCar", "completed", "error", "awaitingStart", "waitingForStatus" ], "type": "string" }, "CodeExchangeRequest": { "required": [ "code" ], "type": "object", "properties": { "code": { "type": "string", "nullable": true }, "codeChallenge": { "type": "string", "nullable": true } }, "additionalProperties": false }, "ConnectApiDeviceResponse": { "required": [ "category", "clientId", "clientName", "externalId", "modelName", "name", "userId", "vendorName" ], "type": "object", "properties": { "externalId": { "type": "string", "description": "The partner-defined external device identifier.", "nullable": true }, "name": { "type": "string", "description": "Display name of the device.", "nullable": true }, "category": { "$ref": "#/components/schemas/DeviceCategory" }, "vendorName": { "type": "string", "description": "Name of the device vendor/manufacturer.", "nullable": true }, "modelName": { "type": "string", "description": "Name of the device model.", "nullable": true }, "userId": { "type": "string", "description": "The user ID of the user who owns this device.", "nullable": true }, "clientId": { "type": "string", "description": "The OAuth2 client ID that created this device.", "nullable": true }, "clientName": { "type": "string", "description": "Display name of the OAuth2 client that created this device.", "nullable": true } }, "additionalProperties": false, "description": "A device created via the Connect API." }, "ConnectApiDeviceResponsePaginatedResult": { "required": [ "data" ], "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/ConnectApiDeviceResponse" }, "nullable": true }, "cursor": { "type": "string", "nullable": true }, "size": { "type": "integer", "format": "int32", "nullable": true }, "total": { "type": "integer", "format": "int32", "nullable": true }, "hasMoreResults": { "type": "boolean", "nullable": true } }, "additionalProperties": false }, "ConnectApiProblemDetails": { "required": [ "status", "title", "type" ], "type": "object", "properties": { "type": { "$ref": "#/components/schemas/ProblemType" }, "title": { "type": "string", "description": "Short, human-readable summary of the problem type.", "nullable": true }, "status": { "type": "integer", "description": "HTTP status code for this problem.", "format": "int32" }, "detail": { "type": "string", "description": "Human-readable explanation specific to this occurrence of the problem.", "nullable": true }, "traceId": { "type": "string", "description": "Unique identifier for this request, useful for debugging and support.", "nullable": true }, "errorCode": { "$ref": "#/components/schemas/PartnerErrorCode" }, "issues": { "type": "array", "items": { "$ref": "#/components/schemas/ValidationIssue" }, "description": "Collection of validation issues when the request fails validation.\nOnly populated for validation errors (HTTP 400).", "nullable": true }, "extensions": { "type": "object", "additionalProperties": { }, "description": "Additional problem-specific extension properties.\nCan contain metadata like retry-after, documentation links, etc.", "nullable": true } }, "additionalProperties": false, "description": "RFC 7807 Problem Details response for Connect API errors.\nProvides standardized error responses for B2B partner integrations." }, "ConnectApiUserResponse": { "required": [ "clientId", "clientName", "created", "deviceCount", "userId" ], "type": "object", "properties": { "userId": { "type": "string", "description": "The partner-defined user identifier.", "nullable": true }, "deviceCount": { "type": "integer", "description": "Total number of devices associated with this user.", "format": "int32" }, "created": { "type": "string", "description": "UTC timestamp when the user was first provisioned.", "format": "date-time" }, "clientId": { "type": "string", "description": "The OAuth2 client ID that created this user.", "nullable": true }, "clientName": { "type": "string", "description": "Display name of the OAuth2 client that created this user.", "nullable": true } }, "additionalProperties": false, "description": "A user provisioned via the Connect API." }, "ConnectApiUserResponsePaginatedResult": { "required": [ "data" ], "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/ConnectApiUserResponse" }, "nullable": true }, "cursor": { "type": "string", "nullable": true }, "size": { "type": "integer", "format": "int32", "nullable": true }, "total": { "type": "integer", "format": "int32", "nullable": true }, "hasMoreResults": { "type": "boolean", "nullable": true } }, "additionalProperties": false }, "ConnectivityStatus": { "enum": [ "offline", "online", "connecting" ], "type": "string" }, "ConnectorTypeCapabilityData": { "type": "object", "properties": { "connectorType": { "$ref": "#/components/schemas/ChargeConnectorType" } }, "additionalProperties": false }, "ConnectorTypeCapabilityUpdateData": { "type": "object", "properties": { "connectorType": { "$ref": "#/components/schemas/ChargeConnectorType" }, "powerMode": { "$ref": "#/components/schemas/PowerMode" } }, "additionalProperties": false }, "ConsumptionCapabilityData": { "type": "object", "properties": { "consumption": { "type": "integer", "format": "int32", "nullable": true } }, "additionalProperties": false }, "ConsumptionCapabilityUpdateData": { "type": "object", "properties": { "consumption": { "type": "integer", "format": "int32" } }, "additionalProperties": false }, "ConsumptionData": { "type": "object", "properties": { "startTimeUtc": { "type": "string", "format": "date-time" }, "endTimeUtc": { "type": "string", "format": "date-time" }, "interval": { "type": "string", "nullable": true }, "entries": { "type": "array", "items": { "$ref": "#/components/schemas/HourlyConsumptionEntry" }, "nullable": true }, "solarHours": { "type": "number", "format": "double" }, "priceHours": { "type": "number", "format": "double" }, "normalHours": { "type": "number", "format": "double" } }, "additionalProperties": false }, "ConsumptionOverviewCapabilityData": { "type": "object", "properties": { "series": { "type": "array", "items": { "$ref": "#/components/schemas/ConsumptionData" }, "nullable": true } }, "additionalProperties": false }, "ControlPermissionCapabilityData": { "type": "object", "properties": { "state": { "$ref": "#/components/schemas/ControlPermissionState" } }, "additionalProperties": false }, "ControlPermissionCapabilityUpdateData": { "type": "object", "properties": { "request": { "$ref": "#/components/schemas/ControlPermissionRequestType" } }, "additionalProperties": false }, "ControlPermissionRequestType": { "enum": [ "unknown", "allow", "forbid" ], "type": "string" }, "ControlPermissionState": { "enum": [ "unknown", "forbidden", "pending", "allowed" ], "type": "string" }, "CoreCapabilityData": { "type": "object", "properties": { "connectivityStatus": { "$ref": "#/components/schemas/ConnectivityStatus" }, "lastUpdate": { "type": "string", "format": "date-time", "nullable": true } }, "additionalProperties": false }, "CoreCapabilityReadData": { "type": "object", "properties": { "name": { "type": "string", "nullable": true }, "connectivityStatus": { "$ref": "#/components/schemas/ConnectivityStatus" } }, "additionalProperties": false }, "CoreCapabilityUpdateData": { "type": "object", "properties": { "name": { "type": "string", "nullable": true } }, "additionalProperties": false }, "CreateConnectionDto": { "type": "object", "properties": { "type": { "$ref": "#/components/schemas/DeviceOnboardingFlow" } }, "additionalProperties": false }, "CreateConnectionViaAuthCodeDto": { "required": [ "data" ], "type": "object", "properties": { "data": { "$ref": "#/components/schemas/CodeExchangeRequest" }, "type": { "$ref": "#/components/schemas/DeviceOnboardingFlow" } }, "additionalProperties": false }, "CreateConnectionViaFormDto": { "required": [ "data" ], "type": "object", "properties": { "data": { "type": "object", "additionalProperties": { "type": "string" }, "nullable": true }, "type": { "$ref": "#/components/schemas/DeviceOnboardingFlow" } }, "additionalProperties": false }, "CreateConnectionViaUrlDto": { "required": [ "data" ], "type": "object", "properties": { "data": { "$ref": "#/components/schemas/InterceptedUrl" }, "type": { "$ref": "#/components/schemas/DeviceOnboardingFlow" } }, "additionalProperties": false }, "CreateDeviceCommandDto": { "required": [ "externalId", "modelId", "name" ], "type": "object", "properties": { "name": { "type": "string", "nullable": true }, "externalId": { "type": "string", "nullable": true }, "modelId": { "type": "string", "nullable": true }, "connectionId": { "type": "string", "format": "uuid", "nullable": true } }, "additionalProperties": false }, "CreateElectricityTariffRequestDto": { "required": [ "from", "priceType", "type" ], "type": "object", "properties": { "from": { "type": "string", "format": "date-time" }, "to": { "type": "string", "format": "date-time", "nullable": true }, "price": { "type": "number", "format": "double", "nullable": true }, "baseFee": { "type": "number", "format": "double", "nullable": true }, "gridFee": { "type": "number", "format": "double", "nullable": true }, "bettingZone": { "type": "string", "nullable": true }, "priceType": { "$ref": "#/components/schemas/TariffPriceType" }, "type": { "$ref": "#/components/schemas/TariffType" }, "variablePriceComponents": { "type": "array", "items": { "$ref": "#/components/schemas/VariablePriceComponent" }, "nullable": true } }, "additionalProperties": false }, "CreateVendorConnectionDto": { "type": "object", "properties": { "type": { "$ref": "#/components/schemas/DeviceOnboardingFlow" }, "data": { "type": "object", "additionalProperties": { "type": "string" }, "nullable": true } }, "additionalProperties": false }, "DayOfWeek": { "enum": [ "sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday" ], "type": "string" }, "DeviceCapabilityDto": { "type": "object", "properties": { "name": { "type": "string", "nullable": true }, "data": { "nullable": true }, "options": { "nullable": true }, "interventions": { "type": "array", "items": { "$ref": "#/components/schemas/InterventionDto" }, "nullable": true } }, "additionalProperties": false }, "DeviceCategory": { "enum": [ "electricMeter", "wallbox", "switch", "powerStorage", "car", "heatPump", "heatingRod", "dishwasher" ], "type": "string" }, "DeviceControlAction": { "enum": [ "turnOn", "turnOff", "smartOn", "boostOn", "boostOff", "socPlanOn", "socPlanOff", "targetTimesOn", "targetTimesOff" ], "type": "string" }, "DeviceDto": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "nullable": true }, "externalId": { "type": "string", "nullable": true }, "model": { "$ref": "#/components/schemas/ModelDto" }, "capabilities": { "type": "array", "items": { "$ref": "#/components/schemas/DeviceCapabilityDto" }, "nullable": true } }, "additionalProperties": false }, "DeviceDtoPaginatedResult": { "required": [ "data" ], "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/DeviceDto" }, "nullable": true }, "cursor": { "type": "string", "nullable": true }, "size": { "type": "integer", "format": "int32", "nullable": true }, "total": { "type": "integer", "format": "int32", "nullable": true }, "hasMoreResults": { "type": "boolean", "nullable": true } }, "additionalProperties": false }, "DeviceOnboardingFlow": { "enum": [ "none", "form", "oidcProxy", "oidc", "ocpp", "push" ], "type": "string" }, "DeviceOnboardingFormFieldType": { "enum": [ "text", "secret", "login", "oidc" ], "type": "string" }, "DeviceScheduleInfo": { "type": "object", "properties": { "id": { "type": "string", "nullable": true }, "type": { "$ref": "#/components/schemas/ScheduleType" }, "weekDays": { "type": "array", "items": { "$ref": "#/components/schemas/DayOfWeek" }, "nullable": true }, "executionTime": { "type": "string", "format": "date-span" }, "action": { "$ref": "#/components/schemas/DeviceControlAction" }, "actionDetails": { "nullable": true }, "isActive": { "type": "boolean" }, "description": { "type": "string", "nullable": true }, "sendPushNotification": { "type": "boolean" } }, "additionalProperties": false }, "DeviceVendorDto": { "required": [ "id", "name" ], "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "nullable": true } }, "additionalProperties": false }, "DisplayBatteryStateCapabilityData": { "type": "object", "properties": { "state": { "$ref": "#/components/schemas/PowerStorageState" }, "chargingPower": { "type": "integer", "format": "int32", "nullable": true }, "lastUpdate": { "type": "string", "format": "date-time", "nullable": true } }, "additionalProperties": false }, "DisplayChargingCurrentCapabilityData": { "type": "object", "properties": { "amperage": { "type": "integer", "format": "int32" } }, "additionalProperties": false }, "DisplayChargingPhasesCapabilityData": { "type": "object", "properties": { "phases": { "type": "integer", "format": "int32" } }, "additionalProperties": false }, "DisplayChargingStateCapabilityData": { "type": "object", "properties": { "chargingState": { "$ref": "#/components/schemas/ChargingState" } }, "additionalProperties": false }, "DisplayCommunityPowerCapabilityData": { "type": "object", "properties": { "communityPower": { "type": "integer", "format": "int32", "nullable": true }, "lastUpdate": { "type": "string", "format": "date-time", "nullable": true } }, "additionalProperties": false }, "DisplayEstimatedRangeCapabilityData": { "type": "object", "properties": { "range": { "type": "integer", "format": "int32" } }, "additionalProperties": false }, "DisplayGridPowerCapabilityData": { "type": "object", "properties": { "gridPower": { "type": "integer", "format": "int32", "nullable": true }, "lastUpdate": { "type": "string", "format": "date-time", "nullable": true } }, "additionalProperties": false }, "DisplayHeatingStateCapabilityData": { "type": "object", "properties": { "mode": { "$ref": "#/components/schemas/HeatingControlState" }, "state": { "$ref": "#/components/schemas/HeatingState" }, "type": { "$ref": "#/components/schemas/HeatingType" }, "lastUpdate": { "type": "string", "format": "date-time", "nullable": true } }, "additionalProperties": false }, "DisplayPercentagePowerCapabilityData": { "type": "object", "properties": { "percentage": { "type": "number", "format": "double" }, "lastUpdate": { "type": "string", "format": "date-time", "nullable": true } }, "additionalProperties": false }, "DisplayPowerCapabilityData": { "type": "object", "properties": { "power": { "type": "integer", "format": "int32", "nullable": true }, "lastUpdate": { "type": "string", "format": "date-time", "nullable": true } }, "additionalProperties": false }, "DisplaySocCapabilityData": { "type": "object", "properties": { "soc": { "type": "integer", "format": "int32", "nullable": true }, "lastUpdate": { "type": "string", "format": "date-time", "nullable": true } }, "additionalProperties": false }, "DisplaySolarPowerCapabilityData": { "type": "object", "properties": { "solarPower": { "type": "integer", "format": "int32", "nullable": true }, "lastUpdate": { "type": "string", "format": "date-time", "nullable": true } }, "additionalProperties": false }, "DisplayTemperatureCapabilityData": { "type": "object", "properties": { "temperature": { "type": "number", "format": "double" }, "lastUpdate": { "type": "string", "format": "date-time", "nullable": true } }, "additionalProperties": false }, "ElectricityTariffResponseDto": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "nullable": true }, "from": { "type": "string", "format": "date-time" }, "to": { "type": "string", "format": "date-time", "nullable": true }, "price": { "type": "number", "format": "double", "nullable": true }, "baseFee": { "type": "number", "format": "double", "nullable": true }, "gridFee": { "type": "number", "format": "double", "nullable": true }, "bettingZone": { "type": "string", "nullable": true }, "priceType": { "$ref": "#/components/schemas/TariffPriceType" }, "type": { "$ref": "#/components/schemas/TariffType" }, "variablePriceComponents": { "type": "array", "items": { "$ref": "#/components/schemas/VariablePriceComponent" }, "nullable": true } }, "additionalProperties": false }, "EmailConsentCapabilityData": { "type": "object", "properties": { "consentGiven": { "type": "boolean" } }, "additionalProperties": false }, "EmailConsentCapabilityUpdateData": { "type": "object", "additionalProperties": false }, "EnergyDataConsolidatedIntervalDto": { "type": "object", "properties": { "date": { "type": "string", "nullable": true }, "gridConsumptionWh": { "type": "number", "format": "double" }, "gridFeedInWh": { "type": "number", "format": "double" }, "productionWh": { "type": "number", "format": "double", "nullable": true }, "batteryChargedWh": { "type": "number", "format": "double" }, "batteryDischargedWh": { "type": "number", "format": "double" }, "costs": { "$ref": "#/components/schemas/EnergyDataCostsDto" } }, "additionalProperties": false, "description": "Day or sub-period aggregate with energy totals and costs. Values are watt-hours (Wh)." }, "EnergyDataConsolidatedResponseDto": { "type": "object", "properties": { "date": { "type": "string", "nullable": true }, "period": { "$ref": "#/components/schemas/EnergyDataPeriod" }, "summary": { "$ref": "#/components/schemas/EnergyDataSummaryDto" }, "consolidatedIntervals": { "type": "array", "items": { "$ref": "#/components/schemas/EnergyDataConsolidatedIntervalDto" }, "nullable": true }, "devices": { "type": "array", "items": { "$ref": "#/components/schemas/EnergyDataDeviceDto" }, "nullable": true } }, "additionalProperties": false }, "EnergyDataCostsDto": { "type": "object", "properties": { "gridConsumptionCosts": { "type": "number", "format": "double" }, "gridFeedInRevenue": { "type": "number", "format": "double" }, "totalSavings": { "type": "number", "format": "double" }, "pvSavings": { "type": "number", "format": "double" } }, "additionalProperties": false }, "EnergyDataDailyIntervalDto": { "type": "object", "properties": { "timestamp": { "type": "string", "format": "date-time" }, "gridConsumptionWatt": { "type": "integer", "format": "int32" }, "productionWatt": { "type": "integer", "format": "int32", "nullable": true }, "batteryStateOfChargePercent": { "type": "integer", "format": "int32", "nullable": true }, "batteryChargingWatt": { "type": "integer", "format": "int32", "nullable": true }, "batteryDischargingWatt": { "type": "integer", "format": "int32", "nullable": true }, "devices": { "type": "array", "items": { "$ref": "#/components/schemas/EnergyDataDeviceIntervalDto" }, "nullable": true } }, "additionalProperties": false, "description": "5-minute resolution interval with Watt values and per-device consumption." }, "EnergyDataDailyResponseDto": { "type": "object", "properties": { "date": { "type": "string", "nullable": true }, "summary": { "$ref": "#/components/schemas/EnergyDataSummaryDto" }, "intervals": { "type": "array", "items": { "$ref": "#/components/schemas/EnergyDataDailyIntervalDto" }, "nullable": true }, "devices": { "type": "array", "items": { "$ref": "#/components/schemas/EnergyDataDeviceDto" }, "nullable": true } }, "additionalProperties": false }, "EnergyDataDeviceActivityDto": { "type": "object", "properties": { "startedAt": { "type": "string", "format": "date-time" }, "endedAt": { "type": "string", "format": "date-time" }, "consumptionWh": { "type": "number", "format": "double" }, "durationMinutes": { "type": "integer", "format": "int32" }, "costs": { "type": "number", "format": "double", "nullable": true } }, "additionalProperties": false, "description": "Device activity window." }, "EnergyDataDeviceDto": { "type": "object", "properties": { "deviceId": { "type": "string", "format": "uuid" }, "name": { "type": "string", "nullable": true }, "category": { "type": "string", "nullable": true }, "totalConsumptionWh": { "type": "number", "format": "double" }, "costs": { "type": "number", "format": "double", "nullable": true }, "savings": { "type": "number", "format": "double", "nullable": true }, "activities": { "type": "array", "items": { "$ref": "#/components/schemas/EnergyDataDeviceActivityDto" }, "nullable": true } }, "additionalProperties": false }, "EnergyDataDeviceIntervalDto": { "type": "object", "properties": { "deviceId": { "type": "string", "format": "uuid" }, "name": { "type": "string", "nullable": true }, "category": { "type": "string", "nullable": true }, "consumptionWh": { "type": "number", "format": "double" } }, "additionalProperties": false }, "EnergyDataPeriod": { "enum": [ "weekly", "monthly", "yearly" ], "type": "string" }, "EnergyDataSummaryDto": { "type": "object", "properties": { "gridConsumptionWh": { "type": "number", "format": "double" }, "gridFeedInWh": { "type": "number", "format": "double" }, "productionWh": { "type": "number", "format": "double", "nullable": true }, "batteryChargedWh": { "type": "number", "format": "double" }, "batteryDischargedWh": { "type": "number", "format": "double" }, "costs": { "$ref": "#/components/schemas/EnergyDataCostsDto" } }, "additionalProperties": false, "description": "Aggregated energy totals for a day or consolidated period. Values are watt-hours (Wh)." }, "EnergyForecastResponseDto": { "type": "object", "properties": { "createdAtUtc": { "type": "string", "description": "When the forecast was computed, in UTC.", "format": "date-time" }, "entries": { "type": "array", "items": { "$ref": "#/components/schemas/EnergyForecastSlotDto" }, "description": "The quarter-hourly forecast slots, ordered by time.", "nullable": true } }, "additionalProperties": false, "description": "Energy forecast for a user's home: a forward-looking, quarter-hourly series of price, consumption,\nPV production, battery state and grid flows. The series covers the rest of today and tomorrow\n(up to the forecast horizon). All timestamps are UTC." }, "EnergyForecastSlotDto": { "type": "object", "properties": { "time": { "type": "string", "description": "Start of the 15-minute slot, in UTC.", "format": "date-time" }, "price": { "type": "number", "description": "End-customer buy price for the slot in ct/kWh; null when no price is available.", "format": "double", "nullable": true }, "stockPrice": { "type": "number", "description": "Raw exchange (spot) price for the slot in ct/kWh; null when unavailable.", "format": "double", "nullable": true }, "houseConsumptionKwh": { "type": "number", "description": "Forecasted household consumption for the slot, in kWh.", "format": "double" }, "solarProductionKwh": { "type": "number", "description": "Forecasted PV production for the slot, in kWh.", "format": "double" }, "soc": { "type": "integer", "description": "Forecasted battery state of charge at the slot, in percent (0-100); null when the home has no battery.", "format": "int32", "nullable": true }, "gridConsumptionKwh": { "type": "number", "description": "Forecasted grid import for the slot, in kWh.", "format": "double" }, "gridFeedInKwh": { "type": "number", "description": "Forecasted grid feed-in for the slot, in kWh.", "format": "double" } }, "additionalProperties": false, "description": "A single quarter-hourly slot of the home's energy forecast. All timestamps are UTC." }, "EnergyPriceResolution": { "enum": [ "hourly", "quarterHourly" ], "type": "string", "description": "Slot resolution for the energy-prices endpoint. Serialized as camelCase strings\n(`hourly`, `quarterHourly`); query-string binding matches names case-insensitively." }, "EnergyPriceSlotDto": { "type": "object", "properties": { "time": { "type": "string", "description": "UTC timestamp at the start of the slot.", "format": "date-time" }, "stockPrice": { "type": "number", "description": "Raw energy stock market price (e.g. EPEX Spot), in ct/kWh.\nFor users on a flatline betting zone this carries the default betting zone's stock price\nso charts can still render a meaningful market signal.", "format": "double", "nullable": true } }, "additionalProperties": false, "description": "Energy stock price for a single hourly or quarter-hourly time slot." }, "FlowOptionDto": { "type": "object", "properties": { "name": { "type": "string", "nullable": true }, "data": { "type": "string", "nullable": true } }, "additionalProperties": false }, "GetVendorConnectionDto": { "type": "object", "properties": { "connectionId": { "type": "string", "nullable": true }, "vendorId": { "type": "string", "format": "uuid" }, "flowOptions": { "type": "array", "items": { "$ref": "#/components/schemas/FlowOptionDto" }, "nullable": true } }, "additionalProperties": false }, "GetVendorConnectionDtoPaginatedResult": { "required": [ "data" ], "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/GetVendorConnectionDto" }, "nullable": true }, "cursor": { "type": "string", "nullable": true }, "size": { "type": "integer", "format": "int32", "nullable": true }, "total": { "type": "integer", "format": "int32", "nullable": true }, "hasMoreResults": { "type": "boolean", "nullable": true } }, "additionalProperties": false }, "HeatingControlState": { "enum": [ "normal", "sgReady", "powerControl" ], "type": "string" }, "HeatingState": { "enum": [ "idle", "running" ], "type": "string" }, "HeatingType": { "enum": [ "heating", "cooling", "warmWater" ], "type": "string" }, "HomeLocationCapabilityData": { "type": "object", "properties": { "city": { "type": "string", "nullable": true }, "postalCode": { "type": "string", "nullable": true }, "street": { "type": "string", "nullable": true }, "latitude": { "type": "number", "format": "double" }, "longitude": { "type": "number", "format": "double" } }, "additionalProperties": false }, "HourMinute": { "type": "object", "properties": { "hour": { "type": "integer", "format": "int32" }, "minute": { "type": "integer", "format": "int32" }, "isEndOfDay": { "type": "boolean" } }, "additionalProperties": false }, "HourlyConsumptionEntry": { "type": "object", "properties": { "timestamp": { "type": "string", "format": "date-time" }, "total": { "type": "number", "format": "double" }, "solar": { "type": "number", "format": "double" }, "price": { "type": "number", "format": "double" }, "normal": { "type": "number", "format": "double" } }, "additionalProperties": false }, "ICapabilityData": { "type": "object", "additionalProperties": false }, "InstallCertificateCapabilityData": { "type": "object", "properties": { "url": { "type": "string", "nullable": true } }, "additionalProperties": false }, "InstallCertificateCapabilityUpdateData": { "type": "object", "additionalProperties": false }, "InterceptedUrl": { "required": [ "url" ], "type": "object", "properties": { "url": { "type": "string", "nullable": true } }, "additionalProperties": false }, "InterventionDto": { "type": "object", "properties": { "id": { "type": "string", "nullable": true } }, "additionalProperties": false }, "IosModelAppUrlCapabilityData": { "type": "object", "properties": { "url": { "type": "string", "nullable": true } }, "additionalProperties": false }, "IosModelAppUrlCapabilityUpdateData": { "type": "object", "additionalProperties": false }, "IsMainDeviceCapabilityData": { "type": "object", "properties": { "isMainDevice": { "type": "boolean" } }, "additionalProperties": false }, "LinkToWallboxCapabilityData": { "type": "object", "properties": { "linked": { "type": "boolean" } }, "additionalProperties": false }, "LinkToWallboxCapabilityUpdateData": { "type": "object", "properties": { "linked": { "type": "boolean" } }, "additionalProperties": false }, "LocationDetectionCapabilityData": { "type": "object", "properties": { "latitude": { "type": "number", "format": "double", "nullable": true }, "longitude": { "type": "number", "format": "double", "nullable": true }, "atHome": { "type": "boolean" } }, "additionalProperties": false }, "MaxAmperageCapabilityData": { "type": "object", "properties": { "maxAmperage": { "type": "integer", "format": "int32" } }, "additionalProperties": false }, "MaxAmperageCapabilityUpdateData": { "type": "object", "properties": { "maxAmperage": { "type": "integer", "format": "int32" } }, "additionalProperties": false }, "MeasurementPurpose": { "enum": [ "gridMeter", "pvProduction" ], "type": "string" }, "MeasurementPurposeCapabilityData": { "type": "object", "properties": { "purpose": { "$ref": "#/components/schemas/MeasurementPurpose" } }, "additionalProperties": false }, "MeasurementPurposeCapabilityUpdateData": { "type": "object", "properties": { "purpose": { "$ref": "#/components/schemas/MeasurementPurpose" } }, "additionalProperties": false }, "MinMaxAmperageCapabilityData": { "type": "object", "properties": { "minAmperage": { "type": "integer", "format": "int32" }, "maxAmperage": { "type": "integer", "format": "int32" } }, "additionalProperties": false }, "MinMaxAmperageCapabilityUpdateData": { "type": "object", "properties": { "minAmperage": { "type": "integer", "format": "int32" }, "maxAmperage": { "type": "integer", "format": "int32" } }, "additionalProperties": false }, "ModelDto": { "required": [ "category", "id", "maturity", "name", "vendor" ], "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "nullable": true }, "category": { "$ref": "#/components/schemas/DeviceCategory" }, "vendor": { "$ref": "#/components/schemas/DeviceVendorDto" }, "maturity": { "$ref": "#/components/schemas/VendorMaturity" } }, "additionalProperties": false }, "OnOffCapabilityDesiredState": { "enum": [ "off", "on" ], "type": "string" }, "OnOffCapabilityReadData": { "type": "object", "properties": { "state": { "$ref": "#/components/schemas/OnOffCapabilityReadState" } }, "additionalProperties": false }, "OnOffCapabilityReadState": { "enum": [ "off", "on" ], "type": "string" }, "OnOffCapabilityUpdateData": { "type": "object", "properties": { "state": { "$ref": "#/components/schemas/OnOffCapabilityDesiredState" } }, "additionalProperties": false }, "OnOffSmartCapabilityData": { "type": "object", "properties": { "mode": { "type": "string", "nullable": true } }, "additionalProperties": false }, "OperationMode": { "enum": [ "unknown", "normal", "idle", "forceCharge" ], "type": "string" }, "OperationModeCapabilityData": { "type": "object", "properties": { "mode": { "$ref": "#/components/schemas/OperationMode" } }, "additionalProperties": false }, "OperationModeCapabilityUpdateData": { "type": "object", "properties": { "mode": { "$ref": "#/components/schemas/OperationMode" } }, "additionalProperties": false }, "PartnerErrorCode": { "enum": [ "validationError", "capabilityNotSupported", "interventionFound", "internalError", "invalidSubscription", "invalidTimezone", "invalidVatNumber", "invalidBillingAddress", "duplicateDevice", "accessDenied", "deviceTypeNotSupported", "deviceCreationFailed", "deviceNotFound", "deviceCategoryNotFound", "measurementPurposeAlreadySet", "authenticationFailed", "invalidCredentials", "rateLimitExceeded", "personalDetailsIncomplete", "subscriptionAlreadyExists", "invalidReferralCode", "invalidCouponCode", "invalidPaymentProvider", "paymentRequired", "cannotRedeemCoupon", "missingPaymentNotification", "missingCouponPrefix", "chargingLimitReached", "carUnavailable", "carLinkedToWallbox", "teslaSdkRequired", "tariffSavingsNotCreated", "tariffOverlapping", "tariffInPastNotAllowed", "flexFeesNotFound", "contractCannotBeDeleted", "noActiveContract", "tariffBettingZoneRequired", "tariffPriceRequired", "tariffInvalidDateRange", "tariffNotFound", "tariffContractAttached", "tariffTypeImmutable", "tariffPriceTypeImmutable", "scheduleOverlapping", "invalidScheduleExecutionTime", "invalidSchedulePayload", "missingHome", "clientNotFound", "clientExternalUserNotFound", "emailConsentRequired", "pendingOcppWallboxExists", "ocppConnectionNotFound", "connectionNotFound", "ocppConnectionHasDevices", "formConnectionHasDevices", "pushConnectionHasDevices", "vendorNotFound", "vendorFlowNotSupported", "unsupportedOnboardingFlow", "chargingLocationNotVerified", "deleteConnectionNotPossible", "vendorOnboardingFlowMissing", "vendorCommunicationError", "homeLocationRequired", "solarProducerRequired", "solarForecastDisabled", "solarForecastNotAvailable", "vendorPermissionDenied" ], "type": "string", "description": "Partner-facing error codes for the Connect B2B API.\nThese codes are mapped from internal CleverPV error codes and provide\nstable identifiers that partners can use for error handling." }, "PhaseMode": { "enum": [ "singlePhase", "twoPhases", "threePhases", "automatic3Phases", "automatic2Phases" ], "type": "string" }, "PhaseModeCapabilityData": { "type": "object", "properties": { "phaseMode": { "$ref": "#/components/schemas/PhaseMode" } }, "additionalProperties": false }, "PhaseModeCapabilityUpdateData": { "type": "object", "properties": { "phaseMode": { "$ref": "#/components/schemas/PhaseMode" } }, "additionalProperties": false }, "PhaseSwitchingCapabilityData": { "type": "object", "properties": { "supported": { "type": "boolean" } }, "additionalProperties": false }, "PlugAndChargeCapabilityData": { "type": "object", "properties": { "enabled": { "type": "boolean" } }, "additionalProperties": false }, "PlugAndChargeCapabilityUpdateData": { "type": "object", "properties": { "enabled": { "type": "boolean" } }, "additionalProperties": false }, "PowerMode": { "enum": [ "threePointSix", "sevenPointTwo", "eleven", "twentytwo" ], "type": "string" }, "PowerPlanCapabilityData": { "type": "object", "properties": { "name": { "type": "string", "nullable": true }, "state": { "$ref": "#/components/schemas/PowerPlanState" }, "startTime": { "type": "string", "format": "date-time" }, "earliestStart": { "type": "string", "format": "date-time" }, "latestStart": { "type": "string", "format": "date-time" }, "duration": { "type": "string", "format": "date-span" }, "isStoppable": { "type": "boolean" }, "phases": { "type": "array", "items": { "$ref": "#/components/schemas/PowerPlanPhase" }, "nullable": true }, "lastUpdate": { "type": "string", "format": "date-time", "nullable": true } }, "additionalProperties": false }, "PowerPlanPhase": { "type": "object", "properties": { "index": { "type": "integer", "format": "int32" }, "name": { "type": "string", "nullable": true }, "duration": { "type": "string", "format": "date-span" }, "power": { "type": "number", "format": "double" } }, "additionalProperties": false }, "PowerPlanState": { "enum": [ "invalid", "scheduled", "running", "completed" ], "type": "string" }, "PowerStorageState": { "enum": [ "idle", "charging", "disabled", "discharging" ], "type": "string" }, "PreferredOperatingMode": { "enum": [ "manualControl", "smartControl", "monitoring" ], "type": "string" }, "PreferredOperatingModeCapabilityData": { "type": "object", "properties": { "mode": { "$ref": "#/components/schemas/PreferredOperatingMode" } }, "additionalProperties": false }, "PriceComponentType": { "enum": [ "gridFee" ], "type": "string" }, "PriceControlCapabilityData": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "threshold": { "type": "number", "format": "double" } }, "additionalProperties": false }, "PriceOptimizationCapabilityData": { "type": "object", "properties": { "enabled": { "type": "boolean" } }, "additionalProperties": false }, "PriceRule": { "required": [ "daysOfWeek", "timeWindows" ], "type": "object", "properties": { "daysOfWeek": { "type": "array", "items": { "$ref": "#/components/schemas/DayOfWeek" }, "nullable": true }, "timeWindows": { "type": "array", "items": { "$ref": "#/components/schemas/TimeWindow" }, "nullable": true } }, "additionalProperties": false }, "PriorityCapabilityData": { "type": "object", "properties": { "priority": { "type": "integer", "format": "int32" } }, "additionalProperties": false }, "ProblemDetails": { "type": "object", "properties": { "type": { "type": "string", "nullable": true }, "title": { "type": "string", "nullable": true }, "status": { "type": "integer", "format": "int32", "nullable": true }, "detail": { "type": "string", "nullable": true }, "instance": { "type": "string", "nullable": true } }, "additionalProperties": { } }, "ProblemType": { "enum": [ "unauthorized", "validationFailed", "accessDenied", "deviceNotFound", "capabilityNotSupported", "conflict", "paymentRequired", "internalError", "clientNotFound", "clientExternalUserNotFound", "homeNotFound", "vendorNotFound", "onboardingFlowNotSupported", "connectionNotFound", "chargingLocationNotVerified", "tariffNotFound", "flexFeesNotFound", "unprocessableEntity", "vendorCommunicationError", "homeLocationRequired", "solarProducerRequired", "solarForecastDisabled", "solarForecastNotAvailable" ], "type": "string", "description": "Defines the standard problem types returned by the Connect API.\nValues are serialized as strings in JSON responses per RFC 7807." }, "PvControlCapabilityData": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "priority": { "type": "integer", "format": "int32" }, "ratedCapacity": { "type": "integer", "format": "int32" } }, "additionalProperties": false }, "RatedCapacityCapabilityData": { "type": "object", "properties": { "ratedCapacity": { "type": "integer", "format": "int32" } }, "additionalProperties": false }, "RenewSubscriptionCapabilityData": { "type": "object", "properties": { "id": { "type": "string", "nullable": true }, "expiresAt": { "type": "string", "format": "date-time", "nullable": true } }, "additionalProperties": false }, "ScheduleType": { "enum": [ "once", "repeat" ], "type": "string" }, "SetChargeLimitCapabilityData": { "type": "object", "properties": { "chargeLimit": { "type": "integer", "format": "int32" } }, "additionalProperties": false }, "SetChargeLimitCapabilityUpdateData": { "type": "object", "properties": { "chargeLimit": { "type": "integer", "format": "int32" } }, "additionalProperties": false }, "SetChargingPowerCapabilityData": { "type": "object", "properties": { "desiredAmperage": { "type": "integer", "format": "int32" } }, "additionalProperties": false }, "SetChargingPowerCapabilityUpdateData": { "type": "object", "properties": { "desiredAmperage": { "type": "integer", "format": "int32" } }, "additionalProperties": false }, "SetHomeLocationRequestDto": { "required": [ "latitude", "longitude" ], "type": "object", "properties": { "latitude": { "type": "number", "description": "Latitude in decimal degrees (-90 to 90).", "format": "double" }, "longitude": { "type": "number", "description": "Longitude in decimal degrees (-180 to 180).", "format": "double" }, "street": { "type": "string", "description": "Optional street and house number.", "nullable": true }, "postalCode": { "type": "string", "description": "Optional postal code.", "nullable": true }, "city": { "type": "string", "description": "Optional city.", "nullable": true } }, "additionalProperties": false, "description": "Request to set the geographic location of a user's home.\nCoordinates are required and drive the solar forecast; address fields are optional and informational." }, "SetPercentagePowerCapabilityData": { "type": "object", "properties": { "percentage": { "type": "number", "format": "double" } }, "additionalProperties": false }, "SetScopesCapabilityData": { "type": "object", "properties": { "url": { "type": "string", "nullable": true } }, "additionalProperties": false }, "SetScopesCapabilityUpdateData": { "type": "object", "additionalProperties": false }, "SetSolarForecastEnabledRequestDto": { "required": [ "enabled" ], "type": "object", "properties": { "enabled": { "type": "boolean", "description": "True to enable (and regenerate) the forecast; false to disable it." } }, "additionalProperties": false, "description": "Request to enable or disable the solar forecast for a user's home.\nEnabling also restarts forecast generation from the stored home location." }, "SleepModeCapabilityData": { "type": "object", "properties": { "isSleeping": { "type": "boolean" } }, "additionalProperties": false }, "SmartControlReason": { "enum": [ "none", "solar", "price", "soc" ], "type": "string" }, "SmartModeCapabilityData": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "reason": { "$ref": "#/components/schemas/SmartControlReason" } }, "additionalProperties": false }, "SocPlanCapabilityData": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "minControlSoc": { "type": "integer", "format": "int32" }, "maxControlSoc": { "type": "integer", "format": "int32", "nullable": true } }, "additionalProperties": false }, "SocPlanCapabilityUpdateData": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "minControlSoc": { "type": "integer", "format": "int32" }, "maxControlSoc": { "type": "integer", "format": "int32", "nullable": true } }, "additionalProperties": false }, "SolarForecastPointDto": { "type": "object", "properties": { "time": { "type": "string", "description": "The hour the value applies to, in UTC.", "format": "date-time" }, "value": { "type": "number", "description": "The measured or forecasted PV production for the hour, as produced by the clever-PV solar forecast.\nNull when no value is available for the hour.", "format": "double", "nullable": true } }, "additionalProperties": false, "description": "A single hourly point of a solar production / forecast series." }, "SolarForecastResponseDto": { "type": "object", "properties": { "enabled": { "type": "boolean", "description": "Whether the solar forecast is currently enabled for the home." }, "trainingDays": { "type": "integer", "description": "Number of days of historic data the model has been trained on.", "format": "int32" }, "maxTrainingDays": { "type": "integer", "description": "Maximum number of training days the model uses.", "format": "int32" }, "sunrise": { "type": "string", "description": "Sunrise time for the home's location, in UTC.", "format": "date-time" }, "sunset": { "type": "string", "description": "Sunset time for the home's location, in UTC.", "format": "date-time" }, "meanError": { "type": "number", "description": "Mean relative forecast error (0-1).", "format": "double" }, "production": { "type": "array", "items": { "$ref": "#/components/schemas/SolarForecastPointDto" }, "description": "Actual measured production so far today, by hour.", "nullable": true }, "forecastToday": { "type": "array", "items": { "$ref": "#/components/schemas/SolarForecastPointDto" }, "description": "Forecasted production for today, by hour.", "nullable": true }, "forecastTomorrow": { "type": "array", "items": { "$ref": "#/components/schemas/SolarForecastPointDto" }, "description": "Forecasted production for tomorrow, by hour.", "nullable": true }, "weatherToday": { "type": "array", "items": { "$ref": "#/components/schemas/WeatherForecastPointDto" }, "description": "Weather data underlying today's forecast, by hour.", "nullable": true }, "weatherTomorrow": { "type": "array", "items": { "$ref": "#/components/schemas/WeatherForecastPointDto" }, "description": "Weather data underlying tomorrow's forecast, by hour.", "nullable": true } }, "additionalProperties": false, "description": "Solar production forecast for a user's home. All timestamps are UTC." }, "SolarOptimizationCapabilityData": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "ratedCapacity": { "type": "integer", "format": "int32" }, "sunShare": { "type": "integer", "format": "int32", "nullable": true } }, "additionalProperties": false }, "SolarOptimizationCapabilityUpdateData": { "type": "object", "properties": { "enabled": { "type": "boolean" } }, "additionalProperties": false }, "SunShareCapabilityData": { "type": "object", "properties": { "ratedCapacity": { "type": "integer", "format": "int32" }, "sunShare": { "type": "integer", "format": "int32" } }, "additionalProperties": false }, "SunShareCapabilityUpdateData": { "type": "object", "properties": { "sunShare": { "type": "integer", "format": "int32" } }, "additionalProperties": false }, "SupportTeslaVehicleCommandProtocolCapabilityData": { "type": "object", "properties": { "supported": { "type": "boolean" } }, "additionalProperties": false }, "SwitchDelayCapabilityData": { "type": "object", "properties": { "switchDelayOn": { "type": "integer", "format": "int32" }, "switchDelayOff": { "type": "integer", "format": "int32" } }, "additionalProperties": false }, "TargetChargingCapabilityData": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "targetControlLock": { "type": "string", "format": "date-time", "nullable": true }, "currentRuntime": { "type": "number", "format": "double" }, "hasReachedTarget": { "type": "boolean" }, "lastTargetControl": { "type": "string", "format": "date-time", "nullable": true }, "executionTime": { "type": "string", "format": "date-span", "nullable": true }, "targetWh": { "type": "integer", "format": "int32", "nullable": true }, "targetSoC": { "type": "integer", "format": "int32", "nullable": true }, "unitType": { "$ref": "#/components/schemas/TargetChargingUnitType" }, "targetDate": { "type": "string", "format": "date-time", "nullable": true }, "weekDays": { "type": "array", "items": { "$ref": "#/components/schemas/DayOfWeek" }, "nullable": true }, "type": { "$ref": "#/components/schemas/ScheduleType" }, "useSmartChargePlan": { "type": "boolean", "nullable": true }, "sendPushNotification": { "type": "boolean", "nullable": true }, "enableTargetControlLimit": { "type": "boolean", "nullable": true }, "chargedWh": { "type": "number", "format": "double", "nullable": true }, "consumption": { "type": "integer", "format": "int32", "nullable": true }, "batteryCapacity": { "type": "integer", "format": "int32", "nullable": true }, "currentSoC": { "type": "integer", "format": "int32", "nullable": true }, "socLimit": { "type": "integer", "format": "int32", "nullable": true }, "lastUpdate": { "type": "string", "format": "date-time", "nullable": true } }, "additionalProperties": false }, "TargetChargingCapabilityUpdateData": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "executionTime": { "type": "string", "format": "date-span", "nullable": true }, "targetWh": { "type": "integer", "format": "int32", "nullable": true }, "targetSoC": { "type": "integer", "format": "int32", "nullable": true }, "unitType": { "$ref": "#/components/schemas/TargetChargingUnitType" }, "weekDays": { "type": "array", "items": { "$ref": "#/components/schemas/DayOfWeek" }, "nullable": true }, "type": { "$ref": "#/components/schemas/ScheduleType" }, "useSmartChargePlan": { "type": "boolean", "nullable": true }, "sendPushNotification": { "type": "boolean", "nullable": true }, "enableTargetControlLimit": { "type": "boolean", "nullable": true } }, "additionalProperties": false }, "TargetChargingUnitType": { "enum": [ "kwh", "percent" ], "type": "string" }, "TargetControlCapabilityData": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "targetControlLock": { "type": "string", "format": "date-time", "nullable": true }, "currentRuntime": { "type": "number", "format": "double" }, "hasReachedTarget": { "type": "boolean" }, "lastTargetControl": { "type": "string", "format": "date-time", "nullable": true }, "startTime": { "type": "string", "format": "date-span", "nullable": true }, "endTime": { "type": "string", "format": "date-span", "nullable": true }, "targetRuntime": { "type": "string", "format": "date-span", "nullable": true }, "startDate": { "type": "string", "format": "date-time", "nullable": true }, "endDate": { "type": "string", "format": "date-time", "nullable": true }, "weekDays": { "type": "array", "items": { "$ref": "#/components/schemas/DayOfWeek" }, "nullable": true }, "type": { "$ref": "#/components/schemas/ScheduleType" }, "useSmartChargePlan": { "type": "boolean", "nullable": true }, "sendPushNotification": { "type": "boolean", "nullable": true }, "enableTargetControlLimit": { "type": "boolean", "nullable": true }, "lastUpdate": { "type": "string", "format": "date-time", "nullable": true } }, "additionalProperties": false }, "TargetControlCapabilityUpdateData": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "startTime": { "type": "string", "format": "date-span", "nullable": true }, "endTime": { "type": "string", "format": "date-span", "nullable": true }, "targetRuntime": { "type": "string", "format": "date-span", "nullable": true }, "weekDays": { "type": "array", "items": { "$ref": "#/components/schemas/DayOfWeek" }, "nullable": true }, "type": { "$ref": "#/components/schemas/ScheduleType" }, "useSmartChargePlan": { "type": "boolean", "nullable": true }, "sendPushNotification": { "type": "boolean", "nullable": true }, "enableTargetControlLimit": { "type": "boolean", "nullable": true } }, "additionalProperties": false }, "TariffPriceType": { "enum": [ "fix", "dynamic" ], "type": "string" }, "TariffType": { "enum": [ "buy", "sell", "referenceBuy", "referenceSell" ], "type": "string" }, "TemperatureValueType": { "enum": [ "actual", "target", "min" ], "type": "string" }, "TemperatureZoneCapabilityData": { "type": "object", "properties": { "temperature": { "type": "number", "format": "double" }, "zone": { "$ref": "#/components/schemas/TemperatureZoneCategory" }, "sensorId": { "type": "string", "nullable": true }, "valueType": { "$ref": "#/components/schemas/TemperatureValueType" }, "lastUpdate": { "type": "string", "format": "date-time", "nullable": true } }, "additionalProperties": false }, "TemperatureZoneCategory": { "enum": [ "unknown", "warmWater", "heatingCircuit", "outdoor", "room" ], "type": "string" }, "TemperatureZonesCapabilityData": { "type": "object", "properties": { "zones": { "type": "array", "items": { "$ref": "#/components/schemas/TemperatureZoneCapabilityData" }, "nullable": true } }, "additionalProperties": false }, "TimeControlCapabilityData": { "type": "object", "properties": { "nextSchedule": { "$ref": "#/components/schemas/DeviceScheduleInfo" }, "schedules": { "type": "array", "items": { "$ref": "#/components/schemas/DeviceScheduleInfo" }, "nullable": true } }, "additionalProperties": false }, "TimeControlCapabilityUpdateData": { "type": "object", "properties": { "schedules": { "type": "array", "items": { "$ref": "#/components/schemas/TimeControlScheduleItem" }, "nullable": true } }, "additionalProperties": false }, "TimeControlScheduleItem": { "type": "object", "properties": { "id": { "type": "string", "nullable": true }, "type": { "$ref": "#/components/schemas/ScheduleType" }, "executionTime": { "type": "string", "format": "date-span" }, "action": { "$ref": "#/components/schemas/DeviceControlAction" }, "actionDetails": { "nullable": true }, "weekDays": { "type": "array", "items": { "$ref": "#/components/schemas/DayOfWeek" }, "nullable": true }, "isActive": { "type": "boolean" }, "description": { "type": "string", "nullable": true }, "sendPushNotification": { "type": "boolean" } }, "additionalProperties": false }, "TimeWindow": { "required": [ "end", "priceCtPerKwh", "start" ], "type": "object", "properties": { "start": { "$ref": "#/components/schemas/HourMinute" }, "end": { "$ref": "#/components/schemas/HourMinute" }, "priceCtPerKwh": { "type": "number", "format": "double" } }, "additionalProperties": false }, "UpdateDeviceCommandDto": { "required": [ "name" ], "type": "object", "properties": { "name": { "maxLength": 200, "minLength": 1, "type": "string" } }, "additionalProperties": false }, "UpdateElectricityTariffRequestDto": { "required": [ "from", "priceType", "type" ], "type": "object", "properties": { "from": { "type": "string", "format": "date-time" }, "to": { "type": "string", "format": "date-time", "nullable": true }, "price": { "type": "number", "format": "double", "nullable": true }, "baseFee": { "type": "number", "format": "double", "nullable": true }, "gridFee": { "type": "number", "format": "double", "nullable": true }, "bettingZone": { "type": "string", "nullable": true }, "priceType": { "$ref": "#/components/schemas/TariffPriceType" }, "type": { "$ref": "#/components/schemas/TariffType" }, "variablePriceComponents": { "type": "array", "items": { "$ref": "#/components/schemas/VariablePriceComponent" }, "nullable": true } }, "additionalProperties": false }, "ValidationIssue": { "required": [ "field", "message" ], "type": "object", "properties": { "field": { "type": "string", "description": "The field or property path that failed validation (e.g., \"deviceId\", \"settings.maxPower\").", "nullable": true }, "message": { "type": "string", "description": "Human-readable description of why the validation failed.", "nullable": true }, "code": { "type": "string", "description": "Optional error code specific to this validation issue.", "nullable": true } }, "additionalProperties": false, "description": "Represents a single validation issue in a failed request." }, "VariablePriceComponent": { "required": [ "rules", "type" ], "type": "object", "properties": { "type": { "$ref": "#/components/schemas/PriceComponentType" }, "rules": { "type": "array", "items": { "$ref": "#/components/schemas/PriceRule" }, "nullable": true } }, "additionalProperties": false }, "VendorConnectionOptionsDto": { "type": "object", "properties": { "type": { "$ref": "#/components/schemas/DeviceOnboardingFormFieldType" }, "data": { "type": "object", "additionalProperties": { "nullable": true }, "nullable": true } }, "additionalProperties": false }, "VendorConnectionOptionsDtoPaginatedResult": { "required": [ "data" ], "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/VendorConnectionOptionsDto" }, "nullable": true }, "cursor": { "type": "string", "nullable": true }, "size": { "type": "integer", "format": "int32", "nullable": true }, "total": { "type": "integer", "format": "int32", "nullable": true }, "hasMoreResults": { "type": "boolean", "nullable": true } }, "additionalProperties": false }, "VendorConnectionOptionsResponse": { "type": "object", "properties": { "flow": { "$ref": "#/components/schemas/DeviceOnboardingFlow" }, "formOptions": { "type": "array", "items": { "$ref": "#/components/schemas/VendorConnectionOptionsDto" }, "nullable": true }, "docUrl": { "type": "string", "nullable": true } }, "additionalProperties": false }, "VendorDeviceDto": { "required": [ "available", "externalId", "model", "name", "text" ], "type": "object", "properties": { "name": { "type": "string", "nullable": true }, "text": { "type": "string", "nullable": true }, "model": { "$ref": "#/components/schemas/VendorModelDto" }, "externalId": { "type": "string", "nullable": true }, "available": { "type": "boolean" }, "connectionId": { "type": "string", "format": "uuid", "nullable": true } }, "additionalProperties": false }, "VendorDeviceDtoPaginatedResult": { "required": [ "data" ], "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/VendorDeviceDto" }, "nullable": true }, "cursor": { "type": "string", "nullable": true }, "size": { "type": "integer", "format": "int32", "nullable": true }, "total": { "type": "integer", "format": "int32", "nullable": true }, "hasMoreResults": { "type": "boolean", "nullable": true } }, "additionalProperties": false }, "VendorDto": { "required": [ "flow", "id", "identifier", "models", "name" ], "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "identifier": { "type": "string", "nullable": true }, "name": { "type": "string", "nullable": true }, "flow": { "$ref": "#/components/schemas/DeviceOnboardingFlow" }, "models": { "type": "array", "items": { "$ref": "#/components/schemas/VendorModelDto" }, "nullable": true } }, "additionalProperties": false, "description": "Public catalog representation of a supported vendor, including the supported device models.\nIntentionally omits the internal release maturity exposed by the application-layer DTO." }, "VendorDtoPaginatedResult": { "required": [ "data" ], "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/VendorDto" }, "nullable": true }, "cursor": { "type": "string", "nullable": true }, "size": { "type": "integer", "format": "int32", "nullable": true }, "total": { "type": "integer", "format": "int32", "nullable": true }, "hasMoreResults": { "type": "boolean", "nullable": true } }, "additionalProperties": false }, "VendorMaturity": { "enum": [ "live", "beta", "planned" ], "type": "string" }, "VendorModelDto": { "required": [ "category", "id", "name" ], "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "nullable": true }, "category": { "$ref": "#/components/schemas/DeviceCategory" } }, "additionalProperties": false }, "VendorsDto": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "nullable": true }, "identifier": { "type": "string", "nullable": true }, "flow": { "$ref": "#/components/schemas/DeviceOnboardingFlow" }, "maturity": { "$ref": "#/components/schemas/VendorMaturity" } }, "additionalProperties": false }, "VendorsDtoPaginatedResult": { "required": [ "data" ], "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/VendorsDto" }, "nullable": true }, "cursor": { "type": "string", "nullable": true }, "size": { "type": "integer", "format": "int32", "nullable": true }, "total": { "type": "integer", "format": "int32", "nullable": true }, "hasMoreResults": { "type": "boolean", "nullable": true } }, "additionalProperties": false }, "WeatherForecastPointDto": { "type": "object", "properties": { "time": { "type": "string", "description": "The hour the data applies to, in UTC.", "format": "date-time" }, "clouds": { "type": "integer", "description": "Cloud cover in percent (0-100).", "format": "int32" }, "weatherIconId": { "type": "integer", "description": "Provider weather icon identifier.", "format": "int32" }, "rain": { "type": "number", "description": "Rain volume for the hour in millimetres.", "format": "double" }, "snow": { "type": "number", "description": "Snow volume for the hour in millimetres.", "format": "double" } }, "additionalProperties": false, "description": "A single hourly weather data point underlying the solar forecast." }, "WebModelAppUrlCapabilityData": { "type": "object", "properties": { "url": { "type": "string", "nullable": true } }, "additionalProperties": false }, "WebModelAppUrlCapabilityUpdateData": { "type": "object", "additionalProperties": false } }, "securitySchemes": { "Bearer": { "type": "http", "description": "Please enter a valid JWT token", "scheme": "Bearer", "bearerFormat": "JWT" } } }, "security": [ { "Bearer": [ ] } ], "tags": [ { "name": "CleverPV.Connect.API" }, { "name": "Device 2.0" }, { "name": "Electricity Tariffs" }, { "name": "Energy Data" }, { "name": "Energy Prices" }, { "name": "Home" }, { "name": "Management" }, { "name": "PublicCatalog" }, { "name": "Vendors" } ] } ```