"description":"Return all configuration sections. Secret field values are masked (empty string).",
"operationId":"get_config_api_config_get",
"responses":{
"200":{
"description":"Successful Response",
"content":{
"application/json":{
"schema":{
"$ref":"#/components/schemas/ConfigResponse"
}
}
}
}
}
},
"put":{
"tags":[
"api-config"
],
"summary":"Put Config",
"description":"Save configuration updates.\n\n- Blank secret value keeps the existing stored value (no change).\n- Invalid values return 422 and nothing is written to the database.",
"description":"Send a test SMTP email using the current runtime settings.\n\nReturns a structured result indicating success or the category of failure.\nThree possible outcomes:\n- 200 { \"result\": \"success\", \"message\": ... }\n- 400 { \"result\": \"config-error\", \"message\": ... } (EmailConfigurationError)\n- 502 { \"result\": \"failed\", \"message\": ... } (EmailDeliveryError)\n\nSMTP credentials are never echoed in the response.",
"description":"Return location records with optional time-window filtering and pagination.\n\n- ``start`` / ``end`` are ISO8601 strings; filtering is **inclusive** on both bounds.\n- Results are ordered by ``datetime`` ascending.\n- ``limit`` is capped at 5000 to prevent full-table exports.",
"operationId":"get_locations_api_locations_get",
"parameters":[
{
"name":"limit",
"in":"query",
"required":false,
"schema":{
"type":"integer",
"maximum":5000,
"minimum":1,
"default":1000,
"title":"Limit"
}
},
{
"name":"offset",
"in":"query",
"required":false,
"schema":{
"type":"integer",
"minimum":0,
"default":0,
"title":"Offset"
}
},
{
"name":"start",
"in":"query",
"required":false,
"schema":{
"anyOf":[
{
"type":"string"
},
{
"type":"null"
}
],
"title":"Start"
}
},
{
"name":"end",
"in":"query",
"required":false,
"schema":{
"anyOf":[
{
"type":"string"
},
{
"type":"null"
}
],
"title":"End"
}
}
],
"responses":{
"200":{
"description":"Successful Response",
"content":{
"application/json":{
"schema":{
"$ref":"#/components/schemas/LocationsResponse"
}
}
}
},
"422":{
"description":"Validation Error",
"content":{
"application/json":{
"schema":{
"$ref":"#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/poo":{
"get":{
"tags":[
"api-data"
],
"summary":"Get Poo",
"description":"Return poo records ordered by timestamp descending (most recent first).\n\n``limit`` is capped at 1000 to prevent full-table exports.",
"operationId":"get_poo_api_poo_get",
"parameters":[
{
"name":"limit",
"in":"query",
"required":false,
"schema":{
"type":"integer",
"maximum":1000,
"minimum":1,
"default":100,
"title":"Limit"
}
},
{
"name":"offset",
"in":"query",
"required":false,
"schema":{
"type":"integer",
"minimum":0,
"default":0,
"title":"Offset"
}
}
],
"responses":{
"200":{
"description":"Successful Response",
"content":{
"application/json":{
"schema":{
"$ref":"#/components/schemas/PooResponse"
}
}
}
},
"422":{
"description":"Validation Error",
"content":{
"application/json":{
"schema":{
"$ref":"#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/public-ip":{
"get":{
"tags":[
"api-data"
],
"summary":"Get Public Ip",
"description":"Return the current public IP state and recent history.\n\n- ``state`` is ``null`` if no IP check has been performed yet.\n- ``history`` is ordered by ``observed_at`` descending (most recent first).\n- ``limit`` applies to the history list and is capped at 1000.",
"description":"Update the non-PK fields of a single location record.\n\n- ``person`` and ``datetime`` identify the row (composite PK) and are immutable.\n- Only ``latitude``, ``longitude``, and ``altitude`` may be updated.\n- Omitted body fields are left unchanged.\n- Returns **404** if the PK does not exist.",
"description":"Delete the single location record identified by its composite PK.\n\n- Exactly one row is deleted; **404** if the PK does not exist.\n- No batch delete / truncate path is available.",
"description":"Update the non-PK fields of a single poo record.\n\n- ``timestamp`` is the PK and is immutable.\n- Only ``status``, ``latitude``, and ``longitude`` may be updated.\n- Omitted body fields are left unchanged.\n- Returns **404** if the PK does not exist.",
"description":"Delete the single poo record identified by its PK.\n\n- Exactly one row is deleted; **404** if the PK does not exist.\n- No batch delete / truncate path is available.",
"description":"Create a new Modbus device.\n\n- Validates that the referenced ``profile`` exists; returns 422 if not.\n- Returns 201 with the created device on success.",
"description":"Partially update a Modbus device (including enable/disable).\n\nOnly fields explicitly provided in the request body are updated.\nProviding ``profile`` triggers a profile-existence check (422 if unknown).",
"description":"Delete a Modbus device.\n\nReturns 409 Conflict if the device has any associated readings; use\n``enabled=false`` to disable it instead.\n\n**Application-layer safety**: the check is performed via an explicit\nSELECT COUNT query — not by relying on SQLite's FK RESTRICT constraint,\nwhich is not enforced at runtime in this project (no PRAGMA foreign_keys=ON).",
"description":"Return the most recent reading for a device.\n\nIf no readings exist yet, returns ``{\"found\": false, \"recorded_at\": null,\n\"payload\": null}`` (200, not 404) so the front-end can distinguish\n\"device exists but has no data\" from \"device not found\".",
"description":"Return time-range readings for a device.\n\nResults are ordered by ``recorded_at`` ascending and capped by ``limit``\n(max {_READINGS_LIMIT_MAX}) to prevent accidental full-table exports.\n\nThe query uses the ``(device_id, recorded_at)`` composite index for\nefficient time-window scans.\n\nQuery parameters:\n- ``start``: inclusive lower bound (ISO8601 datetime)\n- ``end``: inclusive upper bound (ISO8601 datetime)\n- ``limit``: max rows to return (default 500, max {_READINGS_LIMIT_MAX})",
"description":"Return the metric catalogue for a device's profile.\n\nEach entry has ``key``, ``label`` (derived from key if the profile has\nnone — underscores → spaces, title-cased), ``unit``, and ``device_class``.\nThis is the authoritative metadata source for front-end card labels and\nchart axis labels.",
"description":"Immediately read and decode the device once without persisting any data.\n\nUseful for validating gateway connectivity and Modbus address settings\nbefore relying on the background polling job.\n\nThis is a write-class endpoint (it initiates network I/O on demand) and\ntherefore requires a CSRF token. The response payload is never stored.",
"description":"Authenticate with username and password.\n\nOn success, sets an HttpOnly session cookie and returns the session user + CSRF token.\nOn failure, returns 401 with no cookie set.\nRepeated failures trigger exponential back-off (429 + Retry-After).\nNo X-CSRF-Token required (unauthenticated endpoint).",
"description":"Revoke the current session and clear the session cookie.\nRequires authentication and X-CSRF-Token header.\nReturns 204 No Content.",
"operationId":"post_logout_api_auth_logout_post",
"parameters":[
{
"name":"X-CSRF-Token",
"in":"header",
"required":false,
"schema":{
"anyOf":[
{
"type":"string"
},
{
"type":"null"
}
],
"title":"X-Csrf-Token"
}
}
],
"responses":{
"200":{
"description":"Successful Response",
"content":{
"application/json":{
"schema":{}
}
}
},
"422":{
"description":"Validation Error",
"content":{
"application/json":{
"schema":{
"$ref":"#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/auth/password":{
"post":{
"tags":[
"api-session"
],
"summary":"Post Change Password",
"description":"Change the current user's password.\nRequires authentication and X-CSRF-Token header.\nOn AuthPasswordChangeError returns 400 with a generic message.\nOn success, force_password_change becomes False (handled by the service).\nReturns 204 No Content.",
"description":"Generate a new pending TOTP secret, otpauth URI, and one-time recovery codes.\n\nThe secret is stored in the DB but TOTP is NOT yet enabled (totp_enabled stays\nFalse until the user confirms with POST /api/auth/totp/enable).\n\nRecovery codes are returned here as plaintext exactly once; their Argon2 hashes\nare persisted immediately so enable only needs to flip the enabled flag.\n\nRepeating this call replaces any prior pending secret and regenerates codes.\n\nRequires: session cookie + X-CSRF-Token.",
"description":"Enable TOTP by confirming with the current 6-digit code from the authenticator app.\n\nRequires a prior call to POST /api/auth/totp/setup (so that a pending secret\nexists). On success, totp_enabled becomes True.\n\nReturns 400 if the code is wrong or there is no pending secret.\nRequires: session cookie + X-CSRF-Token.",
"description":"Disable TOTP. The caller must provide exactly one of:\n- ``password``: the user's current login password, OR\n- ``code``: the current 6-digit TOTP code.\n\nOn success: totp_enabled=False, totp_secret cleared, all recovery codes deleted.\nReturns 400 if neither credential matches or neither is provided.\nRequires: session cookie + X-CSRF-Token.",
"description":"Return the current TOTP status for the authenticated user.\n\nResponse contains only ``{\"enabled\": bool}``.\nSecret and recovery codes are NEVER returned here.\nRequires: session cookie only (no CSRF — read-only).",
"description":"Response schema for listing Modbus devices."
},
"ModbusDeviceResponse":{
"properties":{
"uuid":{
"type":"string",
"title":"Uuid"
},
"friendly_name":{
"type":"string",
"title":"Friendly Name"
},
"transport":{
"type":"string",
"title":"Transport"
},
"host":{
"type":"string",
"title":"Host"
},
"port":{
"type":"integer",
"title":"Port"
},
"unit_id":{
"type":"integer",
"title":"Unit Id"
},
"profile":{
"type":"string",
"title":"Profile"
},
"poll_interval_s":{
"type":"integer",
"title":"Poll Interval S"
},
"enabled":{
"type":"boolean",
"title":"Enabled"
},
"last_poll_at":{
"anyOf":[
{
"type":"string",
"format":"date-time"
},
{
"type":"null"
}
],
"title":"Last Poll At"
},
"last_poll_ok":{
"anyOf":[
{
"type":"boolean"
},
{
"type":"null"
}
],
"title":"Last Poll Ok"
},
"created_at":{
"type":"string",
"format":"date-time",
"title":"Created At"
},
"updated_at":{
"type":"string",
"format":"date-time",
"title":"Updated At"
}
},
"type":"object",
"required":[
"uuid",
"friendly_name",
"transport",
"host",
"port",
"unit_id",
"profile",
"poll_interval_s",
"enabled",
"last_poll_at",
"last_poll_ok",
"created_at",
"updated_at"
],
"title":"ModbusDeviceResponse",
"description":"Response schema for a single Modbus device."
},
"ModbusDeviceUpdate":{
"properties":{
"friendly_name":{
"anyOf":[
{
"type":"string",
"maxLength":255,
"minLength":1
},
{
"type":"null"
}
],
"title":"Friendly Name"
},
"transport":{
"anyOf":[
{
"type":"string",
"maxLength":16
},
{
"type":"null"
}
],
"title":"Transport"
},
"host":{
"anyOf":[
{
"type":"string",
"maxLength":255,
"minLength":1
},
{
"type":"null"
}
],
"title":"Host"
},
"port":{
"anyOf":[
{
"type":"integer",
"maximum":65535.0,
"minimum":1.0
},
{
"type":"null"
}
],
"title":"Port"
},
"unit_id":{
"anyOf":[
{
"type":"integer",
"maximum":247.0,
"minimum":0.0
},
{
"type":"null"
}
],
"title":"Unit Id"
},
"profile":{
"anyOf":[
{
"type":"string",
"maxLength":64,
"minLength":1
},
{
"type":"null"
}
],
"title":"Profile"
},
"poll_interval_s":{
"anyOf":[
{
"type":"integer",
"maximum":3600.0,
"minimum":1.0
},
{
"type":"null"
}
],
"title":"Poll Interval S"
},
"enabled":{
"anyOf":[
{
"type":"boolean"
},
{
"type":"null"
}
],
"title":"Enabled"
}
},
"type":"object",
"title":"ModbusDeviceUpdate",
"description":"Request body for PATCH /api/modbus/devices/{uuid} — all fields optional."
},
"ModbusLatestResponse":{
"properties":{
"found":{
"type":"boolean",
"title":"Found"
},
"recorded_at":{
"anyOf":[
{
"type":"string",
"format":"date-time"
},
{
"type":"null"
}
],
"title":"Recorded At"
},
"payload":{
"anyOf":[
{
"additionalProperties":true,
"type":"object"
},
{
"type":"null"
}
],
"title":"Payload"
}
},
"type":"object",
"required":[
"found",
"recorded_at",
"payload"
],
"title":"ModbusLatestResponse",
"description":"Response for the /latest endpoint.\n\n``found`` is False and ``recorded_at``/``payload`` are None when the device\nhas no readings yet. Callers should check ``found`` before using the values."
},
"ModbusMetricsResponse":{
"properties":{
"profile":{
"type":"string",
"title":"Profile"
},
"metrics":{
"items":{
"$ref":"#/components/schemas/MetricInfo"
},
"type":"array",
"title":"Metrics"
}
},
"type":"object",
"required":[
"profile",
"metrics"
],
"title":"ModbusMetricsResponse",
"description":"Response schema for GET /api/modbus/devices/{uuid}/metrics."
},
"ModbusProfilesResponse":{
"properties":{
"profiles":{
"items":{
"$ref":"#/components/schemas/ProfileSummary"
},
"type":"array",
"title":"Profiles"
}
},
"type":"object",
"required":[
"profiles"
],
"title":"ModbusProfilesResponse",
"description":"Response schema for GET /api/modbus/profiles."
},
"ModbusReadingResponse":{
"properties":{
"recorded_at":{
"type":"string",
"format":"date-time",
"title":"Recorded At"
},
"payload":{
"additionalProperties":true,
"type":"object",
"title":"Payload"
}
},
"type":"object",
"required":[
"recorded_at",
"payload"
],
"title":"ModbusReadingResponse",
"description":"A single reading row: timestamp + decoded payload."
"description":"Response schema for the readings time-range endpoint."
},
"ModbusTestReadResponse":{
"properties":{
"ok":{
"type":"boolean",
"title":"Ok"
},
"payload":{
"anyOf":[
{
"additionalProperties":true,
"type":"object"
},
{
"type":"null"
}
],
"title":"Payload"
},
"error":{
"anyOf":[
{
"type":"string"
},
{
"type":"null"
}
],
"title":"Error"
}
},
"type":"object",
"required":[
"ok"
],
"title":"ModbusTestReadResponse",
"description":"Response for POST /api/modbus/devices/{uuid}/test.\n\nOn success ``ok=True`` and ``payload`` contains the decoded values.\nOn failure ``ok=False`` and ``error`` describes the problem."
"description":"Disable TOTP by proving identity.\n\nExactly one of ``password`` or ``code`` must be provided."
},
"TotpEnableRequest":{
"properties":{
"code":{
"type":"string",
"title":"Code"
}
},
"type":"object",
"required":[
"code"
],
"title":"TotpEnableRequest",
"description":"The user confirms setup by providing the 6-digit TOTP code."
},
"TotpSetupResponse":{
"properties":{
"secret":{
"type":"string",
"title":"Secret"
},
"otpauth_uri":{
"type":"string",
"title":"Otpauth Uri"
},
"recovery_codes":{
"items":{
"type":"string"
},
"type":"array",
"title":"Recovery Codes"
}
},
"type":"object",
"required":[
"secret",
"otpauth_uri",
"recovery_codes"
],
"title":"TotpSetupResponse",
"description":"Returned once after a setup call.\n\n``secret`` and ``recovery_codes`` are **one-time plaintext values**.\nThey are never returned again by any subsequent API call.\nThe frontend must display and instruct the user to save them before confirming."
},
"TotpStatusResponse":{
"properties":{
"enabled":{
"type":"boolean",
"title":"Enabled"
}
},
"type":"object",
"required":[
"enabled"
],
"title":"TotpStatusResponse",
"description":"Minimal status response — never exposes secret or recovery codes."