From a4c15698ee49c6de1b93b2892d03d15aaadfeb35 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 12:21:43 +0000 Subject: [PATCH 1/2] docs(portal-extensions): add prosumer & load-cycle examples Document the visualizationMetadata hook and typed data-retrieval responses, with end-to-end worked examples (prosumer feed-in/feed-out, industrial load-cycle min/average/max) based on the epilot example integration service and its setup scenarios. https://claude.ai/code/session_01NAGddUiJH2xfj6nAJqZie5 --- docs/apps/components/portal-extension.md | 197 +++++++++++++++++++++++ 1 file changed, 197 insertions(+) diff --git a/docs/apps/components/portal-extension.md b/docs/apps/components/portal-extension.md index a997cfc7..466aca23 100644 --- a/docs/apps/components/portal-extension.md +++ b/docs/apps/components/portal-extension.md @@ -318,6 +318,203 @@ These variables are automatically provided by epilot based on the portal user's - **Error Handling**: Ensure your API returns appropriate error responses that epilot can handle gracefully - **Time Zone Handling**: Take care when handling Time Zones and DSTs +#### Returning Typed Data + +The consumption hook may return more than one record per timestamp by adding a `type` field to each record. This lets a single chart break a value down into multiple series — for example a household that both draws from and feeds into the grid, or a meter billed on two tariffs. + +```json title="Typed consumption response (prosumer)" +{ + "consumptions": [ + { "timestamp": "2026-01-20T11:00:00.000+00:00", "type": "feed-in", "value": 3.21 }, + { "timestamp": "2026-01-20T11:00:00.000+00:00", "type": "feed-out", "value": 0.0 }, + { "timestamp": "2026-01-20T12:00:00.000+00:00", "type": "feed-in", "value": 3.84 }, + { "timestamp": "2026-01-20T12:00:00.000+00:00", "type": "feed-out", "value": 0.0 }, + { "timestamp": "2026-01-20T20:00:00.000+00:00", "type": "feed-in", "value": 0.0 }, + { "timestamp": "2026-01-20T20:00:00.000+00:00", "type": "feed-out", "value": 0.42 } + ] +} +``` + +The `type` values are free-form, but they only render meaningfully when the portal knows how to label, color and combine them. That information comes from the **Visualization Metadata** hook described below — each `type` returned by the data hook should match a `type_options[].id` returned by the metadata hook. + +### Visualization Metadata Hook + +The `visualizationMetadata` hook returns runtime metadata describing **how** a visualization should be rendered for the current portal context (which meter, contract, etc. the user is looking at). The portal invokes it _before_ the data hook, with the same context, so the shape of the chart can vary per meter or contract — different tariff models, available intervals, or history depth. + +A `visualizationMetadata` hook is looked up implicitly per extension (one per extension); a data-retrieval hook does not need to reference it explicitly. + +The metadata response has three optional fields: + +- **`type_options`**: the series advertised for this context. Each option's `id` matches the `type` field on the data hook records. Options also carry a localized `label`, an `aggregation_group`, a `statistical_method`, a `unit`, a Spark `color`, and a display `precision`. +- **`intervals`**: the intervals supported for this context (`PT15M`, `PT1H`, `P1D`, `P1M`). Prefer this over the now-deprecated `intervals` field on the data-retrieval hooks, so the supported intervals can vary per meter/contract. +- **`data_range`**: the earliest (`from`) and latest (`to`) timestamps for which data is available, used to bound the date picker. + +The `statistical_method` on each type both describes the aggregation already applied to that type's data and dictates the chart shape: + +- `sum` → **bar chart**. Same-`aggregation_group` types are stacked into one bar; different groups render side-by-side. +- `min` / `average` / `max` → **line chart**. Same-`aggregation_group` types render as an area band; different groups render as separate lines. + +Because the method is per-type, a single visualization can mix bar-shaped and line-shaped series. + +```json title="Visualization metadata hook" +{ + "id": "visualization_metadata", + "type": "visualizationMetadata", + "auth": { + "url": "{{Options.api_url}}/token", + "method": "GET", + "headers": { + "API-Key": "{{Options.api_key}}", + "UID": "{{Contact.customer_number}}" + }, + "cache": { + "key": "{{Options.api_key}}-{{Contact.customer_number}}", + "ttl": "3600" + } + }, + "call": { + "url": "{{Options.api_url}}/example/visualization/metadata", + "headers": { + "Authorization": "Bearer {{AuthResponse.data.data.token}}" + }, + "params": { + "setup": "{{Options.setup}}" + } + }, + "resolved": { + "data_path": "", + "error_message_path": "message" + } +} +``` + +:::note +`use_static_ips` is deprecated on all hook types — prefer `secure_proxy` (route requests through the ERP Integration secure proxy by setting `integration_id` and `use_case_slug`). The `resolved.dataPath` field has also been renamed to `resolved.data_path`; the old name still works but is deprecated. +::: + +### Worked Examples: epilot Example Integration + +The epilot **example integration** service is a reference backend that powers Dynamic Tariff and Consumption blocks against synthetic-but-realistic German energy data. It exposes the data-retrieval endpoints (`/example/price`, `/example/consumption`, `/example/cost`) and a `/example/visualization/metadata` endpoint, and accepts a `setup` query parameter that selects a predefined deployment scenario. Using the same `setup` across the metadata and data hooks keeps the advertised `type_options` aligned with the records the data hooks return. + +The endpoints also accept `from`, `to` and `interval` (and, for consumption/cost, an optional `multiplier` for B2B scenarios). The two scenarios below show the prosumer and load-cycle setups end-to-end. + +#### Prosumer (feed-in / feed-out) + +A household with a rooftop PV system both imports from and exports to the grid. The `prosumer` setup advertises two series — `feed-in` (surplus exported to the grid, peaks midday) and `feed-out` (drawn from the grid, mostly at night) — as separate groups so they render as distinct series. + +```json title="Visualization metadata response (setup=prosumer)" +{ + "type_options": [ + { "id": "feed-in", "label": { "en": "Feed-in", "de": "Einspeisung" }, "aggregation_group": "feed-in", "statistical_method": "sum", "unit": "kWh", "color": "green", "precision": 2 }, + { "id": "feed-out", "label": { "en": "Feed-out", "de": "Ausspeisung" }, "aggregation_group": "feed-out", "statistical_method": "sum", "unit": "kWh", "color": "blue", "precision": 2 } + ], + "intervals": ["PT15M", "PT1H", "P1D", "P1M"], + "data_range": { "from": "2024-06-08T00:00:00.000Z", "to": "2026-06-08T00:00:00.000Z" } +} +``` + +```json title="Consumption hook (setup=prosumer)" +{ + "id": "consumption", + "type": "consumptionDataRetrieval", + "name": { + "en": "Consumption", + "de": "Verbrauch" + }, + "auth": { + "url": "{{Options.api_url}}/token", + "method": "GET", + "headers": { + "API-Key": "{{Options.api_key}}", + "UID": "{{Contact.customer_number}}" + }, + "cache": { + "key": "{{Options.api_key}}-{{Contact.customer_number}}", + "ttl": "3600" + } + }, + "call": { + "url": "{{Options.api_url}}/example/consumption", + "headers": { + "Authorization": "Bearer {{AuthResponse.data.data.token}}" + }, + "params": { + "from": "{{Scope.from}}", + "to": "{{Scope.to}}", + "interval": "{{Scope.interval}}", + "setup": "prosumer" + } + }, + "resolved": { + "data_path": "consumptions" + } +} +``` + +The matching consumption response (hourly) returns a `feed-in` and a `feed-out` record per timestamp — surplus during the day, grid draw at night: + +```json title="Consumption response (setup=prosumer, interval=PT1H)" +{ + "consumptions": [ + { "timestamp": "2026-01-20T07:00:00.000+00:00", "type": "feed-in", "value": 0.0 }, + { "timestamp": "2026-01-20T07:00:00.000+00:00", "type": "feed-out", "value": 0.31 }, + { "timestamp": "2026-01-20T13:00:00.000+00:00", "type": "feed-in", "value": 4.12 }, + { "timestamp": "2026-01-20T13:00:00.000+00:00", "type": "feed-out", "value": 0.0 }, + { "timestamp": "2026-01-20T21:00:00.000+00:00", "type": "feed-in", "value": 0.0 }, + { "timestamp": "2026-01-20T21:00:00.000+00:00", "type": "feed-out", "value": 0.58 } + ] +} +``` + +The cost hook (`/example/cost`, `setup=prosumer`) reuses the same setup so the returned costs stay consistent with the consumption above. For a line-chart variant of the same data, the example integration also ships a `prosumer-line` setup (feed-in/feed-out advertised with `statistical_method: average`). + +#### Load Cycle (min / average / max) + +An industrial / B2B site is billed on instantaneous power (kW) rather than energy. The `load-cycle` setup advertises three series — `min`, `average` and `max` — sharing a single `aggregation_group` but each carrying its own `statistical_method`. The portal renders this as a min–max area band with the average drawn as a line on top. + +```json title="Visualization metadata response (setup=load-cycle)" +{ + "type_options": [ + { "id": "min", "label": { "en": "Minimum", "de": "Minimum" }, "aggregation_group": "load", "statistical_method": "min", "unit": "kW", "color": "slate", "precision": 0 }, + { "id": "average", "label": { "en": "Average", "de": "Durchschnitt" }, "aggregation_group": "load", "statistical_method": "average", "unit": "kW", "color": "primary", "precision": 0 }, + { "id": "max", "label": { "en": "Maximum", "de": "Maximum" }, "aggregation_group": "load", "statistical_method": "max", "unit": "kW", "color": "red", "precision": 0 } + ], + "intervals": ["PT15M", "PT1H", "P1D"], + "data_range": { "from": "2025-06-08T00:00:00.000Z", "to": "2026-06-08T00:00:00.000Z" } +} +``` + +```json title="Consumption response (setup=load-cycle, interval=PT1H)" +{ + "consumptions": [ + { "timestamp": "2026-01-20T03:00:00.000+00:00", "type": "min", "value": 118 }, + { "timestamp": "2026-01-20T03:00:00.000+00:00", "type": "average", "value": 128 }, + { "timestamp": "2026-01-20T03:00:00.000+00:00", "type": "max", "value": 139 }, + { "timestamp": "2026-01-20T09:00:00.000+00:00", "type": "min", "value": 548 }, + { "timestamp": "2026-01-20T09:00:00.000+00:00", "type": "average", "value": 642 }, + { "timestamp": "2026-01-20T09:00:00.000+00:00", "type": "max", "value": 731 } + ] +} +``` + +The hook configuration is identical to the prosumer consumption hook above, only with `"setup": "load-cycle"` in `call.params`. + +#### Other Built-in Setups + +The example integration ships further setups you can point the `setup` parameter at to exercise different chart shapes and discovery payloads: + +| `setup` | Series (`type_options`) | Rendered as | Notes | +| --- | --- | --- | --- | +| `default` | `default` (Consumption, kWh) | Bar | Single-tariff household, all intervals, last 2 years. | +| `dual-tariff` | `ht` (High tariff), `nt` (Night tariff) | Stacked bar | HT 06:00–22:00, NT otherwise; both in the `consumption` group. | +| `prosumer` | `feed-in`, `feed-out` | Bars (separate groups) | Net grid flows for a 5 kWp PV household. | +| `prosumer-line` | `feed-in`, `feed-out` | Area / line | Prosumer data as `statistical_method: average`. | +| `load-cycle` | `min`, `average`, `max` | Min–max band + average line | Industrial load in kW. | +| `consumption-with-load` | `consumption` (kWh, bar), `average-load` (kW, line) | Mixed bar + line | Two `aggregation_group`s in one chart. | +| `daily-only` | `default` | Bar | Only `P1D` + `P1M` (e.g. monthly meter readings). | +| `partial-history` | `ht`, `nt` | Stacked bar | Last 6 months only (e.g. recently switched provider). | +| `current-month` | `default` | Bar | Current month only (`PT1H` + `P1D`), e.g. start-of-contract demos. | + ### Data Existence Check/Retrieval Sometimes it is desired to check against a third party system before allowing a user to register or self-assign business objects to their account. From 4e69a591784ba86499642a6d1a26b921335821f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 12:56:01 +0000 Subject: [PATCH 2/2] docs(portal-extensions): drop cost-hook aside from prosumer example https://claude.ai/code/session_01NAGddUiJH2xfj6nAJqZie5 --- docs/apps/components/portal-extension.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/apps/components/portal-extension.md b/docs/apps/components/portal-extension.md index 466aca23..aee722d5 100644 --- a/docs/apps/components/portal-extension.md +++ b/docs/apps/components/portal-extension.md @@ -466,7 +466,7 @@ The matching consumption response (hourly) returns a `feed-in` and a `feed-out` } ``` -The cost hook (`/example/cost`, `setup=prosumer`) reuses the same setup so the returned costs stay consistent with the consumption above. For a line-chart variant of the same data, the example integration also ships a `prosumer-line` setup (feed-in/feed-out advertised with `statistical_method: average`). +For a line-chart variant of the same data, the example integration also ships a `prosumer-line` setup (feed-in/feed-out advertised with `statistical_method: average`). #### Load Cycle (min / average / max)