M6-T09: add Energy data API (prices/costs/summary/dsmr-latest/recompute/tibber-test)
- routes/api/energy.py + schemas/energy.py: GET prices (tibber points or manual tariff, buy/sell consistent with the pricing strategies), GET costs (time window + limit, ascending), GET costs/summary (summarize passthrough), GET dsmr/latest, POST costs/recompute (idempotent, <=366d guard, CSRF), POST tibber/test (three-state, token never echoed, CSRF). - main.py registers router; OpenAPI re-exported; tests for all endpoints.
This commit is contained in:
@@ -695,6 +695,409 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/energy/prices": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"api-energy"
|
||||
],
|
||||
"summary": "Get Prices",
|
||||
"description": "Return the price curve for the active contract.\n\n**Tibber contracts** (kind=\"tibber\"):\n Fetches ``tibber_price`` rows within ``[start, end]``, ordered ascending\n by ``starts_at``. At most ``limit`` rows are returned (most recent first\n within the window, then reversed to ascending order — identical to the\n modbus readings pattern).\n\n Response ``points`` carries per-slot:\n - ``buy = total`` (Tibber all-inclusive price)\n - ``sell = total − energy_tax − sell_adjust`` (from active version values)\n - ``level`` (Tibber price level, may be null)\n\n ``tariff`` is null.\n\n**Manual contracts** (kind=\"manual\"):\n ``points`` is empty. ``tariff`` carries the four effective prices\n derived using the billing engine formula:\n - ``buy_dal = energy.buy.dal + energy_tax + ode``\n - ``buy_normal = energy.buy.normal + energy_tax + ode``\n - ``sell_dal = energy.sell.dal``\n - ``sell_normal = energy.sell.normal``\n\n**No active contract**: returns kind=null, currency=\"EUR\", points=[], tariff=null (200).",
|
||||
"operationId": "get_prices_api_energy_prices_get",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "start",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Inclusive start of the time window (ISO 8601). Defaults to the start of today UTC when omitted.",
|
||||
"title": "Start"
|
||||
},
|
||||
"description": "Inclusive start of the time window (ISO 8601). Defaults to the start of today UTC when omitted."
|
||||
},
|
||||
{
|
||||
"name": "end",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Inclusive end of the time window (ISO 8601). Defaults to the end of tomorrow UTC when omitted.",
|
||||
"title": "End"
|
||||
},
|
||||
"description": "Inclusive end of the time window (ISO 8601). Defaults to the end of tomorrow UTC when omitted."
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"maximum": 5000,
|
||||
"minimum": 1,
|
||||
"description": "Maximum number of Tibber price points to return.",
|
||||
"default": 500,
|
||||
"title": "Limit"
|
||||
},
|
||||
"description": "Maximum number of Tibber price points to return."
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PricesResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/energy/costs": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"api-energy"
|
||||
],
|
||||
"summary": "Get Costs",
|
||||
"description": "Return energy_cost_period rows within a time window.\n\nRows are ordered by ``period_start`` ascending. When the window contains\nmore rows than ``limit``, the **most recent** N rows are returned (DESC LIMIT),\nthen reversed to ascending order — identical to the modbus readings pattern.\n\nQuery parameters:\n- ``start``: inclusive lower bound on ``period_start`` (ISO 8601 datetime).\n- ``end``: inclusive upper bound on ``period_start`` (ISO 8601 datetime).\n- ``limit``: max rows to return (default 500, max {_COSTS_LIMIT_MAX}).",
|
||||
"operationId": "get_costs_api_energy_costs_get",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "start",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Inclusive lower bound for period_start (ISO 8601).",
|
||||
"title": "Start"
|
||||
},
|
||||
"description": "Inclusive lower bound for period_start (ISO 8601)."
|
||||
},
|
||||
{
|
||||
"name": "end",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Inclusive upper bound for period_start (ISO 8601).",
|
||||
"title": "End"
|
||||
},
|
||||
"description": "Inclusive upper bound for period_start (ISO 8601)."
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"maximum": 5000,
|
||||
"minimum": 1,
|
||||
"description": "Maximum number of cost periods to return (default 500, max 5000).",
|
||||
"default": 500,
|
||||
"title": "Limit"
|
||||
},
|
||||
"description": "Maximum number of cost periods to return (default 500, max 5000)."
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CostsResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/energy/costs/summary": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"api-energy"
|
||||
],
|
||||
"summary": "Get Costs Summary",
|
||||
"description": "Aggregate billing for a time interval.\n\nCalls ``energy_cost.summarize(session, start, end)`` which computes:\n\n total_payable = Σ(net_cost) + fixed_costs − credits\n\nwhere ``fixed_costs`` is (network_fee + management_fee) apportioned to the\ninterval length in days (÷30 per month), and ``credits`` is heffingskorting\napportioned similarly (÷365 per year).\n\nBoth ``fixed_costs`` and ``credits`` are derived from the **currently active\ncontract version at ``end``**. When no active contract exists they are 0.",
|
||||
"operationId": "get_costs_summary_api_energy_costs_summary_get",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "start",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Inclusive start of the summary interval (ISO 8601). Defaults to the start of the current UTC day.",
|
||||
"title": "Start"
|
||||
},
|
||||
"description": "Inclusive start of the summary interval (ISO 8601). Defaults to the start of the current UTC day."
|
||||
},
|
||||
{
|
||||
"name": "end",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Exclusive end of the summary interval (ISO 8601). Defaults to the start of the next UTC day (i.e. today's full data).",
|
||||
"title": "End"
|
||||
},
|
||||
"description": "Exclusive end of the summary interval (ISO 8601). Defaults to the start of the next UTC day (i.e. today's full data)."
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SummaryResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/energy/dsmr/latest": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"api-energy"
|
||||
],
|
||||
"summary": "Get Dsmr Latest",
|
||||
"description": "Return the most recent dsmr_reading row.\n\nReturns ``{\"found\": false, \"recorded_at\": null, \"payload\": null}`` (200, not\n404) when no rows exist yet, so the front-end can distinguish \"no data\" from\na server error.",
|
||||
"operationId": "get_dsmr_latest_api_energy_dsmr_latest_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DsmrLatestResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/energy/costs/recompute": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"api-energy"
|
||||
],
|
||||
"summary": "Post Recompute",
|
||||
"description": "Idempotently recompute billing records in a time window.\n\nCalls ``energy_cost.recompute_range(session, start, end)`` which overwrites\nexisting rows (including successful ones) for every UTC quarter-hour boundary\nin ``[start, end)``.\n\n**Idempotency**: repeated calls with the same window produce the same\noutcome. No rows are deleted; only upserted.\n\n**Window constraint**: the maximum allowed range is {_RECOMPUTE_MAX_DAYS} days.\nRequests exceeding this return 422.\n\nReturns the number of periods for which a billing record was written.\nPeriods skipped due to missing contract or missing Tibber price are not counted.",
|
||||
"operationId": "post_recompute_api_energy_costs_recompute_post",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "start",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Inclusive start of the recompute window (ISO 8601). Required.",
|
||||
"title": "Start"
|
||||
},
|
||||
"description": "Inclusive start of the recompute window (ISO 8601). Required."
|
||||
},
|
||||
{
|
||||
"name": "end",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Exclusive end of the recompute window (ISO 8601). Required.",
|
||||
"title": "End"
|
||||
},
|
||||
"description": "Exclusive end of the recompute window (ISO 8601). Required."
|
||||
},
|
||||
{
|
||||
"name": "X-CSRF-Token",
|
||||
"in": "header",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "X-Csrf-Token"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RecomputeResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation error (missing window or range too large)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/energy/tibber/test": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"api-energy"
|
||||
],
|
||||
"summary": "Post Tibber Test",
|
||||
"description": "Test Tibber API connectivity by fetching the current price point.\n\nThree possible outcomes:\n\n- **200** ``{ result: \"success\", message: ..., price: {...} }``\n The Tibber API responded with a valid current price. ``price`` contains\n starts_at, total, energy, tax, currency, and level.\n\n- **400** ``{ result: \"config-error\", message: ... }``\n The Tibber API token is empty or not configured.\n\n- **502** ``{ result: \"failed\", message: ... }``\n The API call failed (authentication rejected, network error, timeout,\n unexpected response, etc.).\n\nThe API token is **never** included in the response body or logged.",
|
||||
"operationId": "post_tibber_test_api_energy_tibber_test_post",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "X-CSRF-Token",
|
||||
"in": "header",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "X-Csrf-Token"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TibberTestResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TibberTestResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"502": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TibberTestResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Bad Gateway"
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/energy/profiles": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -2461,6 +2864,110 @@
|
||||
"title": "ContractVersionResponse",
|
||||
"description": "Response schema for a single EnergyContractVersion row."
|
||||
},
|
||||
"CostPeriodSchema": {
|
||||
"properties": {
|
||||
"period_start": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Period Start"
|
||||
},
|
||||
"d1_kwh": {
|
||||
"type": "number",
|
||||
"title": "D1 Kwh",
|
||||
"description": "Delivered low-tariff kWh for this period."
|
||||
},
|
||||
"d2_kwh": {
|
||||
"type": "number",
|
||||
"title": "D2 Kwh",
|
||||
"description": "Delivered normal-tariff kWh for this period."
|
||||
},
|
||||
"r1_kwh": {
|
||||
"type": "number",
|
||||
"title": "R1 Kwh",
|
||||
"description": "Returned low-tariff kWh for this period."
|
||||
},
|
||||
"r2_kwh": {
|
||||
"type": "number",
|
||||
"title": "R2 Kwh",
|
||||
"description": "Returned normal-tariff kWh for this period."
|
||||
},
|
||||
"import_cost": {
|
||||
"type": "number",
|
||||
"title": "Import Cost",
|
||||
"description": "Cost of electricity drawn from grid (EUR)."
|
||||
},
|
||||
"export_revenue": {
|
||||
"type": "number",
|
||||
"title": "Export Revenue",
|
||||
"description": "Revenue from electricity fed to grid (EUR)."
|
||||
},
|
||||
"net_cost": {
|
||||
"type": "number",
|
||||
"title": "Net Cost",
|
||||
"description": "import_cost − export_revenue (EUR)."
|
||||
},
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"title": "Currency",
|
||||
"description": "ISO 4217 currency code."
|
||||
},
|
||||
"degraded": {
|
||||
"type": "boolean",
|
||||
"title": "Degraded",
|
||||
"description": "True when the period was computed with incomplete data."
|
||||
},
|
||||
"contract_version_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Contract Version Id",
|
||||
"description": "FK to the contract version used for this billing period (null when degraded)."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"period_start",
|
||||
"d1_kwh",
|
||||
"d2_kwh",
|
||||
"r1_kwh",
|
||||
"r2_kwh",
|
||||
"import_cost",
|
||||
"export_revenue",
|
||||
"net_cost",
|
||||
"currency",
|
||||
"degraded"
|
||||
],
|
||||
"title": "CostPeriodSchema",
|
||||
"description": "One 15-minute billing record from the energy_cost_period table."
|
||||
},
|
||||
"CostsResponse": {
|
||||
"properties": {
|
||||
"items": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/CostPeriodSchema"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Items"
|
||||
},
|
||||
"total": {
|
||||
"type": "integer",
|
||||
"title": "Total",
|
||||
"description": "Number of items returned."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"items",
|
||||
"total"
|
||||
],
|
||||
"title": "CostsResponse",
|
||||
"description": "Response for GET /api/energy/costs."
|
||||
},
|
||||
"DeviceInfoSchema": {
|
||||
"properties": {
|
||||
"identifiers": {
|
||||
@@ -2483,6 +2990,44 @@
|
||||
"title": "DeviceInfoSchema",
|
||||
"description": "HA device grouping info for an exposable entity."
|
||||
},
|
||||
"DsmrLatestResponse": {
|
||||
"properties": {
|
||||
"found": {
|
||||
"type": "boolean",
|
||||
"title": "Found"
|
||||
},
|
||||
"recorded_at": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Recorded At"
|
||||
},
|
||||
"payload": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Payload"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"found"
|
||||
],
|
||||
"title": "DsmrLatestResponse",
|
||||
"description": "Response for GET /api/energy/dsmr/latest.\n\n``found`` is False when no dsmr_reading rows exist yet. The front-end\nshould check ``found`` before reading ``recorded_at`` or ``payload``."
|
||||
},
|
||||
"ExposableEntitySchema": {
|
||||
"properties": {
|
||||
"key": {
|
||||
@@ -2746,6 +3291,39 @@
|
||||
],
|
||||
"title": "LoginRequest"
|
||||
},
|
||||
"ManualTariffSchema": {
|
||||
"properties": {
|
||||
"buy_dal": {
|
||||
"type": "number",
|
||||
"title": "Buy Dal",
|
||||
"description": "Effective buy price, low-tariff / dal (EUR/kWh)."
|
||||
},
|
||||
"buy_normal": {
|
||||
"type": "number",
|
||||
"title": "Buy Normal",
|
||||
"description": "Effective buy price, normal / high-tariff (EUR/kWh)."
|
||||
},
|
||||
"sell_dal": {
|
||||
"type": "number",
|
||||
"title": "Sell Dal",
|
||||
"description": "Sell price, low-tariff / dal (EUR/kWh)."
|
||||
},
|
||||
"sell_normal": {
|
||||
"type": "number",
|
||||
"title": "Sell Normal",
|
||||
"description": "Sell price, normal / high-tariff (EUR/kWh)."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"buy_dal",
|
||||
"buy_normal",
|
||||
"sell_dal",
|
||||
"sell_normal"
|
||||
],
|
||||
"title": "ManualTariffSchema",
|
||||
"description": "Fixed-tariff breakdown for manual contracts.\n\nPrices are the *effective* buy prices as used by the billing engine\n(energy_buy_x + energy_tax + ode) and the raw sell prices."
|
||||
},
|
||||
"MetricInfo": {
|
||||
"properties": {
|
||||
"key": {
|
||||
@@ -3377,6 +3955,93 @@
|
||||
"title": "PooUpdateRequest",
|
||||
"description": "PATCH body for a poo record — all fields optional; PK field excluded."
|
||||
},
|
||||
"PricePointSchema": {
|
||||
"properties": {
|
||||
"starts_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Starts At"
|
||||
},
|
||||
"buy": {
|
||||
"type": "number",
|
||||
"title": "Buy",
|
||||
"description": "All-in buy price in EUR/kWh (including taxes)."
|
||||
},
|
||||
"sell": {
|
||||
"type": "number",
|
||||
"title": "Sell",
|
||||
"description": "Net sell price in EUR/kWh."
|
||||
},
|
||||
"level": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Level",
|
||||
"description": "Tibber price level (CHEAP / NORMAL / EXPENSIVE); null for manual."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"starts_at",
|
||||
"buy",
|
||||
"sell"
|
||||
],
|
||||
"title": "PricePointSchema",
|
||||
"description": "A single 15-minute price point (tibber) or placeholder entry."
|
||||
},
|
||||
"PricesResponse": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Kind",
|
||||
"description": "Active contract kind ('tibber' or 'manual'), or null if no active contract."
|
||||
},
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"title": "Currency",
|
||||
"description": "ISO 4217 currency code."
|
||||
},
|
||||
"points": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PricePointSchema"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Points",
|
||||
"description": "15-minute price points for tibber contracts (ascending by starts_at). Empty for manual contracts or when no active contract exists."
|
||||
},
|
||||
"tariff": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ManualTariffSchema"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Fixed tariff table for manual contracts. Null for tibber contracts and when no active contract exists."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"currency",
|
||||
"points"
|
||||
],
|
||||
"title": "PricesResponse",
|
||||
"description": "Response for GET /api/energy/prices.\n\n``kind`` mirrors the active contract kind:\n- ``\"tibber\"`` → ``points`` has actual 15-min price entries; ``tariff`` is null.\n- ``\"manual\"`` → ``points`` is empty; ``tariff`` carries the fixed-rate table.\n- ``None`` → no active contract; both ``points`` and ``tariff`` are empty/null.\n\n``currency`` comes from the active contract (or \"EUR\" fallback).\n``points`` is always ascending by ``starts_at``."
|
||||
},
|
||||
"ProfileSummary": {
|
||||
"properties": {
|
||||
"name": {
|
||||
@@ -3596,6 +4261,21 @@
|
||||
],
|
||||
"title": "PublicIPStateSchema"
|
||||
},
|
||||
"RecomputeResponse": {
|
||||
"properties": {
|
||||
"recomputed": {
|
||||
"type": "integer",
|
||||
"title": "Recomputed",
|
||||
"description": "Number of 15-minute periods for which a billing record was written (inserted or updated). Periods skipped due to missing contract or missing Tibber price are not counted."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"recomputed"
|
||||
],
|
||||
"title": "RecomputeResponse",
|
||||
"description": "Response for POST /api/energy/costs/recompute."
|
||||
},
|
||||
"RepublishResponse": {
|
||||
"properties": {
|
||||
"ok": {
|
||||
@@ -3687,6 +4367,154 @@
|
||||
],
|
||||
"title": "StatusResponse"
|
||||
},
|
||||
"SummaryResponse": {
|
||||
"properties": {
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"title": "Currency"
|
||||
},
|
||||
"metered_import": {
|
||||
"type": "number",
|
||||
"title": "Metered Import",
|
||||
"description": "Σ import_cost for non-degraded periods."
|
||||
},
|
||||
"metered_export": {
|
||||
"type": "number",
|
||||
"title": "Metered Export",
|
||||
"description": "Σ export_revenue for non-degraded periods."
|
||||
},
|
||||
"metered_net": {
|
||||
"type": "number",
|
||||
"title": "Metered Net",
|
||||
"description": "Σ net_cost for non-degraded periods."
|
||||
},
|
||||
"fixed_costs": {
|
||||
"type": "number",
|
||||
"title": "Fixed Costs",
|
||||
"description": "Standing charges (network_fee + management_fee) apportioned over the interval."
|
||||
},
|
||||
"credits": {
|
||||
"type": "number",
|
||||
"title": "Credits",
|
||||
"description": "Energy-tax credit (heffingskorting) apportioned over the interval."
|
||||
},
|
||||
"total_payable": {
|
||||
"type": "number",
|
||||
"title": "Total Payable",
|
||||
"description": "metered_net + fixed_costs − credits (actual amount owed)."
|
||||
},
|
||||
"period_count": {
|
||||
"type": "integer",
|
||||
"title": "Period Count",
|
||||
"description": "Number of non-degraded billing periods in range."
|
||||
},
|
||||
"degraded_count": {
|
||||
"type": "integer",
|
||||
"title": "Degraded Count",
|
||||
"description": "Number of degraded billing periods in range."
|
||||
},
|
||||
"days": {
|
||||
"type": "number",
|
||||
"title": "Days",
|
||||
"description": "Interval length in days."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"currency",
|
||||
"metered_import",
|
||||
"metered_export",
|
||||
"metered_net",
|
||||
"fixed_costs",
|
||||
"credits",
|
||||
"total_payable",
|
||||
"period_count",
|
||||
"degraded_count",
|
||||
"days"
|
||||
],
|
||||
"title": "SummaryResponse",
|
||||
"description": "Response for GET /api/energy/costs/summary.\n\nAll monetary values are in ``currency``.\n\n``total_payable = metered_net + fixed_costs − credits``"
|
||||
},
|
||||
"TibberTestPriceSchema": {
|
||||
"properties": {
|
||||
"starts_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Starts At"
|
||||
},
|
||||
"total": {
|
||||
"type": "number",
|
||||
"title": "Total"
|
||||
},
|
||||
"energy": {
|
||||
"type": "number",
|
||||
"title": "Energy"
|
||||
},
|
||||
"tax": {
|
||||
"type": "number",
|
||||
"title": "Tax"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"title": "Currency"
|
||||
},
|
||||
"level": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Level"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"starts_at",
|
||||
"total",
|
||||
"energy",
|
||||
"tax",
|
||||
"currency"
|
||||
],
|
||||
"title": "TibberTestPriceSchema",
|
||||
"description": "Current Tibber price point returned on a successful test.\n\nCarries enough fields for the front-end to confirm the API is working and\ndisplay the live price. The API token is **never** included."
|
||||
},
|
||||
"TibberTestResponse": {
|
||||
"properties": {
|
||||
"result": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"success",
|
||||
"config-error",
|
||||
"failed"
|
||||
],
|
||||
"title": "Result"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"title": "Message"
|
||||
},
|
||||
"price": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/TibberTestPriceSchema"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"result",
|
||||
"message"
|
||||
],
|
||||
"title": "TibberTestResponse",
|
||||
"description": "Three-state response for POST /api/energy/tibber/test.\n\nPossible ``result`` values:\n\n- ``\"success\"`` — Tibber API responded with a valid price.\n- ``\"config-error\"`` — Token is missing or not configured.\n- ``\"failed\"`` — API call failed (auth rejected, network error, timeout, etc.).\n\n``price`` is populated only on ``\"success\"``; it is null otherwise.\n``message`` always contains a human-readable explanation."
|
||||
},
|
||||
"TotpDisableRequest": {
|
||||
"properties": {
|
||||
"password": {
|
||||
|
||||
@@ -516,6 +516,338 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
/api/energy/prices:
|
||||
get:
|
||||
tags:
|
||||
- api-energy
|
||||
summary: Get Prices
|
||||
description: "Return the price curve for the active contract.\n\n**Tibber contracts**\
|
||||
\ (kind=\"tibber\"):\n Fetches ``tibber_price`` rows within ``[start, end]``,\
|
||||
\ ordered ascending\n by ``starts_at``. At most ``limit`` rows are returned\
|
||||
\ (most recent first\n within the window, then reversed to ascending order\
|
||||
\ — identical to the\n modbus readings pattern).\n\n Response ``points``\
|
||||
\ carries per-slot:\n - ``buy = total`` (Tibber all-inclusive\
|
||||
\ price)\n - ``sell = total − energy_tax − sell_adjust`` (from active\
|
||||
\ version values)\n - ``level`` (Tibber price level,\
|
||||
\ may be null)\n\n ``tariff`` is null.\n\n**Manual contracts** (kind=\"\
|
||||
manual\"):\n ``points`` is empty. ``tariff`` carries the four effective\
|
||||
\ prices\n derived using the billing engine formula:\n - ``buy_dal \
|
||||
\ = energy.buy.dal + energy_tax + ode``\n - ``buy_normal = energy.buy.normal\
|
||||
\ + energy_tax + ode``\n - ``sell_dal = energy.sell.dal``\n - ``sell_normal\
|
||||
\ = energy.sell.normal``\n\n**No active contract**: returns kind=null, currency=\"\
|
||||
EUR\", points=[], tariff=null (200)."
|
||||
operationId: get_prices_api_energy_prices_get
|
||||
parameters:
|
||||
- name: start
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
anyOf:
|
||||
- type: string
|
||||
format: date-time
|
||||
- type: 'null'
|
||||
description: Inclusive start of the time window (ISO 8601). Defaults to
|
||||
the start of today UTC when omitted.
|
||||
title: Start
|
||||
description: Inclusive start of the time window (ISO 8601). Defaults to the
|
||||
start of today UTC when omitted.
|
||||
- name: end
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
anyOf:
|
||||
- type: string
|
||||
format: date-time
|
||||
- type: 'null'
|
||||
description: Inclusive end of the time window (ISO 8601). Defaults to the
|
||||
end of tomorrow UTC when omitted.
|
||||
title: End
|
||||
description: Inclusive end of the time window (ISO 8601). Defaults to the
|
||||
end of tomorrow UTC when omitted.
|
||||
- name: limit
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
maximum: 5000
|
||||
minimum: 1
|
||||
description: Maximum number of Tibber price points to return.
|
||||
default: 500
|
||||
title: Limit
|
||||
description: Maximum number of Tibber price points to return.
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PricesResponse'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
/api/energy/costs:
|
||||
get:
|
||||
tags:
|
||||
- api-energy
|
||||
summary: Get Costs
|
||||
description: 'Return energy_cost_period rows within a time window.
|
||||
|
||||
|
||||
Rows are ordered by ``period_start`` ascending. When the window contains
|
||||
|
||||
more rows than ``limit``, the **most recent** N rows are returned (DESC LIMIT),
|
||||
|
||||
then reversed to ascending order — identical to the modbus readings pattern.
|
||||
|
||||
|
||||
Query parameters:
|
||||
|
||||
- ``start``: inclusive lower bound on ``period_start`` (ISO 8601 datetime).
|
||||
|
||||
- ``end``: inclusive upper bound on ``period_start`` (ISO 8601 datetime).
|
||||
|
||||
- ``limit``: max rows to return (default 500, max {_COSTS_LIMIT_MAX}).'
|
||||
operationId: get_costs_api_energy_costs_get
|
||||
parameters:
|
||||
- name: start
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
anyOf:
|
||||
- type: string
|
||||
format: date-time
|
||||
- type: 'null'
|
||||
description: Inclusive lower bound for period_start (ISO 8601).
|
||||
title: Start
|
||||
description: Inclusive lower bound for period_start (ISO 8601).
|
||||
- name: end
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
anyOf:
|
||||
- type: string
|
||||
format: date-time
|
||||
- type: 'null'
|
||||
description: Inclusive upper bound for period_start (ISO 8601).
|
||||
title: End
|
||||
description: Inclusive upper bound for period_start (ISO 8601).
|
||||
- name: limit
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
maximum: 5000
|
||||
minimum: 1
|
||||
description: Maximum number of cost periods to return (default 500, max
|
||||
5000).
|
||||
default: 500
|
||||
title: Limit
|
||||
description: Maximum number of cost periods to return (default 500, max 5000).
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CostsResponse'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
/api/energy/costs/summary:
|
||||
get:
|
||||
tags:
|
||||
- api-energy
|
||||
summary: Get Costs Summary
|
||||
description: "Aggregate billing for a time interval.\n\nCalls ``energy_cost.summarize(session,\
|
||||
\ start, end)`` which computes:\n\n total_payable = Σ(net_cost) + fixed_costs\
|
||||
\ − credits\n\nwhere ``fixed_costs`` is (network_fee + management_fee) apportioned\
|
||||
\ to the\ninterval length in days (÷30 per month), and ``credits`` is heffingskorting\n\
|
||||
apportioned similarly (÷365 per year).\n\nBoth ``fixed_costs`` and ``credits``\
|
||||
\ are derived from the **currently active\ncontract version at ``end``**.\
|
||||
\ When no active contract exists they are 0."
|
||||
operationId: get_costs_summary_api_energy_costs_summary_get
|
||||
parameters:
|
||||
- name: start
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
anyOf:
|
||||
- type: string
|
||||
format: date-time
|
||||
- type: 'null'
|
||||
description: Inclusive start of the summary interval (ISO 8601). Defaults
|
||||
to the start of the current UTC day.
|
||||
title: Start
|
||||
description: Inclusive start of the summary interval (ISO 8601). Defaults
|
||||
to the start of the current UTC day.
|
||||
- name: end
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
anyOf:
|
||||
- type: string
|
||||
format: date-time
|
||||
- type: 'null'
|
||||
description: Exclusive end of the summary interval (ISO 8601). Defaults
|
||||
to the start of the next UTC day (i.e. today's full data).
|
||||
title: End
|
||||
description: Exclusive end of the summary interval (ISO 8601). Defaults to
|
||||
the start of the next UTC day (i.e. today's full data).
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SummaryResponse'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
/api/energy/dsmr/latest:
|
||||
get:
|
||||
tags:
|
||||
- api-energy
|
||||
summary: Get Dsmr Latest
|
||||
description: 'Return the most recent dsmr_reading row.
|
||||
|
||||
|
||||
Returns ``{"found": false, "recorded_at": null, "payload": null}`` (200, not
|
||||
|
||||
404) when no rows exist yet, so the front-end can distinguish "no data" from
|
||||
|
||||
a server error.'
|
||||
operationId: get_dsmr_latest_api_energy_dsmr_latest_get
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DsmrLatestResponse'
|
||||
/api/energy/costs/recompute:
|
||||
post:
|
||||
tags:
|
||||
- api-energy
|
||||
summary: Post Recompute
|
||||
description: 'Idempotently recompute billing records in a time window.
|
||||
|
||||
|
||||
Calls ``energy_cost.recompute_range(session, start, end)`` which overwrites
|
||||
|
||||
existing rows (including successful ones) for every UTC quarter-hour boundary
|
||||
|
||||
in ``[start, end)``.
|
||||
|
||||
|
||||
**Idempotency**: repeated calls with the same window produce the same
|
||||
|
||||
outcome. No rows are deleted; only upserted.
|
||||
|
||||
|
||||
**Window constraint**: the maximum allowed range is {_RECOMPUTE_MAX_DAYS}
|
||||
days.
|
||||
|
||||
Requests exceeding this return 422.
|
||||
|
||||
|
||||
Returns the number of periods for which a billing record was written.
|
||||
|
||||
Periods skipped due to missing contract or missing Tibber price are not counted.'
|
||||
operationId: post_recompute_api_energy_costs_recompute_post
|
||||
parameters:
|
||||
- name: start
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Inclusive start of the recompute window (ISO 8601). Required.
|
||||
title: Start
|
||||
description: Inclusive start of the recompute window (ISO 8601). Required.
|
||||
- name: end
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Exclusive end of the recompute window (ISO 8601). Required.
|
||||
title: End
|
||||
description: Exclusive end of the recompute window (ISO 8601). Required.
|
||||
- name: X-CSRF-Token
|
||||
in: header
|
||||
required: false
|
||||
schema:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: 'null'
|
||||
title: X-Csrf-Token
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RecomputeResponse'
|
||||
'422':
|
||||
description: Validation error (missing window or range too large)
|
||||
/api/energy/tibber/test:
|
||||
post:
|
||||
tags:
|
||||
- api-energy
|
||||
summary: Post Tibber Test
|
||||
description: "Test Tibber API connectivity by fetching the current price point.\n\
|
||||
\nThree possible outcomes:\n\n- **200** ``{ result: \"success\", message:\
|
||||
\ ..., price: {...} }``\n The Tibber API responded with a valid current price.\
|
||||
\ ``price`` contains\n starts_at, total, energy, tax, currency, and level.\n\
|
||||
\n- **400** ``{ result: \"config-error\", message: ... }``\n The Tibber API\
|
||||
\ token is empty or not configured.\n\n- **502** ``{ result: \"failed\", \
|
||||
\ message: ... }``\n The API call failed (authentication rejected, network\
|
||||
\ error, timeout,\n unexpected response, etc.).\n\nThe API token is **never**\
|
||||
\ included in the response body or logged."
|
||||
operationId: post_tibber_test_api_energy_tibber_test_post
|
||||
parameters:
|
||||
- name: X-CSRF-Token
|
||||
in: header
|
||||
required: false
|
||||
schema:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: 'null'
|
||||
title: X-Csrf-Token
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TibberTestResponse'
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TibberTestResponse'
|
||||
description: Bad Request
|
||||
'502':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TibberTestResponse'
|
||||
description: Bad Gateway
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
/api/energy/profiles:
|
||||
get:
|
||||
tags:
|
||||
@@ -1899,6 +2231,86 @@ components:
|
||||
- created_at
|
||||
title: ContractVersionResponse
|
||||
description: Response schema for a single EnergyContractVersion row.
|
||||
CostPeriodSchema:
|
||||
properties:
|
||||
period_start:
|
||||
type: string
|
||||
format: date-time
|
||||
title: Period Start
|
||||
d1_kwh:
|
||||
type: number
|
||||
title: D1 Kwh
|
||||
description: Delivered low-tariff kWh for this period.
|
||||
d2_kwh:
|
||||
type: number
|
||||
title: D2 Kwh
|
||||
description: Delivered normal-tariff kWh for this period.
|
||||
r1_kwh:
|
||||
type: number
|
||||
title: R1 Kwh
|
||||
description: Returned low-tariff kWh for this period.
|
||||
r2_kwh:
|
||||
type: number
|
||||
title: R2 Kwh
|
||||
description: Returned normal-tariff kWh for this period.
|
||||
import_cost:
|
||||
type: number
|
||||
title: Import Cost
|
||||
description: Cost of electricity drawn from grid (EUR).
|
||||
export_revenue:
|
||||
type: number
|
||||
title: Export Revenue
|
||||
description: Revenue from electricity fed to grid (EUR).
|
||||
net_cost:
|
||||
type: number
|
||||
title: Net Cost
|
||||
description: import_cost − export_revenue (EUR).
|
||||
currency:
|
||||
type: string
|
||||
title: Currency
|
||||
description: ISO 4217 currency code.
|
||||
degraded:
|
||||
type: boolean
|
||||
title: Degraded
|
||||
description: True when the period was computed with incomplete data.
|
||||
contract_version_id:
|
||||
anyOf:
|
||||
- type: integer
|
||||
- type: 'null'
|
||||
title: Contract Version Id
|
||||
description: FK to the contract version used for this billing period (null
|
||||
when degraded).
|
||||
type: object
|
||||
required:
|
||||
- period_start
|
||||
- d1_kwh
|
||||
- d2_kwh
|
||||
- r1_kwh
|
||||
- r2_kwh
|
||||
- import_cost
|
||||
- export_revenue
|
||||
- net_cost
|
||||
- currency
|
||||
- degraded
|
||||
title: CostPeriodSchema
|
||||
description: One 15-minute billing record from the energy_cost_period table.
|
||||
CostsResponse:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/components/schemas/CostPeriodSchema'
|
||||
type: array
|
||||
title: Items
|
||||
total:
|
||||
type: integer
|
||||
title: Total
|
||||
description: Number of items returned.
|
||||
type: object
|
||||
required:
|
||||
- items
|
||||
- total
|
||||
title: CostsResponse
|
||||
description: Response for GET /api/energy/costs.
|
||||
DeviceInfoSchema:
|
||||
properties:
|
||||
identifiers:
|
||||
@@ -1915,6 +2327,33 @@ components:
|
||||
- name
|
||||
title: DeviceInfoSchema
|
||||
description: HA device grouping info for an exposable entity.
|
||||
DsmrLatestResponse:
|
||||
properties:
|
||||
found:
|
||||
type: boolean
|
||||
title: Found
|
||||
recorded_at:
|
||||
anyOf:
|
||||
- type: string
|
||||
format: date-time
|
||||
- type: 'null'
|
||||
title: Recorded At
|
||||
payload:
|
||||
anyOf:
|
||||
- additionalProperties: true
|
||||
type: object
|
||||
- type: 'null'
|
||||
title: Payload
|
||||
type: object
|
||||
required:
|
||||
- found
|
||||
title: DsmrLatestResponse
|
||||
description: 'Response for GET /api/energy/dsmr/latest.
|
||||
|
||||
|
||||
``found`` is False when no dsmr_reading rows exist yet. The front-end
|
||||
|
||||
should check ``found`` before reading ``recorded_at`` or ``payload``.'
|
||||
ExposableEntitySchema:
|
||||
properties:
|
||||
key:
|
||||
@@ -2097,6 +2536,37 @@ components:
|
||||
- username
|
||||
- password
|
||||
title: LoginRequest
|
||||
ManualTariffSchema:
|
||||
properties:
|
||||
buy_dal:
|
||||
type: number
|
||||
title: Buy Dal
|
||||
description: Effective buy price, low-tariff / dal (EUR/kWh).
|
||||
buy_normal:
|
||||
type: number
|
||||
title: Buy Normal
|
||||
description: Effective buy price, normal / high-tariff (EUR/kWh).
|
||||
sell_dal:
|
||||
type: number
|
||||
title: Sell Dal
|
||||
description: Sell price, low-tariff / dal (EUR/kWh).
|
||||
sell_normal:
|
||||
type: number
|
||||
title: Sell Normal
|
||||
description: Sell price, normal / high-tariff (EUR/kWh).
|
||||
type: object
|
||||
required:
|
||||
- buy_dal
|
||||
- buy_normal
|
||||
- sell_dal
|
||||
- sell_normal
|
||||
title: ManualTariffSchema
|
||||
description: 'Fixed-tariff breakdown for manual contracts.
|
||||
|
||||
|
||||
Prices are the *effective* buy prices as used by the billing engine
|
||||
|
||||
(energy_buy_x + energy_tax + ode) and the raw sell prices.'
|
||||
MetricInfo:
|
||||
properties:
|
||||
key:
|
||||
@@ -2534,6 +3004,81 @@ components:
|
||||
type: object
|
||||
title: PooUpdateRequest
|
||||
description: PATCH body for a poo record — all fields optional; PK field excluded.
|
||||
PricePointSchema:
|
||||
properties:
|
||||
starts_at:
|
||||
type: string
|
||||
format: date-time
|
||||
title: Starts At
|
||||
buy:
|
||||
type: number
|
||||
title: Buy
|
||||
description: All-in buy price in EUR/kWh (including taxes).
|
||||
sell:
|
||||
type: number
|
||||
title: Sell
|
||||
description: Net sell price in EUR/kWh.
|
||||
level:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: 'null'
|
||||
title: Level
|
||||
description: Tibber price level (CHEAP / NORMAL / EXPENSIVE); null for manual.
|
||||
type: object
|
||||
required:
|
||||
- starts_at
|
||||
- buy
|
||||
- sell
|
||||
title: PricePointSchema
|
||||
description: A single 15-minute price point (tibber) or placeholder entry.
|
||||
PricesResponse:
|
||||
properties:
|
||||
kind:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: 'null'
|
||||
title: Kind
|
||||
description: Active contract kind ('tibber' or 'manual'), or null if no
|
||||
active contract.
|
||||
currency:
|
||||
type: string
|
||||
title: Currency
|
||||
description: ISO 4217 currency code.
|
||||
points:
|
||||
items:
|
||||
$ref: '#/components/schemas/PricePointSchema'
|
||||
type: array
|
||||
title: Points
|
||||
description: 15-minute price points for tibber contracts (ascending by starts_at).
|
||||
Empty for manual contracts or when no active contract exists.
|
||||
tariff:
|
||||
anyOf:
|
||||
- $ref: '#/components/schemas/ManualTariffSchema'
|
||||
- type: 'null'
|
||||
description: Fixed tariff table for manual contracts. Null for tibber contracts
|
||||
and when no active contract exists.
|
||||
type: object
|
||||
required:
|
||||
- kind
|
||||
- currency
|
||||
- points
|
||||
title: PricesResponse
|
||||
description: 'Response for GET /api/energy/prices.
|
||||
|
||||
|
||||
``kind`` mirrors the active contract kind:
|
||||
|
||||
- ``"tibber"`` → ``points`` has actual 15-min price entries; ``tariff`` is
|
||||
null.
|
||||
|
||||
- ``"manual"`` → ``points`` is empty; ``tariff`` carries the fixed-rate table.
|
||||
|
||||
- ``None`` → no active contract; both ``points`` and ``tariff`` are empty/null.
|
||||
|
||||
|
||||
``currency`` comes from the active contract (or "EUR" fallback).
|
||||
|
||||
``points`` is always ascending by ``starts_at``.'
|
||||
ProfileSummary:
|
||||
properties:
|
||||
name:
|
||||
@@ -2689,6 +3234,19 @@ components:
|
||||
- last_check_error
|
||||
- last_provider
|
||||
title: PublicIPStateSchema
|
||||
RecomputeResponse:
|
||||
properties:
|
||||
recomputed:
|
||||
type: integer
|
||||
title: Recomputed
|
||||
description: Number of 15-minute periods for which a billing record was
|
||||
written (inserted or updated). Periods skipped due to missing contract
|
||||
or missing Tibber price are not counted.
|
||||
type: object
|
||||
required:
|
||||
- recomputed
|
||||
title: RecomputeResponse
|
||||
description: Response for POST /api/energy/costs/recompute.
|
||||
RepublishResponse:
|
||||
properties:
|
||||
ok:
|
||||
@@ -2755,6 +3313,143 @@ components:
|
||||
required:
|
||||
- status
|
||||
title: StatusResponse
|
||||
SummaryResponse:
|
||||
properties:
|
||||
currency:
|
||||
type: string
|
||||
title: Currency
|
||||
metered_import:
|
||||
type: number
|
||||
title: Metered Import
|
||||
description: Σ import_cost for non-degraded periods.
|
||||
metered_export:
|
||||
type: number
|
||||
title: Metered Export
|
||||
description: Σ export_revenue for non-degraded periods.
|
||||
metered_net:
|
||||
type: number
|
||||
title: Metered Net
|
||||
description: Σ net_cost for non-degraded periods.
|
||||
fixed_costs:
|
||||
type: number
|
||||
title: Fixed Costs
|
||||
description: Standing charges (network_fee + management_fee) apportioned
|
||||
over the interval.
|
||||
credits:
|
||||
type: number
|
||||
title: Credits
|
||||
description: Energy-tax credit (heffingskorting) apportioned over the interval.
|
||||
total_payable:
|
||||
type: number
|
||||
title: Total Payable
|
||||
description: metered_net + fixed_costs − credits (actual amount owed).
|
||||
period_count:
|
||||
type: integer
|
||||
title: Period Count
|
||||
description: Number of non-degraded billing periods in range.
|
||||
degraded_count:
|
||||
type: integer
|
||||
title: Degraded Count
|
||||
description: Number of degraded billing periods in range.
|
||||
days:
|
||||
type: number
|
||||
title: Days
|
||||
description: Interval length in days.
|
||||
type: object
|
||||
required:
|
||||
- currency
|
||||
- metered_import
|
||||
- metered_export
|
||||
- metered_net
|
||||
- fixed_costs
|
||||
- credits
|
||||
- total_payable
|
||||
- period_count
|
||||
- degraded_count
|
||||
- days
|
||||
title: SummaryResponse
|
||||
description: 'Response for GET /api/energy/costs/summary.
|
||||
|
||||
|
||||
All monetary values are in ``currency``.
|
||||
|
||||
|
||||
``total_payable = metered_net + fixed_costs − credits``'
|
||||
TibberTestPriceSchema:
|
||||
properties:
|
||||
starts_at:
|
||||
type: string
|
||||
format: date-time
|
||||
title: Starts At
|
||||
total:
|
||||
type: number
|
||||
title: Total
|
||||
energy:
|
||||
type: number
|
||||
title: Energy
|
||||
tax:
|
||||
type: number
|
||||
title: Tax
|
||||
currency:
|
||||
type: string
|
||||
title: Currency
|
||||
level:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: 'null'
|
||||
title: Level
|
||||
type: object
|
||||
required:
|
||||
- starts_at
|
||||
- total
|
||||
- energy
|
||||
- tax
|
||||
- currency
|
||||
title: TibberTestPriceSchema
|
||||
description: 'Current Tibber price point returned on a successful test.
|
||||
|
||||
|
||||
Carries enough fields for the front-end to confirm the API is working and
|
||||
|
||||
display the live price. The API token is **never** included.'
|
||||
TibberTestResponse:
|
||||
properties:
|
||||
result:
|
||||
type: string
|
||||
enum:
|
||||
- success
|
||||
- config-error
|
||||
- failed
|
||||
title: Result
|
||||
message:
|
||||
type: string
|
||||
title: Message
|
||||
price:
|
||||
anyOf:
|
||||
- $ref: '#/components/schemas/TibberTestPriceSchema'
|
||||
- type: 'null'
|
||||
type: object
|
||||
required:
|
||||
- result
|
||||
- message
|
||||
title: TibberTestResponse
|
||||
description: 'Three-state response for POST /api/energy/tibber/test.
|
||||
|
||||
|
||||
Possible ``result`` values:
|
||||
|
||||
|
||||
- ``"success"`` — Tibber API responded with a valid price.
|
||||
|
||||
- ``"config-error"`` — Token is missing or not configured.
|
||||
|
||||
- ``"failed"`` — API call failed (auth rejected, network error, timeout,
|
||||
etc.).
|
||||
|
||||
|
||||
``price`` is populated only on ``"success"``; it is null otherwise.
|
||||
|
||||
``message`` always contains a human-readable explanation.'
|
||||
TotpDisableRequest:
|
||||
properties:
|
||||
password:
|
||||
|
||||
Reference in New Issue
Block a user