{
  "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 ``<ha_discovery_prefix>/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.<uuid>.*``.\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."
      }
    }
  }
}