openapi: 3.1.0
info:
  title: Home Automation Backend (Python)
  description: Home automation backend with auth, runtime config, Home Assistant integrations,
    TickTick integration, and SQLite-backed recorders.
  version: 0.1.0
paths:
  /status:
    get:
      tags:
      - system
      summary: Get Status
      operationId: get_status_status_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusResponse'
  /api/config:
    get:
      tags:
      - api-config
      summary: Get Config
      description: Return all configuration sections. Secret field values are masked
        (empty string).
      operationId: get_config_api_config_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConfigResponse'
    put:
      tags:
      - api-config
      summary: Put Config
      description: 'Save configuration updates.


        - Blank secret value keeps the existing stored value (no change).

        - Invalid values return 422 and nothing is written to the database.

        - If MQTT-related settings changed, the MQTT client reconnects automatically.'
      operationId: put_config_api_config_put
      parameters:
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConfigUpdateRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConfigUpdateResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/config/smtp/test:
    post:
      tags:
      - api-config
      summary: Post Smtp Test
      description: 'Send a test SMTP email using the current runtime settings.


        Returns a structured result indicating success or the category of failure.

        Three possible outcomes:

        - 200 { "result": "success", "message": ... }

        - 400 { "result": "config-error", "message": ... }   (EmailConfigurationError)

        - 502 { "result": "failed", "message": ... }          (EmailDeliveryError)


        SMTP credentials are never echoed in the response.'
      operationId: post_smtp_test_api_config_smtp_test_post
      parameters:
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SmtpTestResponse'
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SmtpTestResponse'
          description: Bad Request
        '502':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SmtpTestResponse'
          description: Bad Gateway
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/config/mqtt/test:
    post:
      tags:
      - api-config
      summary: Post Mqtt Test
      description: 'Test MQTT broker connectivity by attempting to connect and publishing
        a

        test message to ``<ha_discovery_prefix>/home-automation/test``.


        The message is visible in MQTT Explorer (or any subscriber) so users can

        confirm the full broker publish path is working.


        Three possible outcomes:

        - 200 { "result": "success", "message": ... }

        - 400 { "result": "config-error", "message": ... }   (not configured)

        - 502 { "result": "failed", "message": ... }          (connection/publish
        error)


        MQTT credentials are never echoed in the response.'
      operationId: post_mqtt_test_api_config_mqtt_test_post
      parameters:
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MqttTestResponse'
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MqttTestResponse'
          description: Bad Request
        '502':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MqttTestResponse'
          description: Bad Gateway
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/locations:
    get:
      tags:
      - api-data
      summary: Get Locations
      description: 'Return location records with optional time-window filtering and
        pagination.


        - ``start`` / ``end`` are ISO8601 strings; filtering is **inclusive** on both
        bounds.

        - Results are ordered by ``datetime`` ascending.

        - ``limit`` is capped at 5000 to prevent full-table exports.'
      operationId: get_locations_api_locations_get
      parameters:
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          maximum: 5000
          minimum: 1
          default: 1000
          title: Limit
      - name: offset
        in: query
        required: false
        schema:
          type: integer
          minimum: 0
          default: 0
          title: Offset
      - name: start
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Start
      - name: end
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: End
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LocationsResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/poo:
    get:
      tags:
      - api-data
      summary: Get Poo
      description: 'Return poo records ordered by timestamp descending (most recent
        first).


        ``limit`` is capped at 1000 to prevent full-table exports.'
      operationId: get_poo_api_poo_get
      parameters:
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          maximum: 1000
          minimum: 1
          default: 100
          title: Limit
      - name: offset
        in: query
        required: false
        schema:
          type: integer
          minimum: 0
          default: 0
          title: Offset
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PooResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/public-ip:
    get:
      tags:
      - api-data
      summary: Get Public Ip
      description: 'Return the current public IP state and recent history.


        - ``state`` is ``null`` if no IP check has been performed yet.

        - ``history`` is ordered by ``observed_at`` descending (most recent first).

        - ``limit`` applies to the history list and is capped at 1000.'
      operationId: get_public_ip_api_public_ip_get
      parameters:
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          maximum: 1000
          minimum: 1
          default: 100
          title: Limit
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicIPResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/locations/{person}/{datetime}:
    patch:
      tags:
      - api-data
      summary: Patch Location
      description: 'Update the non-PK fields of a single location record.


        - ``person`` and ``datetime`` identify the row (composite PK) and are immutable.

        - Only ``latitude``, ``longitude``, and ``altitude`` may be updated.

        - Omitted body fields are left unchanged.

        - Returns **404** if the PK does not exist.'
      operationId: patch_location_api_locations__person___datetime__patch
      parameters:
      - name: person
        in: path
        required: true
        schema:
          type: string
          title: Person
      - name: datetime
        in: path
        required: true
        schema:
          type: string
          title: Datetime
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LocationUpdateRequest'
              default: {}
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LocationRecord'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
    delete:
      tags:
      - api-data
      summary: Delete Location Record
      description: 'Delete the single location record identified by its composite
        PK.


        - Exactly one row is deleted; **404** if the PK does not exist.

        - No batch delete / truncate path is available.'
      operationId: delete_location_record_api_locations__person___datetime__delete
      parameters:
      - name: person
        in: path
        required: true
        schema:
          type: string
          title: Person
      - name: datetime
        in: path
        required: true
        schema:
          type: string
          title: Datetime
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      responses:
        '204':
          description: Successful Response
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/poo/{timestamp}:
    patch:
      tags:
      - api-data
      summary: Patch Poo
      description: 'Update the non-PK fields of a single poo record.


        - ``timestamp`` is the PK and is immutable.

        - Only ``status``, ``latitude``, and ``longitude`` may be updated.

        - Omitted body fields are left unchanged.

        - Returns **404** if the PK does not exist.'
      operationId: patch_poo_api_poo__timestamp__patch
      parameters:
      - name: timestamp
        in: path
        required: true
        schema:
          type: string
          title: Timestamp
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PooUpdateRequest'
              default: {}
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PooRecord'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
    delete:
      tags:
      - api-data
      summary: Delete Poo
      description: 'Delete the single poo record identified by its PK.


        - Exactly one row is deleted; **404** if the PK does not exist.

        - No batch delete / truncate path is available.'
      operationId: delete_poo_api_poo__timestamp__delete
      parameters:
      - name: timestamp
        in: path
        required: true
        schema:
          type: string
          title: Timestamp
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      responses:
        '204':
          description: Successful Response
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/energy/prices:
    get:
      tags:
      - api-energy
      summary: Get Prices
      description: "Return the price curve for the active contract.\n\n**Tibber contracts**\
        \ (kind=\"tibber\"):\n    Fetches ``tibber_price`` rows within ``[start, end]``,\
        \ ordered ascending\n    by ``starts_at``.  At most ``limit`` rows are returned\
        \ (most recent first\n    within the window, then reversed to ascending order\
        \ — identical to the\n    modbus readings pattern).\n\n    Response ``points``\
        \ carries per-slot:\n    - ``buy  = total``              (Tibber all-inclusive\
        \ price)\n    - ``sell = total − energy_tax − sell_fee − sell_adjust``  (from\
        \ active version values)\n    - ``level``                     (Tibber price\
        \ level, may be null)\n\n    ``tariff`` is null.\n\n**Manual contracts** (kind=\"\
        manual\"):\n    ``points`` is empty.  ``tariff`` carries the four effective\
        \ prices\n    derived using the billing engine formula:\n    - ``buy_dal \
        \   = energy.buy.dal    + energy_tax + ode``\n    - ``buy_normal = energy.buy.normal\
        \ + energy_tax + ode``\n    - ``sell_dal   = energy.sell.dal``\n    - ``sell_normal\
        \ = energy.sell.normal``\n\n**No active contract**: returns kind=null, currency=\"\
        EUR\", points=[], tariff=null (200)."
      operationId: get_prices_api_energy_prices_get
      parameters:
      - name: start
        in: query
        required: false
        schema:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          description: Inclusive start of the time window (ISO 8601). Defaults to
            the start of today UTC when omitted.
          title: Start
        description: Inclusive start of the time window (ISO 8601). Defaults to the
          start of today UTC when omitted.
      - name: end
        in: query
        required: false
        schema:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          description: Inclusive end of the time window (ISO 8601). Defaults to the
            end of tomorrow UTC when omitted.
          title: End
        description: Inclusive end of the time window (ISO 8601). Defaults to the
          end of tomorrow UTC when omitted.
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          maximum: 5000
          minimum: 1
          description: Maximum number of Tibber price points to return.
          default: 500
          title: Limit
        description: Maximum number of Tibber price points to return.
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PricesResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/energy/costs:
    get:
      tags:
      - api-energy
      summary: Get Costs
      description: 'Return energy_cost_period rows within a time window.


        Rows are ordered by ``period_start`` ascending.  When the window contains

        more rows than ``limit``, the **most recent** N rows are returned (DESC LIMIT),

        then reversed to ascending order — identical to the modbus readings pattern.


        Query parameters:

        - ``start``: inclusive lower bound on ``period_start`` (ISO 8601 datetime).

        - ``end``:   inclusive upper bound on ``period_start`` (ISO 8601 datetime).

        - ``limit``: max rows to return (default 500, max {_COSTS_LIMIT_MAX}).'
      operationId: get_costs_api_energy_costs_get
      parameters:
      - name: start
        in: query
        required: false
        schema:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          description: Inclusive lower bound for period_start (ISO 8601).
          title: Start
        description: Inclusive lower bound for period_start (ISO 8601).
      - name: end
        in: query
        required: false
        schema:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          description: Inclusive upper bound for period_start (ISO 8601).
          title: End
        description: Inclusive upper bound for period_start (ISO 8601).
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          maximum: 5000
          minimum: 1
          description: Maximum number of cost periods to return (default 500, max
            5000).
          default: 500
          title: Limit
        description: Maximum number of cost periods to return (default 500, max 5000).
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CostsResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/energy/costs/summary:
    get:
      tags:
      - api-energy
      summary: Get Costs Summary
      description: "Aggregate billing for a time interval.\n\nCalls ``energy_cost.summarize(session,\
        \ start, end)`` which computes:\n\n    total_payable = Σ(net_cost) + fixed_costs\
        \ − credits\n\nwhere ``fixed_costs`` is (network_fee + management_fee) apportioned\
        \ to the\ninterval length in days (÷30 per month), and ``credits`` is heffingskorting\n\
        apportioned similarly (÷365 per year).\n\nBoth ``fixed_costs`` and ``credits``\
        \ are derived from the **currently active\ncontract version at ``end``**.\
        \  When no active contract exists they are 0."
      operationId: get_costs_summary_api_energy_costs_summary_get
      parameters:
      - name: start
        in: query
        required: false
        schema:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          description: Inclusive start of the summary interval (ISO 8601). Defaults
            to the start of the current UTC day.
          title: Start
        description: Inclusive start of the summary interval (ISO 8601). Defaults
          to the start of the current UTC day.
      - name: end
        in: query
        required: false
        schema:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          description: Exclusive end of the summary interval (ISO 8601). Defaults
            to the start of the next UTC day (i.e. today's full data).
          title: End
        description: Exclusive end of the summary interval (ISO 8601). Defaults to
          the start of the next UTC day (i.e. today's full data).
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SummaryResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/energy/dsmr/latest:
    get:
      tags:
      - api-energy
      summary: Get Dsmr Latest
      description: 'Return the most recent dsmr_reading row.


        Returns ``{"found": false, "recorded_at": null, "payload": null}`` (200, not

        404) when no rows exist yet, so the front-end can distinguish "no data" from

        a server error.'
      operationId: get_dsmr_latest_api_energy_dsmr_latest_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DsmrLatestResponse'
  /api/energy/costs/recompute:
    post:
      tags:
      - api-energy
      summary: Post Recompute
      description: 'Idempotently recompute billing records in a time window.


        Calls ``energy_cost.recompute_range(session, start, end)`` which overwrites

        existing rows (including successful ones) for every UTC quarter-hour boundary

        in ``[start, end)``.


        **Idempotency**: repeated calls with the same window produce the same

        outcome.  No rows are deleted; only upserted.


        **Window constraint**: the maximum allowed range is {_RECOMPUTE_MAX_DAYS}
        days.

        Requests exceeding this return 422.


        Returns the number of periods for which a billing record was written.

        Periods skipped due to missing contract or missing Tibber price are not counted.'
      operationId: post_recompute_api_energy_costs_recompute_post
      parameters:
      - name: start
        in: query
        required: true
        schema:
          type: string
          format: date-time
          description: Inclusive start of the recompute window (ISO 8601). Required.
          title: Start
        description: Inclusive start of the recompute window (ISO 8601). Required.
      - name: end
        in: query
        required: true
        schema:
          type: string
          format: date-time
          description: Exclusive end of the recompute window (ISO 8601). Required.
          title: End
        description: Exclusive end of the recompute window (ISO 8601). Required.
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecomputeResponse'
        '422':
          description: Validation error (missing window or range too large)
  /api/energy/tibber/test:
    post:
      tags:
      - api-energy
      summary: Post Tibber Test
      description: "Test Tibber API connectivity by fetching the current price point.\n\
        \nThree possible outcomes:\n\n- **200** ``{ result: \"success\",      message:\
        \ ..., price: {...} }``\n  The Tibber API responded with a valid current price.\
        \  ``price`` contains\n  starts_at, total, energy, tax, currency, and level.\n\
        \n- **400** ``{ result: \"config-error\", message: ... }``\n  The Tibber API\
        \ token is empty or not configured.\n\n- **502** ``{ result: \"failed\", \
        \      message: ... }``\n  The API call failed (authentication rejected, network\
        \ error, timeout,\n  unexpected response, etc.).\n\nThe API token is **never**\
        \ included in the response body or logged."
      operationId: post_tibber_test_api_energy_tibber_test_post
      parameters:
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TibberTestResponse'
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TibberTestResponse'
          description: Bad Request
        '502':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TibberTestResponse'
          description: Bad Gateway
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/energy/profiles:
    get:
      tags:
      - api-energy-contracts
      summary: Get Profiles
      description: 'List all available pricing profile structures.


        Returns the full profile structure for each supported contract kind

        (``manual`` and ``tibber``).  The front-end uses this to dynamically

        render the correct fields and labels for the contract creation/editing form.'
      operationId: get_profiles_api_energy_profiles_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProfilesResponse'
  /api/energy/contracts:
    get:
      tags:
      - api-energy-contracts
      summary: List Energy Contracts
      description: 'List all energy contracts with their active status.


        Returns a flat list (no embedded version history); use

        GET /api/energy/contracts/{id} to fetch the full version history for a

        specific contract.'
      operationId: list_energy_contracts_api_energy_contracts_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContractListResponse'
    post:
      tags:
      - api-energy-contracts
      summary: Create Energy Contract
      description: 'Create a new energy contract with an initial pricing version.


        The ``values`` dict is validated against the YAML profile for the given

        ``kind``; non-conforming values result in 422 Unprocessable Entity.

        The new contract is created with ``active=False``; use

        PATCH /api/energy/contracts/{id} with ``active=true`` to activate it.'
      operationId: create_energy_contract_api_energy_contracts_post
      parameters:
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContractCreate'
      responses:
        '201':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContractDetailResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/energy/contracts/{contract_id}:
    get:
      tags:
      - api-energy-contracts
      summary: Get Energy Contract
      description: 'Return a single energy contract with its full version history.


        Versions are ordered by ``effective_from`` ascending so the caller can

        easily inspect the pricing timeline.'
      operationId: get_energy_contract_api_energy_contracts__contract_id__get
      parameters:
      - name: contract_id
        in: path
        required: true
        schema:
          type: integer
          title: Contract Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContractDetailResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
    patch:
      tags:
      - api-energy-contracts
      summary: Patch Energy Contract
      description: 'Partially update a contract: rename or change activation status.


        - ``name``: updates the human-readable label.

        - ``active=true``: activates this contract (all others are deactivated).

        - ``active=false``: deactivates this contract (no effect on others).


        At most one contract may be active at any time; the service layer enforces

        mutual exclusion.'
      operationId: patch_energy_contract_api_energy_contracts__contract_id__patch
      parameters:
      - name: contract_id
        in: path
        required: true
        schema:
          type: integer
          title: Contract Id
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContractPatch'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContractDetailResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/energy/contracts/{contract_id}/versions:
    post:
      tags:
      - api-energy-contracts
      summary: Add Contract Version
      description: 'Add a new pricing version to an existing contract.


        This is how price changes are recorded: the current open version is

        automatically closed (its ``effective_to`` is set to ``body.effective_from``)

        and a new version is created starting at ``body.effective_from``.


        The ``values`` dict must conform to the contract''s pricing profile.

        Non-conforming values return 422.  If ``effective_from`` is not strictly

        after the previous version''s ``effective_from``, 422 is returned without

        writing any rows.


        Historical versions are never modified; this endpoint is append-only.'
      operationId: add_contract_version_api_energy_contracts__contract_id__versions_post
      parameters:
      - name: contract_id
        in: path
        required: true
        schema:
          type: integer
          title: Contract Id
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VersionCreate'
      responses:
        '201':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContractDetailResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/energy/meters:
    get:
      tags:
      - api-energy-meters
      summary: List Energy Meters
      description: 'List all meter epochs in ascending ``started_at`` order.


        Returns the full historical sequence of meter installations across all

        commodities.  The active meter (``ended_at=null``) appears last because it

        has the latest ``started_at``.'
      operationId: list_energy_meters_api_energy_meters_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeterListResponse'
    post:
      tags:
      - api-energy-meters
      summary: Declare Energy Meter
      description: 'Declare a new meter epoch (swap, home move, or initial declaration).


        Closes the current active meter for the given commodity at ``started_at``

        and opens a new active meter.  If no active meter exists, the new meter is

        simply created without closing anything.


        **Validation**: ``started_at`` must be **≥** the current active meter''s

        own ``started_at`` (no chronological backdate below the active epoch''s

        start).  Equal timestamps are allowed (replaces the current meter at the

        same logical moment).  Violation → 422.


        **Retroactive recompute**: if ``started_at`` is in the past, billing

        records from that point forward are re-judged via ``recompute_range`` to

        reflect the new meter attribution.  The recompute is transparent — the

        response body is the created meter (``MeterResponse``) only and does **not**

        include a recompute count; callers should re-fetch costs if they need the

        updated totals.'
      operationId: declare_energy_meter_api_energy_meters_post
      parameters:
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MeterDeclareRequest'
      responses:
        '201':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeterResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/energy/meters/{meter_id}:
    patch:
      tags:
      - api-energy-meters
      summary: Patch Energy Meter
      description: "Partially update a meter epoch: rename, edit note, or correct\
        \ started_at.\n\n- ``label``: updates the human-readable label.\n- ``note``:\
        \ updates the free-form note.\n- ``started_at``: **retroactive correction**\
        \ — shifts this meter's start\n  boundary.  The service layer maintains timeline\
        \ continuity by also\n  updating the preceding meter's ``ended_at``.  Validation:\n\
        \    * Must be strictly after the previous meter's own ``started_at``.\n \
        \   * Must be strictly before this meter's ``ended_at`` (if closed).\n  Violation\
        \ → 422.\n\n**Retroactive recompute when ``started_at`` changes**: billing\
        \ records in\nthe window ``[min(old, new), now)`` are re-judged to reflect\
        \ the corrected\nmeter attribution.\n\nNot found → 404."
      operationId: patch_energy_meter_api_energy_meters__meter_id__patch
      parameters:
      - name: meter_id
        in: path
        required: true
        schema:
          type: integer
          title: Meter Id
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MeterPatchRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeterResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/expose:
    get:
      tags:
      - api-expose
      summary: Get Expose
      description: 'Return the full exposable-entity catalog with toggle states and
        MQTT status.


        The catalog is computed dynamically from registered providers (e.g. the

        Modbus provider enumerates all enabled devices and their metric entities).

        Toggle states come from the ``exposed_entity_toggle`` table; entities with

        no row default to ``enabled=False``.'
      operationId: get_expose_api_expose_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExposeResponse'
    put:
      tags:
      - api-expose
      summary: Put Expose
      description: 'Set per-entity toggle state.


        Accepts a map of ``{key: bool}`` and upserts rows in the

        ``exposed_entity_toggle`` table.  Only keys present in ``body.toggles``

        are touched; other entities'' toggles are left unchanged.


        After writing the toggles, triggers a HA Discovery re-publish so any

        changes (enabled ↔ disabled) are reflected in Home Assistant immediately.'
      operationId: put_expose_api_expose_put
      parameters:
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExposeUpdateRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExposeUpdateResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/expose/republish:
    post:
      tags:
      - api-expose
      summary: Post Expose Republish
      description: 'Manually trigger a full HA Discovery re-publish.


        Calls ``publish_discovery(session)`` from the HA discovery service (M5-T11).

        Returns a status indicating whether the publish was attempted (or skipped

        because MQTT / discovery is not enabled / connected).'
      operationId: post_expose_republish_api_expose_republish_post
      parameters:
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RepublishResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/modbus/profiles:
    get:
      tags:
      - api-modbus
      summary: Get Profiles
      description: 'List all available Modbus YAML profiles (name + description).


        Intended for the front-end''s device-creation profile drop-down.'
      operationId: get_profiles_api_modbus_profiles_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModbusProfilesResponse'
  /api/modbus/devices:
    get:
      tags:
      - api-modbus
      summary: List Devices
      description: Return all Modbus devices (no pagination — device counts stay small).
      operationId: list_devices_api_modbus_devices_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModbusDeviceListResponse'
    post:
      tags:
      - api-modbus
      summary: Create Device
      description: 'Create a new Modbus device.


        - Validates that the referenced ``profile`` exists; returns 422 if not.

        - Returns 201 with the created device on success.'
      operationId: create_device_api_modbus_devices_post
      parameters:
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ModbusDeviceCreate'
      responses:
        '201':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModbusDeviceResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/modbus/devices/{uuid}:
    get:
      tags:
      - api-modbus
      summary: Get Device
      description: Return a single Modbus device by UUID.
      operationId: get_device_api_modbus_devices__uuid__get
      parameters:
      - name: uuid
        in: path
        required: true
        schema:
          type: string
          title: Uuid
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModbusDeviceResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
    patch:
      tags:
      - api-modbus
      summary: Patch Device
      description: 'Partially update a Modbus device (including enable/disable).


        Only fields explicitly provided in the request body are updated.

        Providing ``profile`` triggers a profile-existence check (422 if unknown).'
      operationId: patch_device_api_modbus_devices__uuid__patch
      parameters:
      - name: uuid
        in: path
        required: true
        schema:
          type: string
          title: Uuid
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ModbusDeviceUpdate'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModbusDeviceResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
    delete:
      tags:
      - api-modbus
      summary: Delete Device
      description: 'Delete a Modbus device.


        **Default behaviour (cascade=false)**:

        Returns 409 Conflict if the device has any associated readings; use

        ``enabled=false`` to disable it instead.


        **Cascade delete (cascade=true)**:

        Permanently deletes the device together with all its readings and any

        ``ExposedEntityToggle`` rows whose key matches ``modbus.<uuid>.*``.

        Also makes a best-effort attempt to clear the device''s HA Discovery

        config topics from MQTT (empty retained payload) before the DB rows

        are removed.  MQTT failures are swallowed — the DB deletion proceeds

        regardless.

        Returns HTTP 200 with a ``ModbusDeleteResponse`` JSON body on success.


        **Application-layer safety**: the 409 guard uses an explicit SELECT COUNT

        query to return a friendly message.  FK RESTRICT is enforced at runtime

        (the app sets ``PRAGMA foreign_keys=ON``), so the cascade path deletes

        readings before the device.'
      operationId: delete_device_api_modbus_devices__uuid__delete
      parameters:
      - name: uuid
        in: path
        required: true
        schema:
          type: string
          title: Uuid
      - name: cascade
        in: query
        required: false
        schema:
          type: boolean
          default: false
          title: Cascade
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      responses:
        '204':
          description: Successful Response
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/modbus/devices/{uuid}/latest:
    get:
      tags:
      - api-modbus
      summary: Get Latest Reading
      description: 'Return the most recent reading for a device.


        If no readings exist yet, returns ``{"found": false, "recorded_at": null,

        "payload": null}`` (200, not 404) so the front-end can distinguish

        "device exists but has no data" from "device not found".'
      operationId: get_latest_reading_api_modbus_devices__uuid__latest_get
      parameters:
      - name: uuid
        in: path
        required: true
        schema:
          type: string
          title: Uuid
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModbusLatestResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/modbus/devices/{uuid}/readings:
    get:
      tags:
      - api-modbus
      summary: Get Readings
      description: 'Return time-range readings for a device.


        When the window contains more rows than ``limit``, the **most recent** N rows

        are returned (``ORDER BY recorded_at DESC LIMIT n``), then reversed to

        ascending order before being sent to the client.  This ensures that for long

        time-range requests (e.g. 6 h / 24 h) the caller always sees the latest data

        rather than the oldest segment of the window.


        When the window has fewer rows than ``limit`` the full window is returned
        in

        ascending order — behaviour is identical to a plain ascending query.


        The response schema is unchanged: items are always ``recorded_at`` ascending.


        The query uses the ``(device_id, recorded_at)`` composite index for

        efficient time-window scans.


        Query parameters:

        - ``start``: inclusive lower bound (ISO8601 datetime)

        - ``end``: inclusive upper bound (ISO8601 datetime)

        - ``limit``: max rows to return (default 500, max {_READINGS_LIMIT_MAX})'
      operationId: get_readings_api_modbus_devices__uuid__readings_get
      parameters:
      - name: uuid
        in: path
        required: true
        schema:
          type: string
          title: Uuid
      - name: start
        in: query
        required: false
        schema:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Start
      - name: end
        in: query
        required: false
        schema:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: End
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          maximum: 5000
          minimum: 1
          default: 500
          title: Limit
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModbusReadingsResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/modbus/devices/{uuid}/metrics:
    get:
      tags:
      - api-modbus
      summary: Get Metrics
      description: 'Return the metric catalogue for a device''s profile.


        Each entry has ``key``, ``label`` (derived from key if the profile has

        none — underscores → spaces, title-cased), ``unit``, and ``device_class``.

        This is the authoritative metadata source for front-end card labels and

        chart axis labels.'
      operationId: get_metrics_api_modbus_devices__uuid__metrics_get
      parameters:
      - name: uuid
        in: path
        required: true
        schema:
          type: string
          title: Uuid
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModbusMetricsResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/modbus/devices/{uuid}/test:
    post:
      tags:
      - api-modbus
      summary: Test Read
      description: 'Immediately read and decode the device once without persisting
        any data.


        Useful for validating gateway connectivity and Modbus address settings

        before relying on the background polling job.


        This is a write-class endpoint (it initiates network I/O on demand) and

        therefore requires a CSRF token.  The response payload is never stored.'
      operationId: test_read_api_modbus_devices__uuid__test_post
      parameters:
      - name: uuid
        in: path
        required: true
        schema:
          type: string
          title: Uuid
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModbusTestReadResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/session:
    get:
      tags:
      - api-session
      summary: Get Session
      description: Return the current session user and CSRF token. Returns 401 if
        not authenticated.
      operationId: get_session_api_session_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionResponse'
  /api/auth/login:
    post:
      tags:
      - api-session
      summary: Post Login
      description: 'Authenticate with username and password.


        On success, sets an HttpOnly session cookie and returns the session user +
        CSRF token.

        On failure, returns 401 with no cookie set.

        Repeated failures trigger exponential back-off (429 + Retry-After).

        No X-CSRF-Token required (unauthenticated endpoint).'
      operationId: post_login_api_auth_login_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
        required: true
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/auth/logout:
    post:
      tags:
      - api-session
      summary: Post Logout
      description: 'Revoke the current session and clear the session cookie.

        Requires authentication and X-CSRF-Token header.

        Returns 204 No Content.'
      operationId: post_logout_api_auth_logout_post
      parameters:
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/auth/password:
    post:
      tags:
      - api-session
      summary: Post Change Password
      description: 'Change the current user''s password.

        Requires authentication and X-CSRF-Token header.

        On AuthPasswordChangeError returns 400 with a generic message.

        On success, force_password_change becomes False (handled by the service).

        Returns 204 No Content.'
      operationId: post_change_password_api_auth_password_post
      parameters:
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasswordChangeRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/auth/totp/setup:
    post:
      tags:
      - api-session
      summary: Post Totp Setup
      description: 'Generate a new pending TOTP secret, otpauth URI, and one-time
        recovery codes.


        The secret is stored in the DB but TOTP is NOT yet enabled (totp_enabled stays

        False until the user confirms with POST /api/auth/totp/enable).


        Recovery codes are returned here as plaintext exactly once; their Argon2 hashes

        are persisted immediately so enable only needs to flip the enabled flag.


        Repeating this call replaces any prior pending secret and regenerates codes.


        Requires: session cookie + X-CSRF-Token.'
      operationId: post_totp_setup_api_auth_totp_setup_post
      parameters:
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TotpSetupResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/auth/totp/enable:
    post:
      tags:
      - api-session
      summary: Post Totp Enable
      description: 'Enable TOTP by confirming with the current 6-digit code from the
        authenticator app.


        Requires a prior call to POST /api/auth/totp/setup (so that a pending secret

        exists).  On success, totp_enabled becomes True.


        Returns 400 if the code is wrong or there is no pending secret.

        Requires: session cookie + X-CSRF-Token.'
      operationId: post_totp_enable_api_auth_totp_enable_post
      parameters:
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TotpEnableRequest'
      responses:
        '204':
          description: Successful Response
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/auth/totp/disable:
    post:
      tags:
      - api-session
      summary: Post Totp Disable
      description: 'Disable TOTP.  The caller must provide exactly one of:

        - ``password``: the user''s current login password, OR

        - ``code``: the current 6-digit TOTP code.


        On success: totp_enabled=False, totp_secret cleared, all recovery codes deleted.

        Returns 400 if neither credential matches or neither is provided.

        Requires: session cookie + X-CSRF-Token.'
      operationId: post_totp_disable_api_auth_totp_disable_post
      parameters:
      - name: X-CSRF-Token
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Csrf-Token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TotpDisableRequest'
      responses:
        '204':
          description: Successful Response
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/auth/totp:
    get:
      tags:
      - api-session
      summary: Get Totp Status
      description: 'Return the current TOTP status for the authenticated user.


        Response contains only ``{"enabled": bool}``.

        Secret and recovery codes are NEVER returned here.

        Requires: session cookie only (no CSRF — read-only).'
      operationId: get_totp_status_api_auth_totp_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TotpStatusResponse'
  /homeassistant/publish:
    post:
      tags:
      - homeassistant
      summary: Publish From Homeassistant
      operationId: publish_from_homeassistant_homeassistant_publish_post
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
  /location/record:
    post:
      tags:
      - location
      summary: Create Location Record
      operationId: create_location_record_location_record_post
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
  /poo/record:
    post:
      tags:
      - poo
      summary: Create Poo Record
      operationId: create_poo_record_poo_record_post
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
  /poo/latest:
    get:
      tags:
      - poo
      summary: Notify Latest Poo
      operationId: notify_latest_poo_poo_latest_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
  /public-ip/check:
    get:
      tags:
      - public-ip
      summary: Run Public Ip Check
      operationId: run_public_ip_check_public_ip_check_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicIPCheckResponse'
  /ticktick/auth/start:
    get:
      tags:
      - ticktick
      summary: Start Ticktick Auth
      operationId: start_ticktick_auth_ticktick_auth_start_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
  /ticktick/auth/code:
    get:
      tags:
      - ticktick
      summary: Handle Ticktick Auth Code
      operationId: handle_ticktick_auth_code_ticktick_auth_code_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
components:
  schemas:
    CatalogEntrySchema:
      properties:
        entity:
          $ref: '#/components/schemas/ExposableEntitySchema'
        enabled:
          type: boolean
          title: Enabled
      type: object
      required:
      - entity
      - enabled
      title: CatalogEntrySchema
      description: An entity from the catalog with its current toggle state.
    ConfigField:
      properties:
        env_name:
          type: string
          title: Env Name
        label:
          type: string
          title: Label
        value:
          type: string
          title: Value
        secret:
          type: boolean
          title: Secret
        input_type:
          type: string
          title: Input Type
        configured:
          type: boolean
          title: Configured
      type: object
      required:
      - env_name
      - label
      - value
      - secret
      - input_type
      - configured
      title: ConfigField
    ConfigResponse:
      properties:
        sections:
          items:
            $ref: '#/components/schemas/ConfigSection'
          type: array
          title: Sections
      type: object
      required:
      - sections
      title: ConfigResponse
    ConfigSection:
      properties:
        name:
          type: string
          title: Name
        fields:
          items:
            $ref: '#/components/schemas/ConfigField'
          type: array
          title: Fields
      type: object
      required:
      - name
      - fields
      title: ConfigSection
    ConfigUpdateRequest:
      properties:
        updates:
          additionalProperties:
            type: string
          type: object
          title: Updates
      type: object
      required:
      - updates
      title: ConfigUpdateRequest
      description: Flat mapping of env_name → value, mirroring the existing form semantics.
    ConfigUpdateResponse:
      properties:
        sections:
          items:
            $ref: '#/components/schemas/ConfigSection'
          type: array
          title: Sections
      type: object
      required:
      - sections
      title: ConfigUpdateResponse
    ContractCreate:
      properties:
        name:
          type: string
          maxLength: 255
          minLength: 1
          title: Name
        kind:
          type: string
          maxLength: 32
          minLength: 1
          title: Kind
        currency:
          type: string
          maxLength: 8
          minLength: 1
          title: Currency
          default: EUR
        values:
          additionalProperties: true
          type: object
          title: Values
        effective_from:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Effective From
          description: UTC datetime from which the first pricing version is effective.
            Defaults to the current UTC time when omitted.
      type: object
      required:
      - name
      - kind
      - values
      title: ContractCreate
      description: 'Request body for POST /api/energy/contracts.


        ``effective_from`` defaults to the current UTC time if not provided,

        giving the first version an open-ended start from "now".

        ``kind`` is validated at the application layer against the profile registry;

        clients should send ``"manual"`` or ``"tibber"``.'
    ContractDetailResponse:
      properties:
        id:
          type: integer
          title: Id
        name:
          type: string
          title: Name
        kind:
          type: string
          title: Kind
        active:
          type: boolean
          title: Active
        currency:
          type: string
          title: Currency
        created_at:
          type: string
          format: date-time
          title: Created At
        updated_at:
          type: string
          format: date-time
          title: Updated At
        versions:
          items:
            $ref: '#/components/schemas/ContractVersionResponse'
          type: array
          title: Versions
      type: object
      required:
      - id
      - name
      - kind
      - active
      - currency
      - created_at
      - updated_at
      - versions
      title: ContractDetailResponse
      description: 'Response schema for a single EnergyContract with full version
        history.


        Returned by GET /api/energy/contracts/{id} and by successful POST / PATCH

        operations where the caller needs to see all version data.'
    ContractListResponse:
      properties:
        items:
          items:
            $ref: '#/components/schemas/ContractResponse'
          type: array
          title: Items
        total:
          type: integer
          title: Total
      type: object
      required:
      - items
      - total
      title: ContractListResponse
      description: Response schema for GET /api/energy/contracts.
    ContractPatch:
      properties:
        name:
          anyOf:
          - type: string
            maxLength: 255
            minLength: 1
          - type: 'null'
          title: Name
        active:
          anyOf:
          - type: boolean
          - type: 'null'
          title: Active
      type: object
      title: ContractPatch
      description: 'Request body for PATCH /api/energy/contracts/{id}.


        All fields are optional.  Sending ``active=true`` activates this contract

        (deactivating all others); ``active=false`` deactivates it without affecting

        other contracts.'
    ContractResponse:
      properties:
        id:
          type: integer
          title: Id
        name:
          type: string
          title: Name
        kind:
          type: string
          title: Kind
        active:
          type: boolean
          title: Active
        currency:
          type: string
          title: Currency
        created_at:
          type: string
          format: date-time
          title: Created At
        updated_at:
          type: string
          format: date-time
          title: Updated At
      type: object
      required:
      - id
      - name
      - kind
      - active
      - currency
      - created_at
      - updated_at
      title: ContractResponse
      description: 'Response schema for a single EnergyContract (without embedded
        versions).


        Used for list responses where embedding all versions would be expensive.'
    ContractVersionResponse:
      properties:
        id:
          type: integer
          title: Id
        effective_from:
          type: string
          format: date-time
          title: Effective From
        effective_to:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Effective To
        values:
          additionalProperties: true
          type: object
          title: Values
        created_at:
          type: string
          format: date-time
          title: Created At
      type: object
      required:
      - id
      - effective_from
      - effective_to
      - values
      - created_at
      title: ContractVersionResponse
      description: Response schema for a single EnergyContractVersion row.
    CostPeriodSchema:
      properties:
        period_start:
          type: string
          format: date-time
          title: Period Start
        d1_kwh:
          type: number
          title: D1 Kwh
          description: Delivered low-tariff kWh for this period.
        d2_kwh:
          type: number
          title: D2 Kwh
          description: Delivered normal-tariff kWh for this period.
        r1_kwh:
          type: number
          title: R1 Kwh
          description: Returned low-tariff kWh for this period.
        r2_kwh:
          type: number
          title: R2 Kwh
          description: Returned normal-tariff kWh for this period.
        import_cost:
          type: number
          title: Import Cost
          description: Cost of electricity drawn from grid (EUR).
        export_revenue:
          type: number
          title: Export Revenue
          description: Revenue from electricity fed to grid (EUR).
        net_cost:
          type: number
          title: Net Cost
          description: import_cost − export_revenue (EUR).
        currency:
          type: string
          title: Currency
          description: ISO 4217 currency code.
        degraded:
          type: boolean
          title: Degraded
          description: True when the period was computed with incomplete data.
        contract_version_id:
          anyOf:
          - type: integer
          - type: 'null'
          title: Contract Version Id
          description: FK to the contract version used for this billing period (null
            when degraded).
      type: object
      required:
      - period_start
      - d1_kwh
      - d2_kwh
      - r1_kwh
      - r2_kwh
      - import_cost
      - export_revenue
      - net_cost
      - currency
      - degraded
      title: CostPeriodSchema
      description: One 15-minute billing record from the energy_cost_period table.
    CostsResponse:
      properties:
        items:
          items:
            $ref: '#/components/schemas/CostPeriodSchema'
          type: array
          title: Items
        total:
          type: integer
          title: Total
          description: Number of items returned.
      type: object
      required:
      - items
      - total
      title: CostsResponse
      description: Response for GET /api/energy/costs.
    DeviceInfoSchema:
      properties:
        identifiers:
          items:
            type: string
          type: array
          title: Identifiers
        name:
          type: string
          title: Name
      type: object
      required:
      - identifiers
      - name
      title: DeviceInfoSchema
      description: HA device grouping info for an exposable entity.
    DsmrLatestResponse:
      properties:
        found:
          type: boolean
          title: Found
        recorded_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Recorded At
        payload:
          anyOf:
          - additionalProperties: true
            type: object
          - type: 'null'
          title: Payload
      type: object
      required:
      - found
      title: DsmrLatestResponse
      description: 'Response for GET /api/energy/dsmr/latest.


        ``found`` is False when no dsmr_reading rows exist yet.  The front-end

        should check ``found`` before reading ``recorded_at`` or ``payload``.'
    ExposableEntitySchema:
      properties:
        key:
          type: string
          title: Key
        component:
          type: string
          title: Component
        device:
          $ref: '#/components/schemas/DeviceInfoSchema'
        device_class:
          anyOf:
          - type: string
          - type: 'null'
          title: Device Class
        unit:
          type: string
          title: Unit
        name:
          type: string
          title: Name
        state_class:
          anyOf:
          - type: string
          - type: 'null'
          title: State Class
      type: object
      required:
      - key
      - component
      - device
      - device_class
      - unit
      - name
      title: ExposableEntitySchema
      description: 'One exposable entity in the catalog.


        ``value_getter`` is intentionally excluded — it is a non-serialisable

        callable and is only used internally by the HA Discovery service.'
    ExposeResponse:
      properties:
        catalog:
          items:
            $ref: '#/components/schemas/CatalogEntrySchema'
          type: array
          title: Catalog
        mqtt_status:
          $ref: '#/components/schemas/MqttStatusSchema'
      type: object
      required:
      - catalog
      - mqtt_status
      title: ExposeResponse
      description: Response for GET /api/expose.
    ExposeUpdateRequest:
      properties:
        toggles:
          additionalProperties:
            type: boolean
          type: object
          title: Toggles
      type: object
      required:
      - toggles
      title: ExposeUpdateRequest
      description: 'Request body for PUT /api/expose.


        ``toggles`` is a map from entity key to desired enabled state (bool).

        Only keys present in the map are updated; absent keys are untouched.'
    ExposeUpdateResponse:
      properties:
        catalog:
          items:
            $ref: '#/components/schemas/CatalogEntrySchema'
          type: array
          title: Catalog
        mqtt_status:
          $ref: '#/components/schemas/MqttStatusSchema'
      type: object
      required:
      - catalog
      - mqtt_status
      title: ExposeUpdateResponse
      description: Response for PUT /api/expose (returns updated catalog + status).
    HTTPValidationError:
      properties:
        detail:
          items:
            $ref: '#/components/schemas/ValidationError'
          type: array
          title: Detail
      type: object
      title: HTTPValidationError
    LocationRecord:
      properties:
        person:
          type: string
          title: Person
        datetime:
          type: string
          title: Datetime
        latitude:
          type: number
          title: Latitude
        longitude:
          type: number
          title: Longitude
        altitude:
          anyOf:
          - type: number
          - type: 'null'
          title: Altitude
      type: object
      required:
      - person
      - datetime
      - latitude
      - longitude
      - altitude
      title: LocationRecord
    LocationUpdateRequest:
      properties:
        latitude:
          anyOf:
          - type: number
          - type: 'null'
          title: Latitude
        longitude:
          anyOf:
          - type: number
          - type: 'null'
          title: Longitude
        altitude:
          anyOf:
          - type: number
          - type: 'null'
          title: Altitude
      type: object
      title: LocationUpdateRequest
      description: PATCH body for a location record — all fields optional; PK fields
        excluded.
    LocationsResponse:
      properties:
        items:
          items:
            $ref: '#/components/schemas/LocationRecord'
          type: array
          title: Items
        limit:
          type: integer
          title: Limit
        offset:
          type: integer
          title: Offset
      type: object
      required:
      - items
      - limit
      - offset
      title: LocationsResponse
    LoginRequest:
      properties:
        username:
          type: string
          title: Username
        password:
          type: string
          title: Password
        totp_code:
          anyOf:
          - type: string
          - type: 'null'
          title: Totp Code
      type: object
      required:
      - username
      - password
      title: LoginRequest
    ManualTariffSchema:
      properties:
        buy_dal:
          type: number
          title: Buy Dal
          description: Effective buy price, low-tariff / dal (EUR/kWh).
        buy_normal:
          type: number
          title: Buy Normal
          description: Effective buy price, normal / high-tariff (EUR/kWh).
        sell_dal:
          type: number
          title: Sell Dal
          description: Sell price, low-tariff / dal (EUR/kWh).
        sell_normal:
          type: number
          title: Sell Normal
          description: Sell price, normal / high-tariff (EUR/kWh).
      type: object
      required:
      - buy_dal
      - buy_normal
      - sell_dal
      - sell_normal
      title: ManualTariffSchema
      description: 'Fixed-tariff breakdown for manual contracts.


        Prices are the *effective* buy prices as used by the billing engine

        (energy_buy_x + energy_tax + ode) and the raw sell prices.'
    MeterDeclareRequest:
      properties:
        label:
          type: string
          maxLength: 255
          minLength: 1
          title: Label
        started_at:
          type: string
          format: date-time
          title: Started At
          description: UTC (or server-local naive) datetime from which this meter
            epoch starts.  May be in the past (retroactive declaration).
        reason:
          $ref: '#/components/schemas/MeterReason'
          description: 'Why this epoch was created.  One of: initial, meter_swap,
            home_move, other.'
        note:
          anyOf:
          - type: string
            maxLength: 1024
          - type: 'null'
          title: Note
        commodity:
          type: string
          maxLength: 32
          minLength: 1
          title: Commodity
          description: Energy commodity this meter measures.  Defaults to 'electricity'.
          default: electricity
      type: object
      required:
      - label
      - started_at
      - reason
      title: MeterDeclareRequest
      description: 'Request body for POST /api/energy/meters.


        Declares a new meter epoch (swap, home move, or initial declaration).  The

        service layer closes the current active meter for the given commodity at

        ``started_at`` and opens a new one.


        ``started_at`` follows the Principle-A localisation convention: a

        timezone-naive value is interpreted as the **server''s local wall-clock time**

        (e.g. CEST midnight → stored as UTC the night before); a timezone-aware

        value is converted to UTC as-is.  Omitting ``started_at`` is not allowed —

        every meter declaration must carry an explicit start timestamp.


        ``commodity`` defaults to ``"electricity"``; the field is available for

        future use with ``gas`` or ``heating``.'
    MeterListResponse:
      properties:
        items:
          items:
            $ref: '#/components/schemas/MeterResponse'
          type: array
          title: Items
        total:
          type: integer
          title: Total
      type: object
      required:
      - items
      - total
      title: MeterListResponse
      description: 'Response schema for GET /api/energy/meters.


        Meters are returned in ascending ``started_at`` order so the caller sees

        the historical installation sequence.'
    MeterPatchRequest:
      properties:
        label:
          anyOf:
          - type: string
            maxLength: 255
            minLength: 1
          - type: 'null'
          title: Label
        note:
          anyOf:
          - type: string
            maxLength: 1024
          - type: 'null'
          title: Note
        started_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Started At
          description: Retroactive correction of the meter epoch start timestamp.  Triggers
            billing recompute over the affected window.
      type: object
      title: MeterPatchRequest
      description: 'Request body for PATCH /api/energy/meters/{id}.


        All fields are optional.  Only non-``None`` values are applied.


        Updating ``started_at`` is a **retroactive correction**: the service layer

        maintains timeline continuity (adjusting the preceding meter''s ``ended_at``)

        and the API layer triggers ``recompute_range`` over the affected window so

        that billing attribution is re-judged.'
    MeterReason:
      type: string
      enum:
      - initial
      - meter_swap
      - home_move
      - other
      title: MeterReason
      description: Allowed values for the meter epoch creation reason.
    MeterResponse:
      properties:
        id:
          type: integer
          title: Id
        label:
          type: string
          title: Label
        commodity:
          type: string
          title: Commodity
        started_at:
          type: string
          format: date-time
          title: Started At
        ended_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Ended At
        reason:
          type: string
          title: Reason
        note:
          anyOf:
          - type: string
          - type: 'null'
          title: Note
        created_at:
          type: string
          format: date-time
          title: Created At
      type: object
      required:
      - id
      - label
      - commodity
      - started_at
      - ended_at
      - reason
      - note
      - created_at
      title: MeterResponse
      description: 'Response schema for a single Meter epoch row.


        ``ended_at`` is ``null`` for the currently active meter.'
    MetricInfo:
      properties:
        key:
          type: string
          title: Key
        label:
          type: string
          title: Label
        unit:
          type: string
          title: Unit
        device_class:
          type: string
          title: Device Class
      type: object
      required:
      - key
      - label
      - unit
      - device_class
      title: MetricInfo
      description: Metadata for a single measurable quantity in a device's profile.
    ModbusDeviceCreate:
      properties:
        friendly_name:
          type: string
          maxLength: 255
          minLength: 1
          title: Friendly Name
        transport:
          type: string
          maxLength: 16
          title: Transport
          default: tcp
        host:
          type: string
          maxLength: 255
          minLength: 1
          title: Host
        port:
          type: integer
          maximum: 65535.0
          minimum: 1.0
          title: Port
          default: 502
        unit_id:
          type: integer
          maximum: 247.0
          minimum: 0.0
          title: Unit Id
          default: 1
        profile:
          type: string
          maxLength: 64
          minLength: 1
          title: Profile
        poll_interval_s:
          type: integer
          maximum: 3600.0
          minimum: 1.0
          title: Poll Interval S
          default: 5
        enabled:
          type: boolean
          title: Enabled
          default: true
      type: object
      required:
      - friendly_name
      - host
      - profile
      title: ModbusDeviceCreate
      description: Request body for POST /api/modbus/devices.
    ModbusDeviceListResponse:
      properties:
        items:
          items:
            $ref: '#/components/schemas/ModbusDeviceResponse'
          type: array
          title: Items
        total:
          type: integer
          title: Total
      type: object
      required:
      - items
      - total
      title: ModbusDeviceListResponse
      description: Response schema for listing Modbus devices.
    ModbusDeviceResponse:
      properties:
        uuid:
          type: string
          title: Uuid
        friendly_name:
          type: string
          title: Friendly Name
        transport:
          type: string
          title: Transport
        host:
          type: string
          title: Host
        port:
          type: integer
          title: Port
        unit_id:
          type: integer
          title: Unit Id
        profile:
          type: string
          title: Profile
        poll_interval_s:
          type: integer
          title: Poll Interval S
        enabled:
          type: boolean
          title: Enabled
        last_poll_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Last Poll At
        last_poll_ok:
          anyOf:
          - type: boolean
          - type: 'null'
          title: Last Poll Ok
        created_at:
          type: string
          format: date-time
          title: Created At
        updated_at:
          type: string
          format: date-time
          title: Updated At
      type: object
      required:
      - uuid
      - friendly_name
      - transport
      - host
      - port
      - unit_id
      - profile
      - poll_interval_s
      - enabled
      - last_poll_at
      - last_poll_ok
      - created_at
      - updated_at
      title: ModbusDeviceResponse
      description: Response schema for a single Modbus device.
    ModbusDeviceUpdate:
      properties:
        friendly_name:
          anyOf:
          - type: string
            maxLength: 255
            minLength: 1
          - type: 'null'
          title: Friendly Name
        transport:
          anyOf:
          - type: string
            maxLength: 16
          - type: 'null'
          title: Transport
        host:
          anyOf:
          - type: string
            maxLength: 255
            minLength: 1
          - type: 'null'
          title: Host
        port:
          anyOf:
          - type: integer
            maximum: 65535.0
            minimum: 1.0
          - type: 'null'
          title: Port
        unit_id:
          anyOf:
          - type: integer
            maximum: 247.0
            minimum: 0.0
          - type: 'null'
          title: Unit Id
        profile:
          anyOf:
          - type: string
            maxLength: 64
            minLength: 1
          - type: 'null'
          title: Profile
        poll_interval_s:
          anyOf:
          - type: integer
            maximum: 3600.0
            minimum: 1.0
          - type: 'null'
          title: Poll Interval S
        enabled:
          anyOf:
          - type: boolean
          - type: 'null'
          title: Enabled
      type: object
      title: ModbusDeviceUpdate
      description: Request body for PATCH /api/modbus/devices/{uuid} — all fields
        optional.
    ModbusLatestResponse:
      properties:
        found:
          type: boolean
          title: Found
        recorded_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Recorded At
        payload:
          anyOf:
          - additionalProperties: true
            type: object
          - type: 'null'
          title: Payload
      type: object
      required:
      - found
      - recorded_at
      - payload
      title: ModbusLatestResponse
      description: 'Response for the /latest endpoint.


        ``found`` is False and ``recorded_at``/``payload`` are None when the device

        has no readings yet.  Callers should check ``found`` before using the values.'
    ModbusMetricsResponse:
      properties:
        profile:
          type: string
          title: Profile
        metrics:
          items:
            $ref: '#/components/schemas/MetricInfo'
          type: array
          title: Metrics
      type: object
      required:
      - profile
      - metrics
      title: ModbusMetricsResponse
      description: Response schema for GET /api/modbus/devices/{uuid}/metrics.
    ModbusProfilesResponse:
      properties:
        profiles:
          items:
            $ref: '#/components/schemas/ProfileSummary'
          type: array
          title: Profiles
      type: object
      required:
      - profiles
      title: ModbusProfilesResponse
      description: Response schema for GET /api/modbus/profiles.
    ModbusReadingResponse:
      properties:
        recorded_at:
          type: string
          format: date-time
          title: Recorded At
        payload:
          additionalProperties: true
          type: object
          title: Payload
      type: object
      required:
      - recorded_at
      - payload
      title: ModbusReadingResponse
      description: 'A single reading row: timestamp + decoded payload.'
    ModbusReadingsResponse:
      properties:
        items:
          items:
            $ref: '#/components/schemas/ModbusReadingResponse'
          type: array
          title: Items
      type: object
      required:
      - items
      title: ModbusReadingsResponse
      description: Response schema for the readings time-range endpoint.
    ModbusTestReadResponse:
      properties:
        ok:
          type: boolean
          title: Ok
        payload:
          anyOf:
          - additionalProperties: true
            type: object
          - type: 'null'
          title: Payload
        error:
          anyOf:
          - type: string
          - type: 'null'
          title: Error
      type: object
      required:
      - ok
      title: ModbusTestReadResponse
      description: 'Response for POST /api/modbus/devices/{uuid}/test.


        On success ``ok=True`` and ``payload`` contains the decoded values.

        On failure ``ok=False`` and ``error`` describes the problem.'
    MqttStatusSchema:
      properties:
        mqtt_configured:
          type: boolean
          title: Mqtt Configured
        mqtt_connected:
          type: boolean
          title: Mqtt Connected
        discovery_enabled:
          type: boolean
          title: Discovery Enabled
      type: object
      required:
      - mqtt_configured
      - mqtt_connected
      - discovery_enabled
      title: MqttStatusSchema
      description: Connection status for MQTT and HA Discovery.
    MqttTestResponse:
      properties:
        result:
          type: string
          enum:
          - success
          - config-error
          - failed
          title: Result
        message:
          type: string
          title: Message
      type: object
      required:
      - result
      - message
      title: MqttTestResponse
      description: Response from POST /api/config/mqtt/test.
    PasswordChangeRequest:
      properties:
        current_password:
          type: string
          title: Current Password
        new_password:
          type: string
          title: New Password
        confirm_password:
          type: string
          title: Confirm Password
      type: object
      required:
      - current_password
      - new_password
      - confirm_password
      title: PasswordChangeRequest
    PooRecord:
      properties:
        timestamp:
          type: string
          title: Timestamp
        status:
          type: string
          title: Status
        latitude:
          type: number
          title: Latitude
        longitude:
          type: number
          title: Longitude
      type: object
      required:
      - timestamp
      - status
      - latitude
      - longitude
      title: PooRecord
    PooResponse:
      properties:
        items:
          items:
            $ref: '#/components/schemas/PooRecord'
          type: array
          title: Items
        limit:
          type: integer
          title: Limit
        offset:
          type: integer
          title: Offset
      type: object
      required:
      - items
      - limit
      - offset
      title: PooResponse
    PooUpdateRequest:
      properties:
        status:
          anyOf:
          - type: string
          - type: 'null'
          title: Status
        latitude:
          anyOf:
          - type: number
          - type: 'null'
          title: Latitude
        longitude:
          anyOf:
          - type: number
          - type: 'null'
          title: Longitude
      type: object
      title: PooUpdateRequest
      description: PATCH body for a poo record — all fields optional; PK field excluded.
    PricePointSchema:
      properties:
        starts_at:
          type: string
          format: date-time
          title: Starts At
        buy:
          type: number
          title: Buy
          description: All-in buy price in EUR/kWh (including taxes).
        sell:
          type: number
          title: Sell
          description: Net sell price in EUR/kWh.
        level:
          anyOf:
          - type: string
          - type: 'null'
          title: Level
          description: Tibber price level (CHEAP / NORMAL / EXPENSIVE); null for manual.
      type: object
      required:
      - starts_at
      - buy
      - sell
      title: PricePointSchema
      description: A single 15-minute price point (tibber) or placeholder entry.
    PricesResponse:
      properties:
        kind:
          anyOf:
          - type: string
          - type: 'null'
          title: Kind
          description: Active contract kind ('tibber' or 'manual'), or null if no
            active contract.
        currency:
          type: string
          title: Currency
          description: ISO 4217 currency code.
        points:
          items:
            $ref: '#/components/schemas/PricePointSchema'
          type: array
          title: Points
          description: 15-minute price points for tibber contracts (ascending by starts_at).
            Empty for manual contracts or when no active contract exists.
        tariff:
          anyOf:
          - $ref: '#/components/schemas/ManualTariffSchema'
          - type: 'null'
          description: Fixed tariff table for manual contracts. Null for tibber contracts
            and when no active contract exists.
      type: object
      required:
      - kind
      - currency
      - points
      title: PricesResponse
      description: 'Response for GET /api/energy/prices.


        ``kind`` mirrors the active contract kind:

        - ``"tibber"``  → ``points`` has actual 15-min price entries; ``tariff`` is
        null.

        - ``"manual"``  → ``points`` is empty; ``tariff`` carries the fixed-rate table.

        - ``None``      → no active contract; both ``points`` and ``tariff`` are empty/null.


        ``currency`` comes from the active contract (or "EUR" fallback).

        ``points`` is always ascending by ``starts_at``.'
    ProfileSummary:
      properties:
        name:
          type: string
          title: Name
        description:
          type: string
          title: Description
      type: object
      required:
      - name
      - description
      title: ProfileSummary
      description: One entry in the GET /api/modbus/profiles response.
    ProfilesResponse:
      properties:
        profiles:
          items:
            additionalProperties: true
            type: object
          type: array
          title: Profiles
      type: object
      required:
      - profiles
      title: ProfilesResponse
      description: 'Response schema for GET /api/energy/profiles.


        ``profiles`` is a list of raw profile dicts as produced by

        ``list_profiles()`` (Pydantic model dumps).  Each entry contains at minimum

        ``kind`` and ``label``; the full nested structure allows the front-end to

        render a type-appropriate form for each pricing profile.'
    PublicIPCheckResponse:
      properties:
        status:
          type: string
          enum:
          - first_seen
          - unchanged
          - changed
          - error
          title: Status
        checked_at:
          type: string
          format: date-time
          title: Checked At
        changed:
          type: boolean
          title: Changed
      type: object
      required:
      - status
      - checked_at
      - changed
      title: PublicIPCheckResponse
    PublicIPHistorySchema:
      properties:
        id:
          type: integer
          title: Id
        ipv4:
          type: string
          title: Ipv4
        observed_at:
          type: string
          format: date-time
          title: Observed At
        change_type:
          type: string
          title: Change Type
        provider:
          anyOf:
          - type: string
          - type: 'null'
          title: Provider
      type: object
      required:
      - id
      - ipv4
      - observed_at
      - change_type
      - provider
      title: PublicIPHistorySchema
    PublicIPResponse:
      properties:
        state:
          anyOf:
          - $ref: '#/components/schemas/PublicIPStateSchema'
          - type: 'null'
        history:
          items:
            $ref: '#/components/schemas/PublicIPHistorySchema'
          type: array
          title: History
      type: object
      required:
      - state
      - history
      title: PublicIPResponse
    PublicIPStateSchema:
      properties:
        id:
          type: integer
          title: Id
        current_ipv4:
          type: string
          title: Current Ipv4
        previous_ipv4:
          anyOf:
          - type: string
          - type: 'null'
          title: Previous Ipv4
        first_seen_at:
          type: string
          format: date-time
          title: First Seen At
        last_checked_at:
          type: string
          format: date-time
          title: Last Checked At
        last_changed_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Last Changed At
        last_check_status:
          type: string
          title: Last Check Status
        last_check_error:
          anyOf:
          - type: string
          - type: 'null'
          title: Last Check Error
        last_provider:
          anyOf:
          - type: string
          - type: 'null'
          title: Last Provider
      type: object
      required:
      - id
      - current_ipv4
      - previous_ipv4
      - first_seen_at
      - last_checked_at
      - last_changed_at
      - last_check_status
      - last_check_error
      - last_provider
      title: PublicIPStateSchema
    RecomputeResponse:
      properties:
        recomputed:
          type: integer
          title: Recomputed
          description: Number of 15-minute periods for which a billing record was
            written (inserted or updated).  Periods skipped due to missing contract
            or missing Tibber price are not counted.
      type: object
      required:
      - recomputed
      title: RecomputeResponse
      description: Response for POST /api/energy/costs/recompute.
    RepublishResponse:
      properties:
        ok:
          type: boolean
          title: Ok
        message:
          type: string
          title: Message
      type: object
      required:
      - ok
      - message
      title: RepublishResponse
      description: Response for POST /api/expose/republish.
    SessionResponse:
      properties:
        user:
          $ref: '#/components/schemas/SessionUser'
        csrf_token:
          type: string
          title: Csrf Token
      type: object
      required:
      - user
      - csrf_token
      title: SessionResponse
    SessionUser:
      properties:
        username:
          type: string
          title: Username
        force_password_change:
          type: boolean
          title: Force Password Change
      type: object
      required:
      - username
      - force_password_change
      title: SessionUser
    SmtpTestResponse:
      properties:
        result:
          type: string
          enum:
          - success
          - config-error
          - failed
          title: Result
        message:
          type: string
          title: Message
      type: object
      required:
      - result
      - message
      title: SmtpTestResponse
      description: Response from POST /api/config/smtp/test.
    StatusResponse:
      properties:
        status:
          type: string
          title: Status
      type: object
      required:
      - status
      title: StatusResponse
    SummaryResponse:
      properties:
        currency:
          type: string
          title: Currency
        metered_import:
          type: number
          title: Metered Import
          description: Σ import_cost for non-degraded periods.
        metered_export:
          type: number
          title: Metered Export
          description: Σ export_revenue for non-degraded periods.
        metered_net:
          type: number
          title: Metered Net
          description: Σ net_cost for non-degraded periods.
        fixed_costs:
          type: number
          title: Fixed Costs
          description: Standing charges (network_fee + management_fee) apportioned
            over the interval.
        credits:
          type: number
          title: Credits
          description: Energy-tax credit (heffingskorting) apportioned over the interval.
        total_payable:
          type: number
          title: Total Payable
          description: metered_net + fixed_costs − credits (actual amount owed).
        period_count:
          type: integer
          title: Period Count
          description: Number of non-degraded billing periods in range.
        degraded_count:
          type: integer
          title: Degraded Count
          description: Number of degraded billing periods in range.
        days:
          type: number
          title: Days
          description: Interval length in days.
      type: object
      required:
      - currency
      - metered_import
      - metered_export
      - metered_net
      - fixed_costs
      - credits
      - total_payable
      - period_count
      - degraded_count
      - days
      title: SummaryResponse
      description: 'Response for GET /api/energy/costs/summary.


        All monetary values are in ``currency``.


        ``total_payable = metered_net + fixed_costs − credits``'
    TibberTestPriceSchema:
      properties:
        starts_at:
          type: string
          format: date-time
          title: Starts At
        total:
          type: number
          title: Total
        energy:
          type: number
          title: Energy
        tax:
          type: number
          title: Tax
        currency:
          type: string
          title: Currency
        level:
          anyOf:
          - type: string
          - type: 'null'
          title: Level
      type: object
      required:
      - starts_at
      - total
      - energy
      - tax
      - currency
      title: TibberTestPriceSchema
      description: 'Current Tibber price point returned on a successful test.


        Carries enough fields for the front-end to confirm the API is working and

        display the live price.  The API token is **never** included.'
    TibberTestResponse:
      properties:
        result:
          type: string
          enum:
          - success
          - config-error
          - failed
          title: Result
        message:
          type: string
          title: Message
        price:
          anyOf:
          - $ref: '#/components/schemas/TibberTestPriceSchema'
          - type: 'null'
      type: object
      required:
      - result
      - message
      title: TibberTestResponse
      description: 'Three-state response for POST /api/energy/tibber/test.


        Possible ``result`` values:


        - ``"success"``      — Tibber API responded with a valid price.

        - ``"config-error"`` — Token is missing or not configured.

        - ``"failed"``       — API call failed (auth rejected, network error, timeout,
        etc.).


        ``price`` is populated only on ``"success"``; it is null otherwise.

        ``message`` always contains a human-readable explanation.'
    TotpDisableRequest:
      properties:
        password:
          anyOf:
          - type: string
          - type: 'null'
          title: Password
        code:
          anyOf:
          - type: string
          - type: 'null'
          title: Code
      type: object
      title: TotpDisableRequest
      description: 'Disable TOTP by proving identity.


        Exactly one of ``password`` or ``code`` must be provided.'
    TotpEnableRequest:
      properties:
        code:
          type: string
          title: Code
      type: object
      required:
      - code
      title: TotpEnableRequest
      description: The user confirms setup by providing the 6-digit TOTP code.
    TotpSetupResponse:
      properties:
        secret:
          type: string
          title: Secret
        otpauth_uri:
          type: string
          title: Otpauth Uri
        recovery_codes:
          items:
            type: string
          type: array
          title: Recovery Codes
      type: object
      required:
      - secret
      - otpauth_uri
      - recovery_codes
      title: TotpSetupResponse
      description: 'Returned once after a setup call.


        ``secret`` and ``recovery_codes`` are **one-time plaintext values**.

        They are never returned again by any subsequent API call.

        The frontend must display and instruct the user to save them before confirming.'
    TotpStatusResponse:
      properties:
        enabled:
          type: boolean
          title: Enabled
      type: object
      required:
      - enabled
      title: TotpStatusResponse
      description: Minimal status response — never exposes secret or recovery codes.
    ValidationError:
      properties:
        loc:
          items:
            anyOf:
            - type: string
            - type: integer
          type: array
          title: Location
        msg:
          type: string
          title: Message
        type:
          type: string
          title: Error Type
      type: object
      required:
      - loc
      - msg
      - type
      title: ValidationError
    VersionCreate:
      properties:
        effective_from:
          type: string
          format: date-time
          title: Effective From
        values:
          additionalProperties: true
          type: object
          title: Values
      type: object
      required:
      - effective_from
      - values
      title: VersionCreate
      description: Request body for POST /api/energy/contracts/{id}/versions.
