{ "openapi": "3.1.0", "info": { "title": "Home Automation Backend (Python)", "description": "Home automation backend with auth, runtime config, Home Assistant integrations, TickTick integration, and SQLite-backed recorders.", "version": "0.1.0" }, "paths": { "/status": { "get": { "tags": [ "system" ], "summary": "Get Status", "operationId": "get_status_status_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StatusResponse" } } } } } } }, "/api/config": { "get": { "tags": [ "api-config" ], "summary": "Get Config", "description": "Return all configuration sections. Secret field values are masked (empty string).", "operationId": "get_config_api_config_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConfigResponse" } } } } } }, "put": { "tags": [ "api-config" ], "summary": "Put Config", "description": "Save configuration updates.\n\n- Blank secret value keeps the existing stored value (no change).\n- Invalid values return 422 and nothing is written to the database.\n- If MQTT-related settings changed, the MQTT client reconnects automatically.", "operationId": "put_config_api_config_put", "parameters": [ { "name": "X-CSRF-Token", "in": "header", "required": false, "schema": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "X-Csrf-Token" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConfigUpdateRequest" } } } }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConfigUpdateResponse" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/config/smtp/test": { "post": { "tags": [ "api-config" ], "summary": "Post Smtp Test", "description": "Send a test SMTP email using the current runtime settings.\n\nReturns a structured result indicating success or the category of failure.\nThree possible outcomes:\n- 200 { \"result\": \"success\", \"message\": ... }\n- 400 { \"result\": \"config-error\", \"message\": ... } (EmailConfigurationError)\n- 502 { \"result\": \"failed\", \"message\": ... } (EmailDeliveryError)\n\nSMTP credentials are never echoed in the response.", "operationId": "post_smtp_test_api_config_smtp_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/SmtpTestResponse" } } } }, "400": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SmtpTestResponse" } } }, "description": "Bad Request" }, "502": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SmtpTestResponse" } } }, "description": "Bad Gateway" }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/config/mqtt/test": { "post": { "tags": [ "api-config" ], "summary": "Post Mqtt Test", "description": "Test MQTT broker connectivity by attempting to connect and publishing a\ntest message to ``/home-automation/test``.\n\nThe message is visible in MQTT Explorer (or any subscriber) so users can\nconfirm the full broker publish path is working.\n\nThree possible outcomes:\n- 200 { \"result\": \"success\", \"message\": ... }\n- 400 { \"result\": \"config-error\", \"message\": ... } (not configured)\n- 502 { \"result\": \"failed\", \"message\": ... } (connection/publish error)\n\nMQTT credentials are never echoed in the response.", "operationId": "post_mqtt_test_api_config_mqtt_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/MqttTestResponse" } } } }, "400": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MqttTestResponse" } } }, "description": "Bad Request" }, "502": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MqttTestResponse" } } }, "description": "Bad Gateway" }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/locations": { "get": { "tags": [ "api-data" ], "summary": "Get Locations", "description": "Return location records with optional time-window filtering and pagination.\n\n- ``start`` / ``end`` are ISO8601 strings; filtering is **inclusive** on both bounds.\n- Results are ordered by ``datetime`` ascending.\n- ``limit`` is capped at 5000 to prevent full-table exports.", "operationId": "get_locations_api_locations_get", "parameters": [ { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "maximum": 5000, "minimum": 1, "default": 1000, "title": "Limit" } }, { "name": "offset", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 0, "default": 0, "title": "Offset" } }, { "name": "start", "in": "query", "required": false, "schema": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Start" } }, { "name": "end", "in": "query", "required": false, "schema": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "End" } } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LocationsResponse" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/poo": { "get": { "tags": [ "api-data" ], "summary": "Get Poo", "description": "Return poo records ordered by timestamp descending (most recent first).\n\n``limit`` is capped at 1000 to prevent full-table exports.", "operationId": "get_poo_api_poo_get", "parameters": [ { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "maximum": 1000, "minimum": 1, "default": 100, "title": "Limit" } }, { "name": "offset", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 0, "default": 0, "title": "Offset" } } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PooResponse" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/public-ip": { "get": { "tags": [ "api-data" ], "summary": "Get Public Ip", "description": "Return the current public IP state and recent history.\n\n- ``state`` is ``null`` if no IP check has been performed yet.\n- ``history`` is ordered by ``observed_at`` descending (most recent first).\n- ``limit`` applies to the history list and is capped at 1000.", "operationId": "get_public_ip_api_public_ip_get", "parameters": [ { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "maximum": 1000, "minimum": 1, "default": 100, "title": "Limit" } } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicIPResponse" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/locations/{person}/{datetime}": { "patch": { "tags": [ "api-data" ], "summary": "Patch Location", "description": "Update the non-PK fields of a single location record.\n\n- ``person`` and ``datetime`` identify the row (composite PK) and are immutable.\n- Only ``latitude``, ``longitude``, and ``altitude`` may be updated.\n- Omitted body fields are left unchanged.\n- Returns **404** if the PK does not exist.", "operationId": "patch_location_api_locations__person___datetime__patch", "parameters": [ { "name": "person", "in": "path", "required": true, "schema": { "type": "string", "title": "Person" } }, { "name": "datetime", "in": "path", "required": true, "schema": { "type": "string", "title": "Datetime" } }, { "name": "X-CSRF-Token", "in": "header", "required": false, "schema": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "X-Csrf-Token" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LocationUpdateRequest", "default": {} } } } }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LocationRecord" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } }, "delete": { "tags": [ "api-data" ], "summary": "Delete Location Record", "description": "Delete the single location record identified by its composite PK.\n\n- Exactly one row is deleted; **404** if the PK does not exist.\n- No batch delete / truncate path is available.", "operationId": "delete_location_record_api_locations__person___datetime__delete", "parameters": [ { "name": "person", "in": "path", "required": true, "schema": { "type": "string", "title": "Person" } }, { "name": "datetime", "in": "path", "required": true, "schema": { "type": "string", "title": "Datetime" } }, { "name": "X-CSRF-Token", "in": "header", "required": false, "schema": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "X-Csrf-Token" } } ], "responses": { "204": { "description": "Successful Response" }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/poo/{timestamp}": { "patch": { "tags": [ "api-data" ], "summary": "Patch Poo", "description": "Update the non-PK fields of a single poo record.\n\n- ``timestamp`` is the PK and is immutable.\n- Only ``status``, ``latitude``, and ``longitude`` may be updated.\n- Omitted body fields are left unchanged.\n- Returns **404** if the PK does not exist.", "operationId": "patch_poo_api_poo__timestamp__patch", "parameters": [ { "name": "timestamp", "in": "path", "required": true, "schema": { "type": "string", "title": "Timestamp" } }, { "name": "X-CSRF-Token", "in": "header", "required": false, "schema": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "X-Csrf-Token" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PooUpdateRequest", "default": {} } } } }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PooRecord" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } }, "delete": { "tags": [ "api-data" ], "summary": "Delete Poo", "description": "Delete the single poo record identified by its PK.\n\n- Exactly one row is deleted; **404** if the PK does not exist.\n- No batch delete / truncate path is available.", "operationId": "delete_poo_api_poo__timestamp__delete", "parameters": [ { "name": "timestamp", "in": "path", "required": true, "schema": { "type": "string", "title": "Timestamp" } }, { "name": "X-CSRF-Token", "in": "header", "required": false, "schema": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "X-Csrf-Token" } } ], "responses": { "204": { "description": "Successful Response" }, "422": { "description": "Validation Error", "content": { "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_fee − 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": [ "api-energy-contracts" ], "summary": "Get Profiles", "description": "List all available pricing profile structures.\n\nReturns the full profile structure for each supported contract kind\n(``manual`` and ``tibber``). The front-end uses this to dynamically\nrender the correct fields and labels for the contract creation/editing form.", "operationId": "get_profiles_api_energy_profiles_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProfilesResponse" } } } } } } }, "/api/energy/contracts": { "get": { "tags": [ "api-energy-contracts" ], "summary": "List Energy Contracts", "description": "List all energy contracts with their active status.\n\nReturns a flat list (no embedded version history); use\nGET /api/energy/contracts/{id} to fetch the full version history for a\nspecific contract.", "operationId": "list_energy_contracts_api_energy_contracts_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContractListResponse" } } } } } }, "post": { "tags": [ "api-energy-contracts" ], "summary": "Create Energy Contract", "description": "Create a new energy contract with an initial pricing version.\n\nThe ``values`` dict is validated against the YAML profile for the given\n``kind``; non-conforming values result in 422 Unprocessable Entity.\nThe new contract is created with ``active=False``; use\nPATCH /api/energy/contracts/{id} with ``active=true`` to activate it.", "operationId": "create_energy_contract_api_energy_contracts_post", "parameters": [ { "name": "X-CSRF-Token", "in": "header", "required": false, "schema": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "X-Csrf-Token" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContractCreate" } } } }, "responses": { "201": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContractDetailResponse" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/energy/contracts/{contract_id}": { "get": { "tags": [ "api-energy-contracts" ], "summary": "Get Energy Contract", "description": "Return a single energy contract with its full version history.\n\nVersions are ordered by ``effective_from`` ascending so the caller can\neasily inspect the pricing timeline.", "operationId": "get_energy_contract_api_energy_contracts__contract_id__get", "parameters": [ { "name": "contract_id", "in": "path", "required": true, "schema": { "type": "integer", "title": "Contract Id" } } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContractDetailResponse" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } }, "patch": { "tags": [ "api-energy-contracts" ], "summary": "Patch Energy Contract", "description": "Partially update a contract: rename or change activation status.\n\n- ``name``: updates the human-readable label.\n- ``active=true``: activates this contract (all others are deactivated).\n- ``active=false``: deactivates this contract (no effect on others).\n\nAt most one contract may be active at any time; the service layer enforces\nmutual exclusion.", "operationId": "patch_energy_contract_api_energy_contracts__contract_id__patch", "parameters": [ { "name": "contract_id", "in": "path", "required": true, "schema": { "type": "integer", "title": "Contract Id" } }, { "name": "X-CSRF-Token", "in": "header", "required": false, "schema": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "X-Csrf-Token" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContractPatch" } } } }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContractDetailResponse" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/energy/contracts/{contract_id}/versions": { "post": { "tags": [ "api-energy-contracts" ], "summary": "Add Contract Version", "description": "Add a new pricing version to an existing contract.\n\nThis is how price changes are recorded: the current open version is\nautomatically closed (its ``effective_to`` is set to ``body.effective_from``)\nand a new version is created starting at ``body.effective_from``.\n\nThe ``values`` dict must conform to the contract's pricing profile.\nNon-conforming values return 422. If ``effective_from`` is not strictly\nafter the previous version's ``effective_from``, 422 is returned without\nwriting any rows.\n\nHistorical versions are never modified; this endpoint is append-only.", "operationId": "add_contract_version_api_energy_contracts__contract_id__versions_post", "parameters": [ { "name": "contract_id", "in": "path", "required": true, "schema": { "type": "integer", "title": "Contract Id" } }, { "name": "X-CSRF-Token", "in": "header", "required": false, "schema": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "X-Csrf-Token" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VersionCreate" } } } }, "responses": { "201": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContractDetailResponse" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/energy/meters": { "get": { "tags": [ "api-energy-meters" ], "summary": "List Energy Meters", "description": "List all meter epochs in ascending ``started_at`` order.\n\nReturns the full historical sequence of meter installations across all\ncommodities. The active meter (``ended_at=null``) appears last because it\nhas the latest ``started_at``.", "operationId": "list_energy_meters_api_energy_meters_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MeterListResponse" } } } } } }, "post": { "tags": [ "api-energy-meters" ], "summary": "Declare Energy Meter", "description": "Declare a new meter epoch (swap, home move, or initial declaration).\n\nCloses the current active meter for the given commodity at ``started_at``\nand opens a new active meter. If no active meter exists, the new meter is\nsimply created without closing anything.\n\n**Validation**: ``started_at`` must be **≥** the current active meter's\nown ``started_at`` (no chronological backdate below the active epoch's\nstart). Equal timestamps are allowed (replaces the current meter at the\nsame logical moment). Violation → 422.\n\n**Retroactive recompute**: if ``started_at`` is in the past, billing\nrecords from that point forward are re-judged via ``recompute_range`` to\nreflect the new meter attribution. The recompute is transparent — the\nresponse body is the created meter (``MeterResponse``) only and does **not**\ninclude a recompute count; callers should re-fetch costs if they need the\nupdated totals.", "operationId": "declare_energy_meter_api_energy_meters_post", "parameters": [ { "name": "X-CSRF-Token", "in": "header", "required": false, "schema": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "X-Csrf-Token" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MeterDeclareRequest" } } } }, "responses": { "201": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MeterResponse" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/energy/meters/{meter_id}": { "patch": { "tags": [ "api-energy-meters" ], "summary": "Patch Energy Meter", "description": "Partially update a meter epoch: rename, edit note, or correct started_at.\n\n- ``label``: updates the human-readable label.\n- ``note``: updates the free-form note.\n- ``started_at``: **retroactive correction** — shifts this meter's start\n boundary. The service layer maintains timeline continuity by also\n updating the preceding meter's ``ended_at``. Validation:\n * Must be strictly after the previous meter's own ``started_at``.\n * Must be strictly before this meter's ``ended_at`` (if closed).\n Violation → 422.\n\n**Retroactive recompute when ``started_at`` changes**: billing records in\nthe window ``[min(old, new), now)`` are re-judged to reflect the corrected\nmeter attribution.\n\nNot found → 404.", "operationId": "patch_energy_meter_api_energy_meters__meter_id__patch", "parameters": [ { "name": "meter_id", "in": "path", "required": true, "schema": { "type": "integer", "title": "Meter Id" } }, { "name": "X-CSRF-Token", "in": "header", "required": false, "schema": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "X-Csrf-Token" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MeterPatchRequest" } } } }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MeterResponse" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/expose": { "get": { "tags": [ "api-expose" ], "summary": "Get Expose", "description": "Return the full exposable-entity catalog with toggle states and MQTT status.\n\nThe catalog is computed dynamically from registered providers (e.g. the\nModbus provider enumerates all enabled devices and their metric entities).\nToggle states come from the ``exposed_entity_toggle`` table; entities with\nno row default to ``enabled=False``.", "operationId": "get_expose_api_expose_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ExposeResponse" } } } } } }, "put": { "tags": [ "api-expose" ], "summary": "Put Expose", "description": "Set per-entity toggle state.\n\nAccepts a map of ``{key: bool}`` and upserts rows in the\n``exposed_entity_toggle`` table. Only keys present in ``body.toggles``\nare touched; other entities' toggles are left unchanged.\n\nAfter writing the toggles, triggers a HA Discovery re-publish so any\nchanges (enabled ↔ disabled) are reflected in Home Assistant immediately.", "operationId": "put_expose_api_expose_put", "parameters": [ { "name": "X-CSRF-Token", "in": "header", "required": false, "schema": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "X-Csrf-Token" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ExposeUpdateRequest" } } } }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ExposeUpdateResponse" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/expose/republish": { "post": { "tags": [ "api-expose" ], "summary": "Post Expose Republish", "description": "Manually trigger a full HA Discovery re-publish.\n\nCalls ``publish_discovery(session)`` from the HA discovery service (M5-T11).\nReturns a status indicating whether the publish was attempted (or skipped\nbecause MQTT / discovery is not enabled / connected).", "operationId": "post_expose_republish_api_expose_republish_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/RepublishResponse" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/modbus/profiles": { "get": { "tags": [ "api-modbus" ], "summary": "Get Profiles", "description": "List all available Modbus YAML profiles (name + description).\n\nIntended for the front-end's device-creation profile drop-down.", "operationId": "get_profiles_api_modbus_profiles_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ModbusProfilesResponse" } } } } } } }, "/api/modbus/devices": { "get": { "tags": [ "api-modbus" ], "summary": "List Devices", "description": "Return all Modbus devices (no pagination — device counts stay small).", "operationId": "list_devices_api_modbus_devices_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ModbusDeviceListResponse" } } } } } }, "post": { "tags": [ "api-modbus" ], "summary": "Create Device", "description": "Create a new Modbus device.\n\n- Validates that the referenced ``profile`` exists; returns 422 if not.\n- Returns 201 with the created device on success.", "operationId": "create_device_api_modbus_devices_post", "parameters": [ { "name": "X-CSRF-Token", "in": "header", "required": false, "schema": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "X-Csrf-Token" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ModbusDeviceCreate" } } } }, "responses": { "201": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ModbusDeviceResponse" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/modbus/devices/{uuid}": { "get": { "tags": [ "api-modbus" ], "summary": "Get Device", "description": "Return a single Modbus device by UUID.", "operationId": "get_device_api_modbus_devices__uuid__get", "parameters": [ { "name": "uuid", "in": "path", "required": true, "schema": { "type": "string", "title": "Uuid" } } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ModbusDeviceResponse" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } }, "patch": { "tags": [ "api-modbus" ], "summary": "Patch Device", "description": "Partially update a Modbus device (including enable/disable).\n\nOnly fields explicitly provided in the request body are updated.\nProviding ``profile`` triggers a profile-existence check (422 if unknown).", "operationId": "patch_device_api_modbus_devices__uuid__patch", "parameters": [ { "name": "uuid", "in": "path", "required": true, "schema": { "type": "string", "title": "Uuid" } }, { "name": "X-CSRF-Token", "in": "header", "required": false, "schema": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "X-Csrf-Token" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ModbusDeviceUpdate" } } } }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ModbusDeviceResponse" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } }, "delete": { "tags": [ "api-modbus" ], "summary": "Delete Device", "description": "Delete a Modbus device.\n\n**Default behaviour (cascade=false)**:\nReturns 409 Conflict if the device has any associated readings; use\n``enabled=false`` to disable it instead.\n\n**Cascade delete (cascade=true)**:\nPermanently deletes the device together with all its readings and any\n``ExposedEntityToggle`` rows whose key matches ``modbus..*``.\nAlso makes a best-effort attempt to clear the device's HA Discovery\nconfig topics from MQTT (empty retained payload) before the DB rows\nare removed. MQTT failures are swallowed — the DB deletion proceeds\nregardless.\nReturns HTTP 200 with a ``ModbusDeleteResponse`` JSON body on success.\n\n**Application-layer safety**: the 409 guard uses an explicit SELECT COUNT\nquery to return a friendly message. FK RESTRICT is enforced at runtime\n(the app sets ``PRAGMA foreign_keys=ON``), so the cascade path deletes\nreadings before the device.", "operationId": "delete_device_api_modbus_devices__uuid__delete", "parameters": [ { "name": "uuid", "in": "path", "required": true, "schema": { "type": "string", "title": "Uuid" } }, { "name": "cascade", "in": "query", "required": false, "schema": { "type": "boolean", "default": false, "title": "Cascade" } }, { "name": "X-CSRF-Token", "in": "header", "required": false, "schema": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "X-Csrf-Token" } } ], "responses": { "204": { "description": "Successful Response" }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/modbus/devices/{uuid}/latest": { "get": { "tags": [ "api-modbus" ], "summary": "Get Latest Reading", "description": "Return the most recent reading for a device.\n\nIf no readings exist yet, returns ``{\"found\": false, \"recorded_at\": null,\n\"payload\": null}`` (200, not 404) so the front-end can distinguish\n\"device exists but has no data\" from \"device not found\".", "operationId": "get_latest_reading_api_modbus_devices__uuid__latest_get", "parameters": [ { "name": "uuid", "in": "path", "required": true, "schema": { "type": "string", "title": "Uuid" } } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ModbusLatestResponse" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/modbus/devices/{uuid}/readings": { "get": { "tags": [ "api-modbus" ], "summary": "Get Readings", "description": "Return time-range readings for a device.\n\nWhen the window contains more rows than ``limit``, the **most recent** N rows\nare returned (``ORDER BY recorded_at DESC LIMIT n``), then reversed to\nascending order before being sent to the client. This ensures that for long\ntime-range requests (e.g. 6 h / 24 h) the caller always sees the latest data\nrather than the oldest segment of the window.\n\nWhen the window has fewer rows than ``limit`` the full window is returned in\nascending order — behaviour is identical to a plain ascending query.\n\nThe response schema is unchanged: items are always ``recorded_at`` ascending.\n\nThe query uses the ``(device_id, recorded_at)`` composite index for\nefficient time-window scans.\n\nQuery parameters:\n- ``start``: inclusive lower bound (ISO8601 datetime)\n- ``end``: inclusive upper bound (ISO8601 datetime)\n- ``limit``: max rows to return (default 500, max {_READINGS_LIMIT_MAX})", "operationId": "get_readings_api_modbus_devices__uuid__readings_get", "parameters": [ { "name": "uuid", "in": "path", "required": true, "schema": { "type": "string", "title": "Uuid" } }, { "name": "start", "in": "query", "required": false, "schema": { "anyOf": [ { "type": "string", "format": "date-time" }, { "type": "null" } ], "title": "Start" } }, { "name": "end", "in": "query", "required": false, "schema": { "anyOf": [ { "type": "string", "format": "date-time" }, { "type": "null" } ], "title": "End" } }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "maximum": 5000, "minimum": 1, "default": 500, "title": "Limit" } } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ModbusReadingsResponse" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/modbus/devices/{uuid}/metrics": { "get": { "tags": [ "api-modbus" ], "summary": "Get Metrics", "description": "Return the metric catalogue for a device's profile.\n\nEach entry has ``key``, ``label`` (derived from key if the profile has\nnone — underscores → spaces, title-cased), ``unit``, and ``device_class``.\nThis is the authoritative metadata source for front-end card labels and\nchart axis labels.", "operationId": "get_metrics_api_modbus_devices__uuid__metrics_get", "parameters": [ { "name": "uuid", "in": "path", "required": true, "schema": { "type": "string", "title": "Uuid" } } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ModbusMetricsResponse" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/modbus/devices/{uuid}/test": { "post": { "tags": [ "api-modbus" ], "summary": "Test Read", "description": "Immediately read and decode the device once without persisting any data.\n\nUseful for validating gateway connectivity and Modbus address settings\nbefore relying on the background polling job.\n\nThis is a write-class endpoint (it initiates network I/O on demand) and\ntherefore requires a CSRF token. The response payload is never stored.", "operationId": "test_read_api_modbus_devices__uuid__test_post", "parameters": [ { "name": "uuid", "in": "path", "required": true, "schema": { "type": "string", "title": "Uuid" } }, { "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/ModbusTestReadResponse" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/session": { "get": { "tags": [ "api-session" ], "summary": "Get Session", "description": "Return the current session user and CSRF token. Returns 401 if not authenticated.", "operationId": "get_session_api_session_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SessionResponse" } } } } } } }, "/api/auth/login": { "post": { "tags": [ "api-session" ], "summary": "Post Login", "description": "Authenticate with username and password.\n\nOn success, sets an HttpOnly session cookie and returns the session user + CSRF token.\nOn failure, returns 401 with no cookie set.\nRepeated failures trigger exponential back-off (429 + Retry-After).\nNo X-CSRF-Token required (unauthenticated endpoint).", "operationId": "post_login_api_auth_login_post", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LoginRequest" } } }, "required": true }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SessionResponse" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/auth/logout": { "post": { "tags": [ "api-session" ], "summary": "Post Logout", "description": "Revoke the current session and clear the session cookie.\nRequires authentication and X-CSRF-Token header.\nReturns 204 No Content.", "operationId": "post_logout_api_auth_logout_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": {} } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/auth/password": { "post": { "tags": [ "api-session" ], "summary": "Post Change Password", "description": "Change the current user's password.\nRequires authentication and X-CSRF-Token header.\nOn AuthPasswordChangeError returns 400 with a generic message.\nOn success, force_password_change becomes False (handled by the service).\nReturns 204 No Content.", "operationId": "post_change_password_api_auth_password_post", "parameters": [ { "name": "X-CSRF-Token", "in": "header", "required": false, "schema": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "X-Csrf-Token" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PasswordChangeRequest" } } } }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/auth/totp/setup": { "post": { "tags": [ "api-session" ], "summary": "Post Totp Setup", "description": "Generate a new pending TOTP secret, otpauth URI, and one-time recovery codes.\n\nThe secret is stored in the DB but TOTP is NOT yet enabled (totp_enabled stays\nFalse until the user confirms with POST /api/auth/totp/enable).\n\nRecovery codes are returned here as plaintext exactly once; their Argon2 hashes\nare persisted immediately so enable only needs to flip the enabled flag.\n\nRepeating this call replaces any prior pending secret and regenerates codes.\n\nRequires: session cookie + X-CSRF-Token.", "operationId": "post_totp_setup_api_auth_totp_setup_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/TotpSetupResponse" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/auth/totp/enable": { "post": { "tags": [ "api-session" ], "summary": "Post Totp Enable", "description": "Enable TOTP by confirming with the current 6-digit code from the authenticator app.\n\nRequires a prior call to POST /api/auth/totp/setup (so that a pending secret\nexists). On success, totp_enabled becomes True.\n\nReturns 400 if the code is wrong or there is no pending secret.\nRequires: session cookie + X-CSRF-Token.", "operationId": "post_totp_enable_api_auth_totp_enable_post", "parameters": [ { "name": "X-CSRF-Token", "in": "header", "required": false, "schema": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "X-Csrf-Token" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TotpEnableRequest" } } } }, "responses": { "204": { "description": "Successful Response" }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/auth/totp/disable": { "post": { "tags": [ "api-session" ], "summary": "Post Totp Disable", "description": "Disable TOTP. The caller must provide exactly one of:\n- ``password``: the user's current login password, OR\n- ``code``: the current 6-digit TOTP code.\n\nOn success: totp_enabled=False, totp_secret cleared, all recovery codes deleted.\nReturns 400 if neither credential matches or neither is provided.\nRequires: session cookie + X-CSRF-Token.", "operationId": "post_totp_disable_api_auth_totp_disable_post", "parameters": [ { "name": "X-CSRF-Token", "in": "header", "required": false, "schema": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "X-Csrf-Token" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TotpDisableRequest" } } } }, "responses": { "204": { "description": "Successful Response" }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/api/auth/totp": { "get": { "tags": [ "api-session" ], "summary": "Get Totp Status", "description": "Return the current TOTP status for the authenticated user.\n\nResponse contains only ``{\"enabled\": bool}``.\nSecret and recovery codes are NEVER returned here.\nRequires: session cookie only (no CSRF — read-only).", "operationId": "get_totp_status_api_auth_totp_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TotpStatusResponse" } } } } } } }, "/homeassistant/publish": { "post": { "tags": [ "homeassistant" ], "summary": "Publish From Homeassistant", "operationId": "publish_from_homeassistant_homeassistant_publish_post", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } } } } }, "/location/record": { "post": { "tags": [ "location" ], "summary": "Create Location Record", "operationId": "create_location_record_location_record_post", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } } } } }, "/poo/record": { "post": { "tags": [ "poo" ], "summary": "Create Poo Record", "operationId": "create_poo_record_poo_record_post", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } } } } }, "/poo/latest": { "get": { "tags": [ "poo" ], "summary": "Notify Latest Poo", "operationId": "notify_latest_poo_poo_latest_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } } } } }, "/public-ip/check": { "get": { "tags": [ "public-ip" ], "summary": "Run Public Ip Check", "operationId": "run_public_ip_check_public_ip_check_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicIPCheckResponse" } } } } } } }, "/ticktick/auth/start": { "get": { "tags": [ "ticktick" ], "summary": "Start Ticktick Auth", "operationId": "start_ticktick_auth_ticktick_auth_start_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } } } } }, "/ticktick/auth/code": { "get": { "tags": [ "ticktick" ], "summary": "Handle Ticktick Auth Code", "operationId": "handle_ticktick_auth_code_ticktick_auth_code_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } } } } } }, "components": { "schemas": { "CatalogEntrySchema": { "properties": { "entity": { "$ref": "#/components/schemas/ExposableEntitySchema" }, "enabled": { "type": "boolean", "title": "Enabled" } }, "type": "object", "required": [ "entity", "enabled" ], "title": "CatalogEntrySchema", "description": "An entity from the catalog with its current toggle state." }, "ConfigField": { "properties": { "env_name": { "type": "string", "title": "Env Name" }, "label": { "type": "string", "title": "Label" }, "value": { "type": "string", "title": "Value" }, "secret": { "type": "boolean", "title": "Secret" }, "input_type": { "type": "string", "title": "Input Type" }, "configured": { "type": "boolean", "title": "Configured" } }, "type": "object", "required": [ "env_name", "label", "value", "secret", "input_type", "configured" ], "title": "ConfigField" }, "ConfigResponse": { "properties": { "sections": { "items": { "$ref": "#/components/schemas/ConfigSection" }, "type": "array", "title": "Sections" } }, "type": "object", "required": [ "sections" ], "title": "ConfigResponse" }, "ConfigSection": { "properties": { "name": { "type": "string", "title": "Name" }, "fields": { "items": { "$ref": "#/components/schemas/ConfigField" }, "type": "array", "title": "Fields" } }, "type": "object", "required": [ "name", "fields" ], "title": "ConfigSection" }, "ConfigUpdateRequest": { "properties": { "updates": { "additionalProperties": { "type": "string" }, "type": "object", "title": "Updates" } }, "type": "object", "required": [ "updates" ], "title": "ConfigUpdateRequest", "description": "Flat mapping of env_name → value, mirroring the existing form semantics." }, "ConfigUpdateResponse": { "properties": { "sections": { "items": { "$ref": "#/components/schemas/ConfigSection" }, "type": "array", "title": "Sections" } }, "type": "object", "required": [ "sections" ], "title": "ConfigUpdateResponse" }, "ContractCreate": { "properties": { "name": { "type": "string", "maxLength": 255, "minLength": 1, "title": "Name" }, "kind": { "type": "string", "maxLength": 32, "minLength": 1, "title": "Kind" }, "currency": { "type": "string", "maxLength": 8, "minLength": 1, "title": "Currency", "default": "EUR" }, "values": { "additionalProperties": true, "type": "object", "title": "Values" }, "effective_from": { "anyOf": [ { "type": "string", "format": "date-time" }, { "type": "null" } ], "title": "Effective From", "description": "UTC datetime from which the first pricing version is effective. Defaults to the current UTC time when omitted." } }, "type": "object", "required": [ "name", "kind", "values" ], "title": "ContractCreate", "description": "Request body for POST /api/energy/contracts.\n\n``effective_from`` defaults to the current UTC time if not provided,\ngiving the first version an open-ended start from \"now\".\n``kind`` is validated at the application layer against the profile registry;\nclients should send ``\"manual\"`` or ``\"tibber\"``." }, "ContractDetailResponse": { "properties": { "id": { "type": "integer", "title": "Id" }, "name": { "type": "string", "title": "Name" }, "kind": { "type": "string", "title": "Kind" }, "active": { "type": "boolean", "title": "Active" }, "currency": { "type": "string", "title": "Currency" }, "created_at": { "type": "string", "format": "date-time", "title": "Created At" }, "updated_at": { "type": "string", "format": "date-time", "title": "Updated At" }, "versions": { "items": { "$ref": "#/components/schemas/ContractVersionResponse" }, "type": "array", "title": "Versions" } }, "type": "object", "required": [ "id", "name", "kind", "active", "currency", "created_at", "updated_at", "versions" ], "title": "ContractDetailResponse", "description": "Response schema for a single EnergyContract with full version history.\n\nReturned by GET /api/energy/contracts/{id} and by successful POST / PATCH\noperations where the caller needs to see all version data." }, "ContractListResponse": { "properties": { "items": { "items": { "$ref": "#/components/schemas/ContractResponse" }, "type": "array", "title": "Items" }, "total": { "type": "integer", "title": "Total" } }, "type": "object", "required": [ "items", "total" ], "title": "ContractListResponse", "description": "Response schema for GET /api/energy/contracts." }, "ContractPatch": { "properties": { "name": { "anyOf": [ { "type": "string", "maxLength": 255, "minLength": 1 }, { "type": "null" } ], "title": "Name" }, "active": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "title": "Active" } }, "type": "object", "title": "ContractPatch", "description": "Request body for PATCH /api/energy/contracts/{id}.\n\nAll fields are optional. Sending ``active=true`` activates this contract\n(deactivating all others); ``active=false`` deactivates it without affecting\nother contracts." }, "ContractResponse": { "properties": { "id": { "type": "integer", "title": "Id" }, "name": { "type": "string", "title": "Name" }, "kind": { "type": "string", "title": "Kind" }, "active": { "type": "boolean", "title": "Active" }, "currency": { "type": "string", "title": "Currency" }, "created_at": { "type": "string", "format": "date-time", "title": "Created At" }, "updated_at": { "type": "string", "format": "date-time", "title": "Updated At" } }, "type": "object", "required": [ "id", "name", "kind", "active", "currency", "created_at", "updated_at" ], "title": "ContractResponse", "description": "Response schema for a single EnergyContract (without embedded versions).\n\nUsed for list responses where embedding all versions would be expensive." }, "ContractVersionResponse": { "properties": { "id": { "type": "integer", "title": "Id" }, "effective_from": { "type": "string", "format": "date-time", "title": "Effective From" }, "effective_to": { "anyOf": [ { "type": "string", "format": "date-time" }, { "type": "null" } ], "title": "Effective To" }, "values": { "additionalProperties": true, "type": "object", "title": "Values" }, "created_at": { "type": "string", "format": "date-time", "title": "Created At" } }, "type": "object", "required": [ "id", "effective_from", "effective_to", "values", "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": { "items": { "type": "string" }, "type": "array", "title": "Identifiers" }, "name": { "type": "string", "title": "Name" } }, "type": "object", "required": [ "identifiers", "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.\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": { "type": "string", "title": "Key" }, "component": { "type": "string", "title": "Component" }, "device": { "$ref": "#/components/schemas/DeviceInfoSchema" }, "device_class": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Device Class" }, "unit": { "type": "string", "title": "Unit" }, "name": { "type": "string", "title": "Name" }, "state_class": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "State Class" } }, "type": "object", "required": [ "key", "component", "device", "device_class", "unit", "name" ], "title": "ExposableEntitySchema", "description": "One exposable entity in the catalog.\n\n``value_getter`` is intentionally excluded — it is a non-serialisable\ncallable and is only used internally by the HA Discovery service." }, "ExposeResponse": { "properties": { "catalog": { "items": { "$ref": "#/components/schemas/CatalogEntrySchema" }, "type": "array", "title": "Catalog" }, "mqtt_status": { "$ref": "#/components/schemas/MqttStatusSchema" } }, "type": "object", "required": [ "catalog", "mqtt_status" ], "title": "ExposeResponse", "description": "Response for GET /api/expose." }, "ExposeUpdateRequest": { "properties": { "toggles": { "additionalProperties": { "type": "boolean" }, "type": "object", "title": "Toggles" } }, "type": "object", "required": [ "toggles" ], "title": "ExposeUpdateRequest", "description": "Request body for PUT /api/expose.\n\n``toggles`` is a map from entity key to desired enabled state (bool).\nOnly keys present in the map are updated; absent keys are untouched." }, "ExposeUpdateResponse": { "properties": { "catalog": { "items": { "$ref": "#/components/schemas/CatalogEntrySchema" }, "type": "array", "title": "Catalog" }, "mqtt_status": { "$ref": "#/components/schemas/MqttStatusSchema" } }, "type": "object", "required": [ "catalog", "mqtt_status" ], "title": "ExposeUpdateResponse", "description": "Response for PUT /api/expose (returns updated catalog + status)." }, "HTTPValidationError": { "properties": { "detail": { "items": { "$ref": "#/components/schemas/ValidationError" }, "type": "array", "title": "Detail" } }, "type": "object", "title": "HTTPValidationError" }, "LocationRecord": { "properties": { "person": { "type": "string", "title": "Person" }, "datetime": { "type": "string", "title": "Datetime" }, "latitude": { "type": "number", "title": "Latitude" }, "longitude": { "type": "number", "title": "Longitude" }, "altitude": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "title": "Altitude" } }, "type": "object", "required": [ "person", "datetime", "latitude", "longitude", "altitude" ], "title": "LocationRecord" }, "LocationUpdateRequest": { "properties": { "latitude": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "title": "Latitude" }, "longitude": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "title": "Longitude" }, "altitude": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "title": "Altitude" } }, "type": "object", "title": "LocationUpdateRequest", "description": "PATCH body for a location record — all fields optional; PK fields excluded." }, "LocationsResponse": { "properties": { "items": { "items": { "$ref": "#/components/schemas/LocationRecord" }, "type": "array", "title": "Items" }, "limit": { "type": "integer", "title": "Limit" }, "offset": { "type": "integer", "title": "Offset" } }, "type": "object", "required": [ "items", "limit", "offset" ], "title": "LocationsResponse" }, "LoginRequest": { "properties": { "username": { "type": "string", "title": "Username" }, "password": { "type": "string", "title": "Password" }, "totp_code": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Totp Code" } }, "type": "object", "required": [ "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.\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." }, "MeterDeclareRequest": { "properties": { "label": { "type": "string", "maxLength": 255, "minLength": 1, "title": "Label" }, "started_at": { "type": "string", "format": "date-time", "title": "Started At", "description": "UTC (or server-local naive) datetime from which this meter epoch starts. May be in the past (retroactive declaration)." }, "reason": { "$ref": "#/components/schemas/MeterReason", "description": "Why this epoch was created. One of: initial, meter_swap, home_move, other." }, "note": { "anyOf": [ { "type": "string", "maxLength": 1024 }, { "type": "null" } ], "title": "Note" }, "commodity": { "type": "string", "maxLength": 32, "minLength": 1, "title": "Commodity", "description": "Energy commodity this meter measures. Defaults to 'electricity'.", "default": "electricity" } }, "type": "object", "required": [ "label", "started_at", "reason" ], "title": "MeterDeclareRequest", "description": "Request body for POST /api/energy/meters.\n\nDeclares a new meter epoch (swap, home move, or initial declaration). The\nservice layer closes the current active meter for the given commodity at\n``started_at`` and opens a new one.\n\n``started_at`` follows the Principle-A localisation convention: a\ntimezone-naive value is interpreted as the **server's local wall-clock time**\n(e.g. CEST midnight → stored as UTC the night before); a timezone-aware\nvalue is converted to UTC as-is. Omitting ``started_at`` is not allowed —\nevery meter declaration must carry an explicit start timestamp.\n\n``commodity`` defaults to ``\"electricity\"``; the field is available for\nfuture use with ``gas`` or ``heating``." }, "MeterListResponse": { "properties": { "items": { "items": { "$ref": "#/components/schemas/MeterResponse" }, "type": "array", "title": "Items" }, "total": { "type": "integer", "title": "Total" } }, "type": "object", "required": [ "items", "total" ], "title": "MeterListResponse", "description": "Response schema for GET /api/energy/meters.\n\nMeters are returned in ascending ``started_at`` order so the caller sees\nthe historical installation sequence." }, "MeterPatchRequest": { "properties": { "label": { "anyOf": [ { "type": "string", "maxLength": 255, "minLength": 1 }, { "type": "null" } ], "title": "Label" }, "note": { "anyOf": [ { "type": "string", "maxLength": 1024 }, { "type": "null" } ], "title": "Note" }, "started_at": { "anyOf": [ { "type": "string", "format": "date-time" }, { "type": "null" } ], "title": "Started At", "description": "Retroactive correction of the meter epoch start timestamp. Triggers billing recompute over the affected window." } }, "type": "object", "title": "MeterPatchRequest", "description": "Request body for PATCH /api/energy/meters/{id}.\n\nAll fields are optional. Only non-``None`` values are applied.\n\nUpdating ``started_at`` is a **retroactive correction**: the service layer\nmaintains timeline continuity (adjusting the preceding meter's ``ended_at``)\nand the API layer triggers ``recompute_range`` over the affected window so\nthat billing attribution is re-judged." }, "MeterReason": { "type": "string", "enum": [ "initial", "meter_swap", "home_move", "other" ], "title": "MeterReason", "description": "Allowed values for the meter epoch creation reason." }, "MeterResponse": { "properties": { "id": { "type": "integer", "title": "Id" }, "label": { "type": "string", "title": "Label" }, "commodity": { "type": "string", "title": "Commodity" }, "started_at": { "type": "string", "format": "date-time", "title": "Started At" }, "ended_at": { "anyOf": [ { "type": "string", "format": "date-time" }, { "type": "null" } ], "title": "Ended At" }, "reason": { "type": "string", "title": "Reason" }, "note": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Note" }, "created_at": { "type": "string", "format": "date-time", "title": "Created At" } }, "type": "object", "required": [ "id", "label", "commodity", "started_at", "ended_at", "reason", "note", "created_at" ], "title": "MeterResponse", "description": "Response schema for a single Meter epoch row.\n\n``ended_at`` is ``null`` for the currently active meter." }, "MetricInfo": { "properties": { "key": { "type": "string", "title": "Key" }, "label": { "type": "string", "title": "Label" }, "unit": { "type": "string", "title": "Unit" }, "device_class": { "type": "string", "title": "Device Class" } }, "type": "object", "required": [ "key", "label", "unit", "device_class" ], "title": "MetricInfo", "description": "Metadata for a single measurable quantity in a device's profile." }, "ModbusDeviceCreate": { "properties": { "friendly_name": { "type": "string", "maxLength": 255, "minLength": 1, "title": "Friendly Name" }, "transport": { "type": "string", "maxLength": 16, "title": "Transport", "default": "tcp" }, "host": { "type": "string", "maxLength": 255, "minLength": 1, "title": "Host" }, "port": { "type": "integer", "maximum": 65535.0, "minimum": 1.0, "title": "Port", "default": 502 }, "unit_id": { "type": "integer", "maximum": 247.0, "minimum": 0.0, "title": "Unit Id", "default": 1 }, "profile": { "type": "string", "maxLength": 64, "minLength": 1, "title": "Profile" }, "poll_interval_s": { "type": "integer", "maximum": 3600.0, "minimum": 1.0, "title": "Poll Interval S", "default": 5 }, "enabled": { "type": "boolean", "title": "Enabled", "default": true } }, "type": "object", "required": [ "friendly_name", "host", "profile" ], "title": "ModbusDeviceCreate", "description": "Request body for POST /api/modbus/devices." }, "ModbusDeviceListResponse": { "properties": { "items": { "items": { "$ref": "#/components/schemas/ModbusDeviceResponse" }, "type": "array", "title": "Items" }, "total": { "type": "integer", "title": "Total" } }, "type": "object", "required": [ "items", "total" ], "title": "ModbusDeviceListResponse", "description": "Response schema for listing Modbus devices." }, "ModbusDeviceResponse": { "properties": { "uuid": { "type": "string", "title": "Uuid" }, "friendly_name": { "type": "string", "title": "Friendly Name" }, "transport": { "type": "string", "title": "Transport" }, "host": { "type": "string", "title": "Host" }, "port": { "type": "integer", "title": "Port" }, "unit_id": { "type": "integer", "title": "Unit Id" }, "profile": { "type": "string", "title": "Profile" }, "poll_interval_s": { "type": "integer", "title": "Poll Interval S" }, "enabled": { "type": "boolean", "title": "Enabled" }, "last_poll_at": { "anyOf": [ { "type": "string", "format": "date-time" }, { "type": "null" } ], "title": "Last Poll At" }, "last_poll_ok": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "title": "Last Poll Ok" }, "created_at": { "type": "string", "format": "date-time", "title": "Created At" }, "updated_at": { "type": "string", "format": "date-time", "title": "Updated At" } }, "type": "object", "required": [ "uuid", "friendly_name", "transport", "host", "port", "unit_id", "profile", "poll_interval_s", "enabled", "last_poll_at", "last_poll_ok", "created_at", "updated_at" ], "title": "ModbusDeviceResponse", "description": "Response schema for a single Modbus device." }, "ModbusDeviceUpdate": { "properties": { "friendly_name": { "anyOf": [ { "type": "string", "maxLength": 255, "minLength": 1 }, { "type": "null" } ], "title": "Friendly Name" }, "transport": { "anyOf": [ { "type": "string", "maxLength": 16 }, { "type": "null" } ], "title": "Transport" }, "host": { "anyOf": [ { "type": "string", "maxLength": 255, "minLength": 1 }, { "type": "null" } ], "title": "Host" }, "port": { "anyOf": [ { "type": "integer", "maximum": 65535.0, "minimum": 1.0 }, { "type": "null" } ], "title": "Port" }, "unit_id": { "anyOf": [ { "type": "integer", "maximum": 247.0, "minimum": 0.0 }, { "type": "null" } ], "title": "Unit Id" }, "profile": { "anyOf": [ { "type": "string", "maxLength": 64, "minLength": 1 }, { "type": "null" } ], "title": "Profile" }, "poll_interval_s": { "anyOf": [ { "type": "integer", "maximum": 3600.0, "minimum": 1.0 }, { "type": "null" } ], "title": "Poll Interval S" }, "enabled": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "title": "Enabled" } }, "type": "object", "title": "ModbusDeviceUpdate", "description": "Request body for PATCH /api/modbus/devices/{uuid} — all fields optional." }, "ModbusLatestResponse": { "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", "recorded_at", "payload" ], "title": "ModbusLatestResponse", "description": "Response for the /latest endpoint.\n\n``found`` is False and ``recorded_at``/``payload`` are None when the device\nhas no readings yet. Callers should check ``found`` before using the values." }, "ModbusMetricsResponse": { "properties": { "profile": { "type": "string", "title": "Profile" }, "metrics": { "items": { "$ref": "#/components/schemas/MetricInfo" }, "type": "array", "title": "Metrics" } }, "type": "object", "required": [ "profile", "metrics" ], "title": "ModbusMetricsResponse", "description": "Response schema for GET /api/modbus/devices/{uuid}/metrics." }, "ModbusProfilesResponse": { "properties": { "profiles": { "items": { "$ref": "#/components/schemas/ProfileSummary" }, "type": "array", "title": "Profiles" } }, "type": "object", "required": [ "profiles" ], "title": "ModbusProfilesResponse", "description": "Response schema for GET /api/modbus/profiles." }, "ModbusReadingResponse": { "properties": { "recorded_at": { "type": "string", "format": "date-time", "title": "Recorded At" }, "payload": { "additionalProperties": true, "type": "object", "title": "Payload" } }, "type": "object", "required": [ "recorded_at", "payload" ], "title": "ModbusReadingResponse", "description": "A single reading row: timestamp + decoded payload." }, "ModbusReadingsResponse": { "properties": { "items": { "items": { "$ref": "#/components/schemas/ModbusReadingResponse" }, "type": "array", "title": "Items" } }, "type": "object", "required": [ "items" ], "title": "ModbusReadingsResponse", "description": "Response schema for the readings time-range endpoint." }, "ModbusTestReadResponse": { "properties": { "ok": { "type": "boolean", "title": "Ok" }, "payload": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "title": "Payload" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Error" } }, "type": "object", "required": [ "ok" ], "title": "ModbusTestReadResponse", "description": "Response for POST /api/modbus/devices/{uuid}/test.\n\nOn success ``ok=True`` and ``payload`` contains the decoded values.\nOn failure ``ok=False`` and ``error`` describes the problem." }, "MqttStatusSchema": { "properties": { "mqtt_configured": { "type": "boolean", "title": "Mqtt Configured" }, "mqtt_connected": { "type": "boolean", "title": "Mqtt Connected" }, "discovery_enabled": { "type": "boolean", "title": "Discovery Enabled" } }, "type": "object", "required": [ "mqtt_configured", "mqtt_connected", "discovery_enabled" ], "title": "MqttStatusSchema", "description": "Connection status for MQTT and HA Discovery." }, "MqttTestResponse": { "properties": { "result": { "type": "string", "enum": [ "success", "config-error", "failed" ], "title": "Result" }, "message": { "type": "string", "title": "Message" } }, "type": "object", "required": [ "result", "message" ], "title": "MqttTestResponse", "description": "Response from POST /api/config/mqtt/test." }, "PasswordChangeRequest": { "properties": { "current_password": { "type": "string", "title": "Current Password" }, "new_password": { "type": "string", "title": "New Password" }, "confirm_password": { "type": "string", "title": "Confirm Password" } }, "type": "object", "required": [ "current_password", "new_password", "confirm_password" ], "title": "PasswordChangeRequest" }, "PooRecord": { "properties": { "timestamp": { "type": "string", "title": "Timestamp" }, "status": { "type": "string", "title": "Status" }, "latitude": { "type": "number", "title": "Latitude" }, "longitude": { "type": "number", "title": "Longitude" } }, "type": "object", "required": [ "timestamp", "status", "latitude", "longitude" ], "title": "PooRecord" }, "PooResponse": { "properties": { "items": { "items": { "$ref": "#/components/schemas/PooRecord" }, "type": "array", "title": "Items" }, "limit": { "type": "integer", "title": "Limit" }, "offset": { "type": "integer", "title": "Offset" } }, "type": "object", "required": [ "items", "limit", "offset" ], "title": "PooResponse" }, "PooUpdateRequest": { "properties": { "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Status" }, "latitude": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "title": "Latitude" }, "longitude": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "title": "Longitude" } }, "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.\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": { "type": "string", "title": "Name" }, "description": { "type": "string", "title": "Description" } }, "type": "object", "required": [ "name", "description" ], "title": "ProfileSummary", "description": "One entry in the GET /api/modbus/profiles response." }, "ProfilesResponse": { "properties": { "profiles": { "items": { "additionalProperties": true, "type": "object" }, "type": "array", "title": "Profiles" } }, "type": "object", "required": [ "profiles" ], "title": "ProfilesResponse", "description": "Response schema for GET /api/energy/profiles.\n\n``profiles`` is a list of raw profile dicts as produced by\n``list_profiles()`` (Pydantic model dumps). Each entry contains at minimum\n``kind`` and ``label``; the full nested structure allows the front-end to\nrender a type-appropriate form for each pricing profile." }, "PublicIPCheckResponse": { "properties": { "status": { "type": "string", "enum": [ "first_seen", "unchanged", "changed", "error" ], "title": "Status" }, "checked_at": { "type": "string", "format": "date-time", "title": "Checked At" }, "changed": { "type": "boolean", "title": "Changed" } }, "type": "object", "required": [ "status", "checked_at", "changed" ], "title": "PublicIPCheckResponse" }, "PublicIPHistorySchema": { "properties": { "id": { "type": "integer", "title": "Id" }, "ipv4": { "type": "string", "title": "Ipv4" }, "observed_at": { "type": "string", "format": "date-time", "title": "Observed At" }, "change_type": { "type": "string", "title": "Change Type" }, "provider": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Provider" } }, "type": "object", "required": [ "id", "ipv4", "observed_at", "change_type", "provider" ], "title": "PublicIPHistorySchema" }, "PublicIPResponse": { "properties": { "state": { "anyOf": [ { "$ref": "#/components/schemas/PublicIPStateSchema" }, { "type": "null" } ] }, "history": { "items": { "$ref": "#/components/schemas/PublicIPHistorySchema" }, "type": "array", "title": "History" } }, "type": "object", "required": [ "state", "history" ], "title": "PublicIPResponse" }, "PublicIPStateSchema": { "properties": { "id": { "type": "integer", "title": "Id" }, "current_ipv4": { "type": "string", "title": "Current Ipv4" }, "previous_ipv4": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Previous Ipv4" }, "first_seen_at": { "type": "string", "format": "date-time", "title": "First Seen At" }, "last_checked_at": { "type": "string", "format": "date-time", "title": "Last Checked At" }, "last_changed_at": { "anyOf": [ { "type": "string", "format": "date-time" }, { "type": "null" } ], "title": "Last Changed At" }, "last_check_status": { "type": "string", "title": "Last Check Status" }, "last_check_error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Last Check Error" }, "last_provider": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Last Provider" } }, "type": "object", "required": [ "id", "current_ipv4", "previous_ipv4", "first_seen_at", "last_checked_at", "last_changed_at", "last_check_status", "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": { "type": "boolean", "title": "Ok" }, "message": { "type": "string", "title": "Message" } }, "type": "object", "required": [ "ok", "message" ], "title": "RepublishResponse", "description": "Response for POST /api/expose/republish." }, "SessionResponse": { "properties": { "user": { "$ref": "#/components/schemas/SessionUser" }, "csrf_token": { "type": "string", "title": "Csrf Token" } }, "type": "object", "required": [ "user", "csrf_token" ], "title": "SessionResponse" }, "SessionUser": { "properties": { "username": { "type": "string", "title": "Username" }, "force_password_change": { "type": "boolean", "title": "Force Password Change" } }, "type": "object", "required": [ "username", "force_password_change" ], "title": "SessionUser" }, "SmtpTestResponse": { "properties": { "result": { "type": "string", "enum": [ "success", "config-error", "failed" ], "title": "Result" }, "message": { "type": "string", "title": "Message" } }, "type": "object", "required": [ "result", "message" ], "title": "SmtpTestResponse", "description": "Response from POST /api/config/smtp/test." }, "StatusResponse": { "properties": { "status": { "type": "string", "title": "Status" } }, "type": "object", "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.\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": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Password" }, "code": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Code" } }, "type": "object", "title": "TotpDisableRequest", "description": "Disable TOTP by proving identity.\n\nExactly one of ``password`` or ``code`` must be provided." }, "TotpEnableRequest": { "properties": { "code": { "type": "string", "title": "Code" } }, "type": "object", "required": [ "code" ], "title": "TotpEnableRequest", "description": "The user confirms setup by providing the 6-digit TOTP code." }, "TotpSetupResponse": { "properties": { "secret": { "type": "string", "title": "Secret" }, "otpauth_uri": { "type": "string", "title": "Otpauth Uri" }, "recovery_codes": { "items": { "type": "string" }, "type": "array", "title": "Recovery Codes" } }, "type": "object", "required": [ "secret", "otpauth_uri", "recovery_codes" ], "title": "TotpSetupResponse", "description": "Returned once after a setup call.\n\n``secret`` and ``recovery_codes`` are **one-time plaintext values**.\nThey are never returned again by any subsequent API call.\nThe frontend must display and instruct the user to save them before confirming." }, "TotpStatusResponse": { "properties": { "enabled": { "type": "boolean", "title": "Enabled" } }, "type": "object", "required": [ "enabled" ], "title": "TotpStatusResponse", "description": "Minimal status response — never exposes secret or recovery codes." }, "ValidationError": { "properties": { "loc": { "items": { "anyOf": [ { "type": "string" }, { "type": "integer" } ] }, "type": "array", "title": "Location" }, "msg": { "type": "string", "title": "Message" }, "type": { "type": "string", "title": "Error Type" } }, "type": "object", "required": [ "loc", "msg", "type" ], "title": "ValidationError" }, "VersionCreate": { "properties": { "effective_from": { "type": "string", "format": "date-time", "title": "Effective From" }, "values": { "additionalProperties": true, "type": "object", "title": "Values" } }, "type": "object", "required": [ "effective_from", "values" ], "title": "VersionCreate", "description": "Request body for POST /api/energy/contracts/{id}/versions." } } } }