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. - Blank secret value keeps the existing stored value (no change). - Invalid values return 422 and nothing is written to the database. - 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. Returns a structured result indicating success or the category of failure. Three possible outcomes: - 200 { "result": "success", "message": ... } - 400 { "result": "config-error", "message": ... } (EmailConfigurationError) - 502 { "result": "failed", "message": ... } (EmailDeliveryError) SMTP 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 test message to ``/home-automation/test``. The message is visible in MQTT Explorer (or any subscriber) so users can confirm the full broker publish path is working. Three possible outcomes: - 200 { "result": "success", "message": ... } - 400 { "result": "config-error", "message": ... } (not configured) - 502 { "result": "failed", "message": ... } (connection/publish error) MQTT 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. - ``start`` / ``end`` are ISO8601 strings; filtering is **inclusive** on both bounds. - Results are ordered by ``datetime`` ascending. - ``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). ``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. - ``state`` is ``null`` if no IP check has been performed yet. - ``history`` is ordered by ``observed_at`` descending (most recent first). - ``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. - ``person`` and ``datetime`` identify the row (composite PK) and are immutable. - Only ``latitude``, ``longitude``, and ``altitude`` may be updated. - Omitted body fields are left unchanged. - 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. - Exactly one row is deleted; **404** if the PK does not exist. - 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. - ``timestamp`` is the PK and is immutable. - Only ``status``, ``latitude``, and ``longitude`` may be updated. - Omitted body fields are left unchanged. - 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. - Exactly one row is deleted; **404** if the PK does not exist. - 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. Rows are ordered by ``period_start`` ascending. When the window contains more rows than ``limit``, the **most recent** N rows are returned (DESC LIMIT), then reversed to ascending order — identical to the modbus readings pattern. Query parameters: - ``start``: inclusive lower bound on ``period_start`` (ISO 8601 datetime). - ``end``: inclusive upper bound on ``period_start`` (ISO 8601 datetime). - ``limit``: max rows to return (default 500, max {_COSTS_LIMIT_MAX}).' operationId: get_costs_api_energy_costs_get parameters: - name: start in: query required: false schema: anyOf: - type: string format: date-time - type: 'null' description: Inclusive lower bound for period_start (ISO 8601). title: Start description: Inclusive lower bound for period_start (ISO 8601). - name: end in: query required: false schema: anyOf: - type: string format: date-time - type: 'null' description: Inclusive upper bound for period_start (ISO 8601). title: End description: Inclusive upper bound for period_start (ISO 8601). - name: limit in: query required: false schema: type: integer maximum: 5000 minimum: 1 description: Maximum number of cost periods to return (default 500, max 5000). default: 500 title: Limit description: Maximum number of cost periods to return (default 500, max 5000). responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/CostsResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /api/energy/costs/summary: get: tags: - api-energy summary: Get Costs Summary description: "Aggregate billing for a time interval.\n\nCalls ``energy_cost.summarize(session,\ \ start, end)`` which computes:\n\n total_payable = Σ(net_cost) + fixed_costs\ \ − credits\n\nwhere ``fixed_costs`` is (network_fee + management_fee) apportioned\ \ to the\ninterval length in days (÷30 per month), and ``credits`` is heffingskorting\n\ apportioned similarly (÷365 per year).\n\nBoth ``fixed_costs`` and ``credits``\ \ are derived from the **currently active\ncontract version at ``end``**.\ \ When no active contract exists they are 0." operationId: get_costs_summary_api_energy_costs_summary_get parameters: - name: start in: query required: false schema: anyOf: - type: string format: date-time - type: 'null' description: Inclusive start of the summary interval (ISO 8601). Defaults to the start of the current UTC day. title: Start description: Inclusive start of the summary interval (ISO 8601). Defaults to the start of the current UTC day. - name: end in: query required: false schema: anyOf: - type: string format: date-time - type: 'null' description: Exclusive end of the summary interval (ISO 8601). Defaults to the start of the next UTC day (i.e. today's full data). title: End description: Exclusive end of the summary interval (ISO 8601). Defaults to the start of the next UTC day (i.e. today's full data). responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/SummaryResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /api/energy/dsmr/latest: get: tags: - api-energy summary: Get Dsmr Latest description: 'Return the most recent dsmr_reading row. Returns ``{"found": false, "recorded_at": null, "payload": null}`` (200, not 404) when no rows exist yet, so the front-end can distinguish "no data" from a server error.' operationId: get_dsmr_latest_api_energy_dsmr_latest_get responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/DsmrLatestResponse' /api/energy/costs/recompute: post: tags: - api-energy summary: Post Recompute description: 'Idempotently recompute billing records in a time window. Calls ``energy_cost.recompute_range(session, start, end)`` which overwrites existing rows (including successful ones) for every UTC quarter-hour boundary in ``[start, end)``. **Idempotency**: repeated calls with the same window produce the same outcome. No rows are deleted; only upserted. **Window constraint**: the maximum allowed range is {_RECOMPUTE_MAX_DAYS} days. Requests exceeding this return 422. Returns the number of periods for which a billing record was written. Periods skipped due to missing contract or missing Tibber price are not counted.' operationId: post_recompute_api_energy_costs_recompute_post parameters: - name: start in: query required: true schema: type: string format: date-time description: Inclusive start of the recompute window (ISO 8601). Required. title: Start description: Inclusive start of the recompute window (ISO 8601). Required. - name: end in: query required: true schema: type: string format: date-time description: Exclusive end of the recompute window (ISO 8601). Required. title: End description: Exclusive end of the recompute window (ISO 8601). Required. - name: X-CSRF-Token in: header required: false schema: anyOf: - type: string - type: 'null' title: X-Csrf-Token responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/RecomputeResponse' '422': description: Validation error (missing window or range too large) /api/energy/tibber/test: post: tags: - api-energy summary: Post Tibber Test description: "Test Tibber API connectivity by fetching the current price point.\n\ \nThree possible outcomes:\n\n- **200** ``{ result: \"success\", message:\ \ ..., price: {...} }``\n The Tibber API responded with a valid current price.\ \ ``price`` contains\n starts_at, total, energy, tax, currency, and level.\n\ \n- **400** ``{ result: \"config-error\", message: ... }``\n The Tibber API\ \ token is empty or not configured.\n\n- **502** ``{ result: \"failed\", \ \ message: ... }``\n The API call failed (authentication rejected, network\ \ error, timeout,\n unexpected response, etc.).\n\nThe API token is **never**\ \ included in the response body or logged." operationId: post_tibber_test_api_energy_tibber_test_post parameters: - name: X-CSRF-Token in: header required: false schema: anyOf: - type: string - type: 'null' title: X-Csrf-Token responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/TibberTestResponse' '400': content: application/json: schema: $ref: '#/components/schemas/TibberTestResponse' description: Bad Request '502': content: application/json: schema: $ref: '#/components/schemas/TibberTestResponse' description: Bad Gateway '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /api/energy/profiles: get: tags: - api-energy-contracts summary: Get Profiles description: 'List all available pricing profile structures. Returns the full profile structure for each supported contract kind (``manual`` and ``tibber``). The front-end uses this to dynamically render 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. Returns a flat list (no embedded version history); use GET /api/energy/contracts/{id} to fetch the full version history for a specific 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. The ``values`` dict is validated against the YAML profile for the given ``kind``; non-conforming values result in 422 Unprocessable Entity. The new contract is created with ``active=False``; use PATCH /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. Versions are ordered by ``effective_from`` ascending so the caller can easily 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. - ``name``: updates the human-readable label. - ``active=true``: activates this contract (all others are deactivated). - ``active=false``: deactivates this contract (no effect on others). At most one contract may be active at any time; the service layer enforces mutual 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. This is how price changes are recorded: the current open version is automatically closed (its ``effective_to`` is set to ``body.effective_from``) and a new version is created starting at ``body.effective_from``. The ``values`` dict must conform to the contract''s pricing profile. Non-conforming values return 422. If ``effective_from`` is not strictly after the previous version''s ``effective_from``, 422 is returned without writing any rows. Historical 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. Returns the full historical sequence of meter installations across all commodities. The active meter (``ended_at=null``) appears last because it has 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). Closes the current active meter for the given commodity at ``started_at`` and opens a new active meter. If no active meter exists, the new meter is simply created without closing anything. **Validation**: ``started_at`` must be **≥** the current active meter''s own ``started_at`` (no chronological backdate below the active epoch''s start). Equal timestamps are allowed (replaces the current meter at the same logical moment). Violation → 422. **Retroactive recompute**: if ``started_at`` is in the past, billing records from that point forward are re-judged via ``recompute_range`` to reflect the new meter attribution. The recompute is transparent — the response body is the created meter (``MeterResponse``) only and does **not** include a recompute count; callers should re-fetch costs if they need the updated 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. The catalog is computed dynamically from registered providers (e.g. the Modbus provider enumerates all enabled devices and their metric entities). Toggle states come from the ``exposed_entity_toggle`` table; entities with no 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. Accepts a map of ``{key: bool}`` and upserts rows in the ``exposed_entity_toggle`` table. Only keys present in ``body.toggles`` are touched; other entities'' toggles are left unchanged. After writing the toggles, triggers a HA Discovery re-publish so any changes (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. Calls ``publish_discovery(session)`` from the HA discovery service (M5-T11). Returns a status indicating whether the publish was attempted (or skipped because 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). Intended 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. - Validates that the referenced ``profile`` exists; returns 422 if not. - 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). Only fields explicitly provided in the request body are updated. Providing ``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. **Default behaviour (cascade=false)**: Returns 409 Conflict if the device has any associated readings; use ``enabled=false`` to disable it instead. **Cascade delete (cascade=true)**: Permanently deletes the device together with all its readings and any ``ExposedEntityToggle`` rows whose key matches ``modbus..*``. Also makes a best-effort attempt to clear the device''s HA Discovery config topics from MQTT (empty retained payload) before the DB rows are removed. MQTT failures are swallowed — the DB deletion proceeds regardless. Returns HTTP 200 with a ``ModbusDeleteResponse`` JSON body on success. **Application-layer safety**: the 409 guard uses an explicit SELECT COUNT query to return a friendly message. FK RESTRICT is enforced at runtime (the app sets ``PRAGMA foreign_keys=ON``), so the cascade path deletes readings 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. If no readings exist yet, returns ``{"found": false, "recorded_at": null, "payload": null}`` (200, not 404) so the front-end can distinguish "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. When the window contains more rows than ``limit``, the **most recent** N rows are returned (``ORDER BY recorded_at DESC LIMIT n``), then reversed to ascending order before being sent to the client. This ensures that for long time-range requests (e.g. 6 h / 24 h) the caller always sees the latest data rather than the oldest segment of the window. When the window has fewer rows than ``limit`` the full window is returned in ascending order — behaviour is identical to a plain ascending query. The response schema is unchanged: items are always ``recorded_at`` ascending. The query uses the ``(device_id, recorded_at)`` composite index for efficient time-window scans. Query parameters: - ``start``: inclusive lower bound (ISO8601 datetime) - ``end``: inclusive upper bound (ISO8601 datetime) - ``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. Each entry has ``key``, ``label`` (derived from key if the profile has none — underscores → spaces, title-cased), ``unit``, and ``device_class``. This is the authoritative metadata source for front-end card labels and chart 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. Useful for validating gateway connectivity and Modbus address settings before relying on the background polling job. This is a write-class endpoint (it initiates network I/O on demand) and therefore 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. On success, sets an HttpOnly session cookie and returns the session user + CSRF token. On failure, returns 401 with no cookie set. Repeated failures trigger exponential back-off (429 + Retry-After). No 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. Requires authentication and X-CSRF-Token header. Returns 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. Requires authentication and X-CSRF-Token header. On AuthPasswordChangeError returns 400 with a generic message. On success, force_password_change becomes False (handled by the service). Returns 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. The secret is stored in the DB but TOTP is NOT yet enabled (totp_enabled stays False until the user confirms with POST /api/auth/totp/enable). Recovery codes are returned here as plaintext exactly once; their Argon2 hashes are persisted immediately so enable only needs to flip the enabled flag. Repeating this call replaces any prior pending secret and regenerates codes. Requires: 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. Requires a prior call to POST /api/auth/totp/setup (so that a pending secret exists). On success, totp_enabled becomes True. Returns 400 if the code is wrong or there is no pending secret. Requires: 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: - ``password``: the user''s current login password, OR - ``code``: the current 6-digit TOTP code. On success: totp_enabled=False, totp_secret cleared, all recovery codes deleted. Returns 400 if neither credential matches or neither is provided. Requires: 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. Response contains only ``{"enabled": bool}``. Secret and recovery codes are NEVER returned here. Requires: 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. ``effective_from`` defaults to the current UTC time if not provided, giving the first version an open-ended start from "now". ``kind`` is validated at the application layer against the profile registry; clients 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. Returned by GET /api/energy/contracts/{id} and by successful POST / PATCH operations 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}. All fields are optional. Sending ``active=true`` activates this contract (deactivating all others); ``active=false`` deactivates it without affecting other 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). Used 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. ``found`` is False when no dsmr_reading rows exist yet. The front-end should check ``found`` before reading ``recorded_at`` or ``payload``.' ExposableEntitySchema: properties: key: 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. ``value_getter`` is intentionally excluded — it is a non-serialisable callable 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. ``toggles`` is a map from entity key to desired enabled state (bool). Only 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. Prices are the *effective* buy prices as used by the billing engine (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. Declares a new meter epoch (swap, home move, or initial declaration). The service layer closes the current active meter for the given commodity at ``started_at`` and opens a new one. ``started_at`` follows the Principle-A localisation convention: a timezone-naive value is interpreted as the **server''s local wall-clock time** (e.g. CEST midnight → stored as UTC the night before); a timezone-aware value is converted to UTC as-is. Omitting ``started_at`` is not allowed — every meter declaration must carry an explicit start timestamp. ``commodity`` defaults to ``"electricity"``; the field is available for future 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. Meters are returned in ascending ``started_at`` order so the caller sees the 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}. All fields are optional. Only non-``None`` values are applied. Updating ``started_at`` is a **retroactive correction**: the service layer maintains timeline continuity (adjusting the preceding meter''s ``ended_at``) and the API layer triggers ``recompute_range`` over the affected window so that 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. ``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. ``found`` is False and ``recorded_at``/``payload`` are None when the device has 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. On success ``ok=True`` and ``payload`` contains the decoded values. On 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. ``kind`` mirrors the active contract kind: - ``"tibber"`` → ``points`` has actual 15-min price entries; ``tariff`` is null. - ``"manual"`` → ``points`` is empty; ``tariff`` carries the fixed-rate table. - ``None`` → no active contract; both ``points`` and ``tariff`` are empty/null. ``currency`` comes from the active contract (or "EUR" fallback). ``points`` is always ascending by ``starts_at``.' ProfileSummary: properties: name: 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. ``profiles`` is a list of raw profile dicts as produced by ``list_profiles()`` (Pydantic model dumps). Each entry contains at minimum ``kind`` and ``label``; the full nested structure allows the front-end to render 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. All monetary values are in ``currency``. ``total_payable = metered_net + fixed_costs − credits``' TibberTestPriceSchema: properties: starts_at: type: string format: date-time title: Starts At total: type: number title: Total energy: type: number title: Energy tax: type: number title: Tax currency: type: string title: Currency level: anyOf: - type: string - type: 'null' title: Level type: object required: - starts_at - total - energy - tax - currency title: TibberTestPriceSchema description: 'Current Tibber price point returned on a successful test. Carries enough fields for the front-end to confirm the API is working and display the live price. The API token is **never** included.' TibberTestResponse: properties: result: type: string enum: - success - config-error - failed title: Result message: type: string title: Message price: anyOf: - $ref: '#/components/schemas/TibberTestPriceSchema' - type: 'null' type: object required: - result - message title: TibberTestResponse description: 'Three-state response for POST /api/energy/tibber/test. Possible ``result`` values: - ``"success"`` — Tibber API responded with a valid price. - ``"config-error"`` — Token is missing or not configured. - ``"failed"`` — API call failed (auth rejected, network error, timeout, etc.). ``price`` is populated only on ``"success"``; it is null otherwise. ``message`` always contains a human-readable explanation.' TotpDisableRequest: properties: password: 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. Exactly 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. ``secret`` and ``recovery_codes`` are **one-time plaintext values**. They are never returned again by any subsequent API call. The 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.