Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 197 additions & 0 deletions docs/apps/components/portal-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
]
}
```

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.
Expand Down
Loading