openapi: 3.1.0 info: title: OTGW-firmware REST API description: | REST API for the OpenTherm Gateway (OTGW) firmware running on ESP8266. This API provides access to OpenTherm data, device information, settings, and control commands for the OpenTherm Gateway hardware. **Base URL**: `http://{device-ip}/api` **API Version**: v2. Earlier versions (v0, v1) have been removed and return 410 Gone. **Authentication**: Optional HTTP Basic Auth (username `admin`, password from settings). When no password is configured, all endpoints are open. When a password is set, mutating endpoints and settings require authentication. CSRF same-origin validation is enforced for authenticated browser requests. **Content-Type**: All responses are `application/json` **API Design** (ADR-035): - All error responses return structured JSON: `{"error":{"status":N,"message":"..."}}` - Queued operations return 202 Accepted - RESTful resource endpoints: `/otgw/messages/{id}`, `/otgw/commands`, `/otgw/discovery` - Consistent CORS headers on all responses - **OPTIONS preflight support** for all v2 endpoints (returns 204 with CORS headers) - **Allow header** on all 405 responses per RFC 7231 §6.5.5 version: 2.0.0 contact: name: Robert van den Breemen url: https://github.com/rvdbreemen/OTGW-firmware license: name: MIT url: https://opensource.org/licenses/MIT servers: - url: http://{device-ip}/api description: Local OTGW device variables: device-ip: default: otgw.local description: IP address or hostname of the OTGW device tags: - name: Health description: Device health and status checks - name: Device Info description: Device information and system status - name: Settings description: Device configuration and settings - name: OpenTherm Data description: Access OpenTherm message data - name: OTGW Commands description: Send commands to the OpenTherm Gateway - name: PIC Gateway description: PIC microcontroller status and settings - name: Flash/Upgrade description: Firmware upgrade status and operations - name: Sensors description: Dallas temperature sensor label management - name: Filesystem description: Filesystem and firmware file listing - name: Simulation description: OTGW simulation mode control - name: Webhook description: Webhook testing - name: Debug description: Flat key/value diagnostic dump of build, runtime, settings and state - name: Network description: WiFi scan and network diagnostics - name: SAT description: | SAT (Smart Autotune Thermostat) is an embedded heating controller that runs entirely on the ESP and turns the OTGW into a standalone smart thermostat. It implements a weather-compensated heating curve combined with a PID v3 controller with automatic gain tuning. Supports continuous modulation and PWM flame cycling. SAT endpoints are available on all builds but will return an error if SAT is not compiled in or not enabled. - name: OT Direct (OTGW32) description: | OT Direct endpoints are only available on OTGW32 hardware (`HAS_DIRECT_OT=1`). On standard ESP8266+PIC hardware all `/v2/otdirect/*` endpoints return `503 Service Unavailable` with `{"error":{"status":503,"message":"No OT-direct hardware - OTGW32 functions disabled"}}`. - name: Discovery description: Retained MQTT auto-discovery verification and republish (ADR-062) - name: MQTT description: Force a full OT-value republish to MQTT (distinct from discovery) paths: /v2/health: get: tags: - Health summary: Get device health status description: | Returns the current health status of the device including uptime, heap memory, connectivity status, and filesystem status. **Side effect**: Each call writes a small probe file (`/.health`) to LittleFS to verify the filesystem is writable (not just mounted). This incurs a flash write on every request. Do not poll this endpoint at high frequency from external monitoring tools - use 30-60 second intervals at minimum. The intended use-case is post-OTA boot detection, where polling stops immediately once `status: UP` is received. operationId: getHealth responses: '200': description: Health status retrieved successfully content: application/json: schema: type: object properties: health: type: object properties: status: type: string enum: [UP, DEGRADED] uptime: type: string example: "2d 3h 45m" heap: type: integer description: Free heap memory in bytes wifirssi: type: integer description: WiFi signal strength in dBm (0 when in AP or Ethernet mode) networkmode: type: string enum: [WiFi, Ethernet] description: Active network mode mqttconnected: type: boolean description: MQTT broker connected (real JSON boolean since the ArduinoJson v7 migration, ADR-141) otgwconnected: type: boolean description: OpenTherm gateway/bus online littlefsMounted: type: boolean description: LittleFS mounted and writable '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/debug: get: tags: - Debug summary: Flat diagnostic dump description: | Returns a flat JSON map of build identifiers, runtime counters, every persistent setting (sensitive fields masked), and the full transient state struct. The output is intended for bug reports and field support triage; the shape is not part of the long-term stable contract and may gain new keys between releases. Sensitive setting values (HTTP password, MQTT password, weather API key) are emitted as `"***"` when present and `"(not set)"` when empty. OT-direct sub-sections appear only on OTGW32 builds; SAT BLE and weather sub-sections only on ESP32 builds. Always requires authentication when an HTTP password is configured. operationId: getDebugDump security: - basicAuth: [] responses: '200': description: Diagnostic dump retrieved successfully content: application/json: schema: type: object properties: debug: type: object description: | Flat key/value map. Keys are dotted paths grouped under `build.*`, `runtime.*`, `settings.
.*`, `state.
.*`. See `handleDebugDump()` in `restAPI.ino` for the authoritative key list. '401': description: Authentication required '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/network/scan: get: tags: - Network summary: Asynchronous WiFi network scan description: | Triggers and reports a non-blocking WiFi network scan (TASK-585). First call starts the scan and returns `{"status":"scanning"}`. Subsequent polls return the same payload while the scan is in progress. Once complete the response is `{"status":"ready","count":N,"networks":[...]}`, the scan buffer is freed, and the next call starts a fresh scan. Refuses to scan while a PIC flash is in progress (returns 503). Quotes and backslashes in SSIDs are replaced with underscore to keep the JSON valid without escaping. operationId: scanWifiNetworks responses: '200': description: Scan in progress or completed content: application/json: schema: oneOf: - type: object properties: status: type: string enum: [scanning] - type: object properties: status: type: string enum: [ready] count: type: integer networks: type: array items: type: object properties: ssid: type: string rssi: type: integer description: Signal strength in dBm channel: type: integer secured: type: boolean connected: type: boolean description: True when this is the SSID the device is currently associated with examples: scanning: value: status: scanning ready: value: status: ready count: 2 networks: - ssid: "MyWiFi" rssi: -55 channel: 6 secured: true connected: true - ssid: "Guest" rssi: -78 channel: 11 secured: true connected: false '404': description: Unknown sub-resource (only `/scan` is implemented) content: application/json: schema: $ref: '#/components/schemas/ApiError' '405': $ref: '#/components/responses/MethodNotAllowedJson' '503': description: Scan unavailable during a PIC flash operation content: application/json: schema: $ref: '#/components/schemas/ApiError' /v2/settings: get: tags: - Settings summary: Get device settings description: | Retrieve all device configuration settings as a JSON object. Requires authentication when HTTP password is configured. Password fields use a protected round-trip format: - `password=0` means no password is currently stored - `password=XX` means a password is stored and `XX` is its length operationId: getSettings responses: '200': description: Settings retrieved successfully content: application/json: schema: type: object properties: settings: type: object description: All device settings with type metadata '401': description: Authentication required '405': $ref: '#/components/responses/MethodNotAllowedJson' post: security: - basicAuth: [] tags: - Settings summary: Update a single device setting description: | Update one device setting by name. Requires authentication when HTTP password is configured. Password field behavior for `httppasswd` and `mqttpasswd`: - `"notthispassword"` keeps the existing stored password unchanged - `""` clears the stored password - Any other string replaces the stored password operationId: updateSettings requestBody: required: true content: application/json: schema: type: object required: - name - value properties: name: type: string description: Setting field name value: description: New value for the setting example: name: "hostname" value: "MyOTGW" responses: '200': description: Settings updated successfully (echoes submitted JSON) '400': $ref: '#/components/responses/BadRequestJson' '401': description: Authentication required '403': description: CSRF protection - invalid origin put: security: - basicAuth: [] tags: - Settings summary: Update a single device setting (PUT alias) description: Alias for POST. Same behavior. operationId: updateSettingsPut requestBody: $ref: '#/paths/~1v2~1settings/post/requestBody' responses: '200': description: Settings updated successfully '400': $ref: '#/components/responses/BadRequestJson' '401': description: Authentication required '403': description: CSRF protection - invalid origin /v2/sensors: get: tags: - Sensors summary: Get sensor status (root alias) description: | Alias for `GET /v2/sensors/status`. Returns the current hardware sensor status including Dallas temperature sensor configuration and readings, and S0 pulse counter state. operationId: getSensorStatusRoot responses: '200': description: Sensor status retrieved successfully content: application/json: schema: $ref: '#/components/schemas/SensorStatus' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sensors/status: get: tags: - Sensors summary: Get sensor hardware status description: | Returns the current hardware sensor status as a JSON object with a `sensors` wrapper. Includes: - Dallas 1-Wire temperature sensor configuration (enabled, GPIO pin, poll interval) - Detection state and per-device readings with temperature (°C) and last-update epoch - S0 pulse counter configuration and current state (enabled, GPIO pin, pulse counts, power in kW) The `devices` sub-object is only present when sensors are detected or sensor simulation is active (`state.debug.bSensorSim`). operationId: getSensorStatus responses: '200': description: Sensor status retrieved successfully content: application/json: schema: $ref: '#/components/schemas/SensorStatus' example: sensors: dallas_enabled: true dallas_detected: true dallas_count: 2 dallas_gpio: 4 dallas_poll_sec: 30 simulated: false devices: "28FF64D1841703F1": temp: 21.5 epoch: 1774548600 "28FF94E2841703F2": temp: 18.3 epoch: 1774548600 s0: enabled: false gpio: 0 poll_sec: 10 pulses: 0 total: 0 power_kw: 0.000 epoch: 0 '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sensors/labels: get: tags: - Sensors summary: Get all Dallas sensor labels description: | Returns all configured Dallas temperature sensor labels. Each sensor is identified by its 1-Wire address and has a user-defined label. Returns empty object `{}` if no labels file exists. operationId: getAllDallasLabels responses: '200': description: Labels retrieved successfully content: application/json: schema: type: object additionalProperties: type: string example: "28FF64D1841703F1": "Living Room" "28FF94E2841703F2": "Kitchen" '405': $ref: '#/components/responses/MethodNotAllowedJson' post: security: - basicAuth: [] tags: - Sensors summary: Update all Dallas sensor labels description: | Update all Dallas temperature sensor labels. Provide a JSON object mapping sensor addresses to their new labels. operationId: updateAllDallasLabels requestBody: required: true content: application/json: schema: type: object additionalProperties: type: string example: "28FF64D1841703F1": "Living Room" "28FF94E2841703F2": "Kitchen" responses: '200': description: Labels updated successfully '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/device/info: get: tags: - Device Info summary: Get comprehensive device information description: | Returns device information wrapped in a top-level `device` object. Boolean values are proper JSON booleans (not strings) in this endpoint. Includes firmware version, network info, memory, connectivity status, and gateway/OT-Direct mode. The field set is **board-conditional and non-exhaustive**: - PIC fields (`picavailable`, `picfwversion`, `picdeviceid`, `picfwtype`, `otgwmode`) appear only on PIC/Classic hardware. - OT-Direct fields (`otd*`) appear only on the OTGW32 / OT-Direct board. - Diagnostic counters (`perf_*`, `hd_*`, `disc_*`) and per-interface network fields (`wifi_current_*`, `eth_current_*`) are also present; they are covered by `additionalProperties` rather than enumerated here. Consumers must ignore unknown keys. operationId: getDeviceInfo responses: '200': description: Device information retrieved successfully content: application/json: schema: type: object properties: device: type: object additionalProperties: true properties: author: type: string fwversion: type: string platform: type: string description: 'Chip platform ("ESP8266" or "ESP32")' board: type: string description: Human-readable board name hardware_type: type: string description: 'Board codepath token ("otgw32", "esp8266", "esp32-classic")' oledpresent: type: boolean description: OLED display present on this board ethernetpresent: type: boolean description: Ethernet hardware present on this board ethernetlink: type: boolean description: Ethernet link is up picavailable: type: boolean description: PIC co-processor present (PIC/Classic hardware only) picfwversion: type: string description: PIC firmware version (PIC/Classic hardware only) picdeviceid: type: string description: PIC device ID (PIC/Classic hardware only) picfwtype: type: string description: PIC firmware type (PIC/Classic hardware only) compiled: type: string hostname: type: string ipaddress: type: string macaddress: type: string freeheap: type: integer maxfreeblock: type: integer psram_found: type: integer description: '1 when PSRAM is present, 0 when absent (ESP32-S3)' psram_size: type: integer description: Total PSRAM size in bytes (0 when absent) psram_free: type: integer description: Free PSRAM in bytes flash_size: type: integer description: Flash chip size in bytes internal_free: type: integer description: Free internal (non-PSRAM) heap in bytes internal_maxblk: type: integer description: Largest free internal (non-PSRAM) heap block in bytes chip_model_est: type: string description: 'Best-effort ESP32-S3 package part number extrapolated from flash + PSRAM (e.g. "ESP32-S3FH4R2", "ESP32-S3FN8")' chipid: type: string coreversion: type: string sdkversion: type: string cpufreq: type: integer sketchsize: type: integer freesketchspace: type: integer flashchipid: type: string flashchipsize: type: number flashchiprealsize: type: number LittleFSsize: type: number flashchipspeed: type: number flashchipmode: type: string ssid: type: string wifirssi: type: integer wifiquality: type: integer wifiquality_text: type: string ntpenable: type: boolean ntptimezone: type: string uptime: type: string lastreset: type: string bootcount: type: integer mqttconnected: type: boolean thermostatconnected: type: boolean boilerconnected: type: boolean otgwmode: type: string description: '"on", "off", or "detecting"' otgwconnected: type: boolean otcommandinterface: type: string enum: [PIC, OT-Direct, None] description: Which OT command interface is active (sendDeviceInfoV2) otgwsimulation: type: boolean # Hardware platform fields board: type: string description: Hardware board identifier (e.g., "esp8266", "esp32s3") hardwaremode: type: string description: Active hardware mode ("pic" or "otdirect") networkmode: type: string enum: [WiFi, Ethernet] description: Active network mode # OT Direct fields - only present when otdirectavailable=true (OTGW32 builds) otdirectavailable: type: boolean description: True when OTGW32 OT-direct hardware is present and enabled otdmode: type: string enum: [gateway, monitor, bypass, master, loopback] description: Current OT-direct operating mode (OTGW32 only) otdbypass: type: boolean description: True when thermostat is routed directly to boiler via bypass relay otdmonitor: type: boolean description: True when engine is in transparent monitor/pass-through mode otdmaster: type: boolean description: True when OTGW32 is acting as sole OT master (no thermostat) otdstepup: type: boolean description: True when the 24V step-up converter is active otdthermostat: type: boolean description: True when a thermostat frame has been received within the setback timeout otdsetback: type: boolean description: True when thermostat is disconnected and setback override is active otdschedtotal: type: integer description: Total number of OT message IDs in the polling schedule otdschedactive: type: integer description: Number of schedule entries currently enabled (not disabled by boiler) otdscheddisabled: type: integer description: Number of schedule entries disabled because boiler returned UNKNOWN_DATA_ID otdoverrides: type: integer description: Number of active write-override slots examples: example: value: device: author: "Robert van den Breemen" fwversion: "1.3.0" picavailable: true hostname: "OTGW" ipaddress: "192.168.1.100" freeheap: 25600 mqttconnected: true otgwconnected: true otgwsimulation: false '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/device/time: get: tags: - Device Info summary: Get device date/time description: | Returns the current device date and time with timezone applied, plus runtime status fields. operationId: getDeviceTime responses: '200': description: Device time retrieved successfully content: application/json: schema: type: object properties: devtime: type: object properties: dateTime: type: string example: "2026-03-26 10:30:00" epoch: type: integer example: 1774548600 message: type: string description: Current status message text psmode: type: boolean description: Priority Service mode active otgwsimulation: type: boolean description: OTGW simulation mode active freeheap: type: integer description: Free heap memory in bytes maxfreeblock: type: integer description: Largest contiguous free block wifiquality: type: integer minimum: 0 maximum: 100 description: WiFi signal quality as a percentage (0-100). Returns 0 when in AP fallback mode, 100 when connected via Ethernet. networkmode: type: string enum: [WiFi, Ethernet] description: Active network mode apfallback: type: boolean description: "True when the device has fallen back to AP mode (SSID: OTGW-, IP: 192.168.4.1). Only present in pre-release builds and only when AP fallback is active." '405': $ref: '#/components/responses/MethodNotAllowedJson' '429': $ref: '#/components/responses/RateLimited' /v2/device/crashlog: get: tags: - Device Info summary: Get crash log description: | Returns the latest stored abnormal reboot/crash diagnostics, if available. Includes exception cause and stack trace summary. operationId: getDeviceCrashLog responses: '200': description: Crash log retrieved successfully content: application/json: schema: type: object properties: crashlog: type: object properties: available: type: boolean description: Whether a crash log is available summary: type: string description: Short crash summary (empty if not available) example: "Exception (28) at 0x40201234" details: type: string description: Detailed crash info (empty if not available) example: "epc1=0x40201234 epc2=0x00000000" '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/flash/status: get: tags: - Flash/Upgrade summary: Get unified flash status description: Returns flash status for both ESP8266 and PIC firmware upgrades. operationId: getFlashStatus responses: '200': description: Flash status retrieved successfully content: application/json: schema: type: object properties: flashstatus: type: object properties: flashing: type: boolean description: Any flash operation in progress pic_flashing: type: boolean description: PIC flash specifically in progress pic_progress: type: integer minimum: 0 maximum: 100 pic_filename: type: string pic_error: type: string '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/pic/flash-status: get: tags: - PIC Gateway summary: Get PIC flash status description: Minimal endpoint for polling PIC flash state during upgrade. operationId: getPICFlashStatus responses: '200': description: PIC flash status retrieved successfully content: application/json: schema: type: object properties: flashstatus: type: object properties: flashing: type: boolean progress: type: integer minimum: 0 maximum: 100 filename: type: string error: type: string '405': $ref: '#/components/responses/MethodNotAllowedJson' '503': $ref: '#/components/responses/PicUnavailable' /v2/pic/update-check: get: tags: - PIC Gateway summary: Check for PIC firmware updates description: | Checks for available PIC firmware updates by making an outbound HTTP request to otgw.tclcode.com. Only call on-demand (e.g., when the user opens the firmware tab). operationId: getPICUpdateCheck responses: '200': description: Update check result content: application/json: schema: type: object properties: pic_update: type: object properties: current: type: string description: Currently installed PIC firmware version latest: type: string description: Latest available version from otgw.tclcode.com update_available: type: boolean '405': $ref: '#/components/responses/MethodNotAllowedJson' '503': $ref: '#/components/responses/PicUnavailable' /v2/pic/settings: get: tags: - PIC Gateway summary: Get PIC gateway settings description: | Returns the cached PIC gateway settings last queried via PR= commands. Triggers a new readout cycle (one PR= command every 3 seconds, approximately 45 seconds for a full cycle). Empty string values mean "not yet queried" or "not supported by this firmware version". Source: Schelte Bron's OTGW firmware docs (https://otgw.tclcode.com/firmware.html) operationId: getPICSettings responses: '200': description: PIC settings retrieved successfully content: application/json: schema: type: object properties: pic_settings: type: object properties: setpoint_override: type: string description: Current temperature setpoint override (PR=A) setback: type: string description: Setback temperature (PR=B) dhw_override: type: string description: DHW override state gpio: type: string description: GPIO pin configuration (PR=G) gpio_states: type: string description: GPIO pin states (PR=I) led: type: string description: LED function assignments (PR=L) tweaks: type: string description: Tweak flags (PR=T) temp_sensor: type: string description: Temperature sensor reading (PR=O) smart_power: type: string description: Smart power mode (PR=P) thermostat_detect: type: string description: Thermostat detection mode (PR=D) builddate: type: string description: PIC firmware build date (PR=B) clock_mhz: type: string description: PIC clock speed (PR=C) reset_cause: type: string description: Last PIC reset cause (PR=Q) standalone_interval: type: string description: Standalone mode interval (PR=M) voltage_ref: type: string description: Voltage reference (PR=V) example: pic_settings: setpoint_override: "20.00" setback: "15.00" dhw_override: "" gpio: "0/1" gpio_states: "0/0" led: "F/X/O/M/P/C" tweaks: "1/1/1/1" temp_sensor: "O=19.50" smart_power: "Low" thermostat_detect: "I" builddate: "2023-01-01" clock_mhz: "4" reset_cause: "Power-on" standalone_interval: "0" voltage_ref: "3.3" '405': $ref: '#/components/responses/MethodNotAllowedJson' '503': $ref: '#/components/responses/PicUnavailable' /v2/firmware/files: get: tags: - Filesystem summary: List PIC firmware files description: Returns list of available PIC firmware files on the filesystem. operationId: getFirmwareFiles responses: '200': description: Firmware file list retrieved successfully content: application/json: schema: type: array items: type: object properties: name: type: string size: type: integer '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/filesystem/files: get: tags: - Filesystem summary: List filesystem files description: | Returns the contents of the LittleFS filesystem as a heterogeneous JSON array. Each leading element is a file or directory entry; the FINAL element is a storage-summary object (`usedBytes`/`totalBytes`/`freeBytes`/ `truncated`) rather than a file entry. Consumers must treat the last element specially (detect by absence of `name`/presence of `totalBytes`). operationId: getFilesystemFiles responses: '200': description: File list retrieved successfully content: application/json: schema: type: array items: anyOf: - type: object description: File or directory entry properties: name: type: string description: Entry name (no leading slash) size: type: integer description: File size in bytes (0 for directories) type: type: string enum: [file, dir] description: Entry type - type: object description: Trailing storage-summary object (last array element) properties: usedBytes: type: integer description: Bytes used on the filesystem totalBytes: type: integer description: Total filesystem capacity in bytes freeBytes: type: integer description: Free bytes remaining truncated: type: boolean description: True if the listing was truncated (too many entries) example: - name: index.html size: 39791 type: file - name: fonts size: 0 type: dir - usedBytes: 971980 totalBytes: 2097152 freeBytes: 1125172 truncated: false '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/filesystem/hash-check: get: tags: - Filesystem summary: Check firmware/filesystem version match description: | Compares the firmware git hash with the filesystem git hash. Used to detect mismatches between firmware and filesystem (e.g., after a firmware-only OTA update without flashing the new LittleFS image). operationId: getFilesystemHashCheck responses: '200': description: Hash check result content: application/json: schema: type: object properties: filesystem_check: type: object properties: match: type: boolean fw_hash: type: string fs_hash: type: string '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/simulate: get: tags: - Simulation summary: Get OTGW simulation status description: Returns the current OTGW simulation mode status. operationId: getSimulationStatus responses: '200': description: Simulation status retrieved content: application/json: schema: $ref: '#/components/schemas/SimulationStatus' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/simulate/start: post: tags: - Simulation summary: Start OTGW simulation description: | Enables OTGW simulation mode. The gateway replays data from `/otgw_simulation.log` instead of reading from the PIC serial port. operationId: startSimulation security: - basicAuth: [] responses: '200': description: Simulation started content: application/json: schema: $ref: '#/components/schemas/SimulationStatus' '405': $ref: '#/components/responses/MethodNotAllowedJson' put: security: - basicAuth: [] tags: - Simulation summary: Start OTGW simulation (PUT alias) operationId: startSimulationPut responses: '200': description: Simulation started content: application/json: schema: $ref: '#/components/schemas/SimulationStatus' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/simulate/stop: post: tags: - Simulation summary: Stop OTGW simulation description: | Disables OTGW simulation mode. Resumes reading from the live PIC serial port. operationId: stopSimulation security: - basicAuth: [] responses: '200': description: Simulation stopped content: application/json: schema: $ref: '#/components/schemas/SimulationStatus' '405': $ref: '#/components/responses/MethodNotAllowedJson' put: security: - basicAuth: [] tags: - Simulation summary: Stop OTGW simulation (PUT alias) operationId: stopSimulationPut responses: '200': description: Simulation stopped content: application/json: schema: $ref: '#/components/schemas/SimulationStatus' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/otgw/otmonitor: get: tags: - OpenTherm Data summary: Get all OpenTherm data description: | Returns all available OpenTherm data. Includes temperatures, status flags, pressures, S0 counter data (when enabled), and Dallas sensor data (when enabled). Each entry has value, unit, and lastupdated (seconds since boot) fields. operationId: getOTmonitor responses: '200': description: OTmonitor data retrieved successfully content: application/json: schema: type: object properties: otmonitor: type: object description: Map of OpenTherm data entries '405': $ref: '#/components/responses/MethodNotAllowedJson' '429': $ref: '#/components/responses/RateLimited' /v2/otgw/telegraf: get: tags: - OpenTherm Data summary: Get OpenTherm data (Telegraf alias) description: | Same response as `/v2/otgw/otmonitor`. Provided as a compatibility alias for Telegraf monitoring system integration. **This alias SHARES one rate-limit budget with `/v2/otgw/otmonitor`** (ADR-172). They are served by the same code path and emit the same bytes, so they are one endpoint for quota purposes — requests to either consume the same tokens, and alternating between them does not raise the ceiling. Budget: 1 request per 1.5 s sustained, burst 2. A scrape interval of 10 s or slower will effectively never be paced, even alongside an open dashboard. Scraping faster than ~2 s will draw `429`s, and Telegraf's `inputs.http` does not retry. operationId: getTelegraf responses: '200': description: Telegraf data retrieved successfully content: application/json: schema: type: object '405': $ref: '#/components/responses/MethodNotAllowedJson' '429': $ref: '#/components/responses/RateLimited' /v2/otgw/messages/{msgid}: get: tags: - OpenTherm Data summary: Get OpenTherm message by ID description: | Retrieve the current value for an OpenTherm message by its numeric ID (0-127). The value type depends on the message: float for f88 (fixed-point) types, integer for all others. operationId: getOTGWMessage parameters: - name: msgid in: path required: true schema: type: integer minimum: 0 maximum: 127 responses: '200': description: Message value retrieved successfully content: application/json: schema: type: object properties: label: type: string value: {} unit: type: string example: label: "boilertemperature" value: 45.5 unit: "\u00b0C" '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/otgw/commands: post: tags: - OTGW Commands summary: Send command to OpenTherm Gateway description: | Send a command to the OpenTherm Gateway. The command is provided in the JSON request body. Returns 202 Accepted because the command is queued for asynchronous processing. If the body is not valid JSON with a `command` field, the raw body text is used as the command string. Command format: two alphabetic characters + `=` + value (e.g., `TT=20.5`). operationId: sendOTGWCommand security: - basicAuth: [] requestBody: required: true content: application/json: schema: type: object required: - command properties: command: type: string pattern: '^[A-Za-z]{2}=.+$' description: OTGW command in format XX=value example: command: "TT=20.5" responses: '202': description: Command queued for processing content: application/json: schema: type: object properties: status: type: string enum: [queued] example: status: "queued" '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' '413': description: Command too long content: application/json: schema: $ref: '#/components/schemas/ApiError' put: security: - basicAuth: [] tags: - OTGW Commands summary: Send command (PUT alias) operationId: sendOTGWCommandPut requestBody: $ref: '#/paths/~1v2~1otgw~1commands/post/requestBody' responses: '202': description: Command queued for processing '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/otgw/discovery: post: tags: - OTGW Commands summary: Trigger MQTT autodiscovery description: | Marks all known OT message IDs as pending and triggers asynchronous drip-publishing of MQTT autodiscovery configs for Home Assistant integration. Discovery configs are composed by hand-written streaming functions in `MQTTHaDiscovery.cpp` (see ADR-077). The legacy runtime template file `data/mqttha.cfg` is archived under `docs/archive/` as historical reference only. The drip publisher issues one config every 3 seconds (slowed to every 30 seconds under heap pressure) to avoid MQTT burst storms. Produced entity categories: 289 sensors, 53 binary_sensors, 2 climate (Thermostat + DHW Control), 1 number (Toutside_override), 13 SAT switches and 1 SAT select (TASK-284), plus runtime Dallas sensor entries. Returns 202 Accepted. operationId: triggerDiscovery security: - basicAuth: [] responses: '202': description: Discovery accepted for processing content: application/json: schema: type: object properties: status: type: string enum: [accepted] example: status: "accepted" '405': $ref: '#/components/responses/MethodNotAllowedJson' put: tags: - OTGW Commands summary: Trigger MQTT autodiscovery (PUT alias) operationId: triggerDiscoveryPut security: - basicAuth: [] responses: '202': description: Discovery accepted for processing '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/discovery: get: tags: - Discovery summary: Get discovery verification status, counters and settings description: | Returns the current state of the retained-discovery verification mechanism introduced in 1.4.1 (ADR-062). This is an observational endpoint: it does not start a verify run or publish anything. Use it to inspect whether a verify window is currently active, when the last run completed, how many retained configs were missing or orphaned on the last run, and whether automatic daily verification is enabled. This endpoint is distinct from `/v2/otgw/discovery`, which publishes all Home Assistant discovery topics unconditionally. operationId: getDiscoveryStatus responses: '200': description: Discovery state retrieved successfully content: application/json: schema: type: object required: - verification - counters - settings properties: verification: type: object properties: active: type: boolean description: True while the 15-second verify window is open. last_epoch: type: integer format: uint32 description: Unix-epoch timestamp of the last completed verify run (0 = never run since boot). last_missing: type: integer format: uint16 description: Retained discovery topics expected but not observed on the last verify run. last_orphan: type: integer format: uint16 description: Retained discovery topics observed under `/+//#` that do not belong to this node. Informational only; orphans are never deleted by OTGW (see ADR-062). counters: type: object properties: published_topics: type: integer format: uint32 description: Running count of discovery topics successfully published by the streaming helpers since boot (or since the last `clearMQTTConfigDone`). pending_ids: type: integer format: uint16 description: Number of discovery IDs currently marked pending in the drip pipeline. verify_runs: type: integer format: uint32 description: Lifetime count of verify windows started since boot. republish_triggered: type: integer format: uint32 description: Lifetime count of verify runs that ended with `last_missing > 0` and triggered `markAllMQTTConfigPending()`. settings: type: object properties: auto_verify: type: boolean description: Reflects `settings.mqtt.bDiscoveryAutoVerify`. When true, a verify run is triggered automatically once per day at the day-flip boundary. example: verification: active: false last_epoch: 1745236800 last_missing: 0 last_orphan: 0 counters: published_topics: 82 pending_ids: 0 verify_runs: 4 republish_triggered: 0 settings: auto_verify: true '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/discovery/verify: post: tags: - Discovery summary: Start a retained-discovery verification window description: | Opens a 15-second subscription window on `/+//#` and counts retained discovery configs seen against `state.discovery.iPublishedTopicCount`. On completion the firmware re-announces any missing configs via the drip. See ADR-062 for the full mechanism. The handler refuses to start the window under several conditions (see error responses); in that case the caller should not retry immediately. operationId: startDiscoveryVerify security: - basicAuth: [] responses: '202': description: Verification window started content: application/json: schema: type: object properties: status: type: string enum: [verification_started] expected: type: integer format: uint32 description: Number of retained configs the firmware expects to see during the window. window_ms: type: integer description: Window duration in milliseconds (currently 15000). example: status: "verification_started" expected: 82 window_ms: 15000 '405': $ref: '#/components/responses/MethodNotAllowedJson' '409': description: Verification cannot start because discovery is busy. content: application/json: schema: $ref: '#/components/schemas/ApiError' examples: alreadyActive: summary: Verify already running value: error: status: 409 message: "Verification already active" dripInProgress: summary: Discovery drip still publishing value: error: status: 409 message: "Discovery drip in progress" '503': description: Verification cannot start because a precondition failed. content: application/json: schema: $ref: '#/components/schemas/ApiError' examples: mqttDown: summary: MQTT not connected value: error: status: 503 message: "MQTT not connected" lowHeap: summary: Free heap below start threshold value: error: status: 503 message: "Heap too low for verify" startRefused: summary: Verification layer refused the start (see telnet log) value: error: status: 503 message: "Verification start refused (see telnet log)" /v2/discovery/republish: post: tags: - Discovery summary: Mark all discovery IDs pending for immediate re-announce description: | Calls `markAllMQTTConfigPending()` directly, without running a verify pass first. All discovery IDs are flagged pending and the drip will re-publish them at its configured cadence. Use this when you already know the broker's retained state is bad (for example after a broker reinstall without persistence) and want to skip the verify window. For normal troubleshooting, prefer `POST /v2/discovery/verify`, which only triggers a republish when retained configs are actually missing. operationId: republishDiscovery security: - basicAuth: [] responses: '200': description: All discovery IDs marked pending content: application/json: schema: type: object properties: status: type: string enum: [marked_pending] count: type: integer format: uint16 description: Number of discovery IDs now pending in the drip pipeline. example: status: "marked_pending" count: 82 '429': description: Republish cooldown active (a 60s rate-limit between republishes). content: application/json: schema: $ref: '#/components/schemas/ApiError' example: error: status: 429 message: "Republish cooldown active, retry in 60s" '405': $ref: '#/components/responses/MethodNotAllowedJson' '503': description: MQTT is not connected; nothing was marked. content: application/json: schema: $ref: '#/components/schemas/ApiError' example: error: status: 503 message: "MQTT not connected" /v2/mqtt/republish: post: security: - basicAuth: [] tags: - MQTT summary: Force a full OT-value republish description: | Resets MQTT publish eligibility so every observed OpenTherm value is re-published as if seen for the first time (TASK-936). Use this to repopulate a broker after a wipe without re-announcing Home Assistant discovery configs. This is distinct from `POST /v2/discovery/republish`, which re-announces the HA discovery *configs* rather than the OT *values*. A 60-second cooldown rate-limits repeated calls (CWE-770); a call inside the window returns 429 with the remaining seconds. operationId: republishMqttValues responses: '200': description: Republish requested content: application/json: schema: type: object properties: status: type: string enum: [republish_requested] example: status: "republish_requested" '405': $ref: '#/components/responses/MethodNotAllowedJson' '429': description: Republish cooldown active (a 60s rate-limit between republishes). content: application/json: schema: $ref: '#/components/schemas/ApiError' example: error: status: 429 message: "Republish cooldown active, retry in 60s" '503': description: MQTT is not connected; nothing was republished. content: application/json: schema: $ref: '#/components/schemas/ApiError' example: error: status: 503 message: "MQTT not connected" /v2/webhook/test: post: security: - basicAuth: [] tags: - Webhook summary: Test webhook delivery description: | Triggers a test webhook call to verify the configured webhook URL is reachable. Requires authentication when HTTP password is configured. operationId: testWebhook parameters: - name: state in: query required: true schema: type: string enum: ["on", "off", "1", "0"] description: Webhook state to test (on/1 or off/0) responses: '200': description: Webhook test completed content: application/json: schema: type: object properties: status: type: string enum: [ok] example: status: "ok" '400': $ref: '#/components/responses/BadRequestJson' '401': description: Authentication required '403': description: CSRF protection - invalid origin '405': $ref: '#/components/responses/MethodNotAllowedJson' put: security: - basicAuth: [] tags: - Webhook summary: Test webhook delivery (PUT alias) operationId: testWebhookPut parameters: - name: state in: query required: true schema: type: string enum: ["on", "off", "1", "0"] responses: '200': description: Webhook test completed '400': $ref: '#/components/responses/BadRequestJson' '401': description: Authentication required '405': $ref: '#/components/responses/MethodNotAllowedJson' # --- OT Direct endpoints (OTGW32 only) --- /v2/otdirect/status: get: tags: - OT Direct (OTGW32) summary: Get OT Direct runtime status description: | Returns the full OTGW32 OT-direct runtime status. All fields reflect the current in-memory state of the OT-direct engine. **OTGW32 only** - returns 503 on standard ESP8266+PIC hardware. operationId: getOTDirectStatus responses: '200': description: OT Direct status retrieved successfully content: application/json: schema: $ref: '#/components/schemas/OTDirectStatus' example: otdirect_status: mode: "gateway" bypass: false stepup: true monitor_mode: false master_mode: false thermostat_connected: true setback_active: false schedule_total: 12 schedule_active: 11 schedule_disabled: 1 overrides_active: 2 ot_online: true thermostat: true boiler: true '503': description: OT Direct hardware not available (standard PIC build) content: application/json: schema: $ref: '#/components/schemas/ApiError' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/otdirect/mode: post: security: - basicAuth: [] tags: - OT Direct (OTGW32) summary: Set OT Direct operating mode description: | Switch the OTGW32 OT-direct engine to the requested operating mode. The command is translated to the equivalent `GW=` command internally. Supported modes: | Mode | Equivalent | Description | |------|------------|-------------| | `gateway` | `GW=1` | Full gateway: scheduler + thermostat forwarding + overrides (default) | | `monitor` | `GW=0` | Monitor / PIC parity (TASK-438) | | `bypass` | `GW=P` | Bypass alias (TASK-438) | | `master` | `GW=S` | Sole OT master: scheduler only, no thermostat expected | | `loopback` | `GW=L` | Internal loopback test: simulated boiler responses, no hardware needed | The response echoes the current status *after* the mode switch command has been queued; the `mode` field in the response reflects the new mode. **OTGW32 only** - returns 503 on standard ESP8266+PIC hardware. operationId: setOTDirectMode requestBody: required: true content: application/x-www-form-urlencoded: schema: type: object required: - mode properties: mode: type: string enum: [gateway, monitor, bypass, master, loopback] description: Operating mode to activate responses: '200': description: Mode switch queued; response contains current status content: application/json: schema: $ref: '#/components/schemas/OTDirectStatus' '400': description: Invalid mode string, or `mode` parameter missing. content: application/json: schema: $ref: '#/components/schemas/ApiError' examples: invalidMode: value: error: status: 400 message: "Invalid mode. Use: gateway, monitor, bypass, master, loopback" missingParam: value: error: status: 400 message: "Missing 'mode' parameter" '503': description: OT Direct hardware not available (standard PIC build) content: application/json: schema: $ref: '#/components/schemas/ApiError' '405': $ref: '#/components/responses/MethodNotAllowedJson' put: security: - basicAuth: [] tags: - OT Direct (OTGW32) summary: Set OT Direct operating mode (PUT alias) operationId: setOTDirectModePut requestBody: $ref: '#/paths/~1v2~1otdirect~1mode/post/requestBody' responses: '200': description: Mode switch queued content: application/json: schema: $ref: '#/components/schemas/OTDirectStatus' '400': $ref: '#/components/responses/BadRequestJson' '503': description: OT Direct hardware not available '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/otdirect/settings: get: tags: - OT Direct (OTGW32) summary: Get OT Direct settings description: | Returns the persisted OT-direct configuration settings. **OTGW32 only** - returns 503 on standard ESP8266+PIC hardware. operationId: getOTDirectSettings responses: '200': description: OT Direct settings retrieved successfully content: application/json: schema: type: object properties: otdirect_settings: type: object properties: mode: type: integer description: Persisted mode index (0=bypass, 1=gateway, 2=monitor, 3=master, 4=loopback) setback_temp: type: number description: Setback temperature applied when thermostat disconnects (C, range 1-30) example: 15.0 setback_timeout: type: integer description: Seconds without a thermostat frame before setback activates example: 300 ch_mode: type: integer description: CH control mode (0=fixed setpoint, 1=heating curve, 2=PI room compensation) flow_temp: type: number description: Fixed flow temperature setpoint (C, used when ch_mode=0) flow_max: type: number description: Maximum flow temperature limit (C) room_setpoint: type: number description: Room temperature setpoint for PI compensation (C) gradient: type: number description: Heating curve gradient exponent: type: number description: Heating curve exponent offset: type: number description: Heating curve offset (C) room_comp: type: boolean description: Whether PI room compensation is enabled kp: type: number description: PI proportional gain ki: type: number description: PI integral gain kboost: type: number description: PI boost gain for large errors example: otdirect_settings: mode: 1 setback_temp: 15.0 setback_timeout: 300 ch_mode: 0 flow_temp: 55.0 flow_max: 80.0 room_setpoint: 20.0 gradient: 1.5 exponent: 1.3 offset: 0.0 room_comp: false kp: 2.0 ki: 0.001 kboost: 5.0 '503': description: OT Direct hardware not available (standard PIC build) content: application/json: schema: $ref: '#/components/schemas/ApiError' '405': $ref: '#/components/responses/MethodNotAllowedJson' post: security: - basicAuth: [] tags: - OT Direct (OTGW32) summary: Update OT Direct settings description: | Update one or both OT-direct settings. Only provided parameters are updated. Returns the current full OT-direct status after the update. **OTGW32 only** - returns 503 on standard ESP8266+PIC hardware. operationId: updateOTDirectSettings requestBody: required: false content: application/x-www-form-urlencoded: schema: type: object properties: setbacktemp: type: string description: Setback temperature in C (range 1.0-30.0) example: "15.0" setbacktimeout: type: string description: Setback timeout in seconds example: "300" chmode: type: string description: CH control mode (0=fixed, 1=heating curve, 2=PI room comp) flowtemp: type: string description: Fixed flow temperature setpoint (C) flowmax: type: string description: Maximum flow temperature limit (C) roomsetpoint: type: string description: Room setpoint for PI compensation (C) gradient: type: string description: Heating curve gradient exponent: type: string description: Heating curve exponent offset: type: string description: Heating curve offset (C) roomcomp: type: string description: PI room compensation enabled (true/false) kp: type: string description: PI proportional gain ki: type: string description: PI integral gain kboost: type: string description: PI boost gain responses: '200': description: Settings updated; response contains current full OT-direct status content: application/json: schema: $ref: '#/components/schemas/OTDirectStatus' '503': description: OT Direct hardware not available (standard PIC build) content: application/json: schema: $ref: '#/components/schemas/ApiError' '405': $ref: '#/components/responses/MethodNotAllowedJson' put: security: - basicAuth: [] tags: - OT Direct (OTGW32) summary: Update OT Direct settings (PUT alias) operationId: updateOTDirectSettingsPut requestBody: $ref: '#/paths/~1v2~1otdirect~1settings/post/requestBody' responses: '200': description: Settings updated content: application/json: schema: $ref: '#/components/schemas/OTDirectStatus' '503': description: OT Direct hardware not available '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/otdirect/overrides: get: tags: - OT Direct (OTGW32) summary: Get active OT Direct overrides description: | Returns all currently active write overrides and response overrides in the OT-direct engine as a JSON object. **OTGW32 only** - returns 503 on standard ESP8266+PIC hardware. operationId: getOTDirectOverrides responses: '200': description: Override list retrieved successfully content: application/json: schema: type: object description: | JSON object containing override lists. Structure defined by `getOTDirectOverridesJSON()` in `OTDirect.ino`. '503': description: OT Direct hardware not available (standard PIC build) content: application/json: schema: $ref: '#/components/schemas/ApiError' '405': $ref: '#/components/responses/MethodNotAllowedJson' post: security: - basicAuth: [] tags: - OT Direct (OTGW32) summary: Manage OT Direct overrides description: | Add, clear, or manage override entries in the OT-direct engine. All actions require `msgid` (0-127). Actions `sr` and `rm` additionally require `value` (4-hex-digit data word). | Action | OT command | Description | |--------|------------|-------------| | `sr` | `SR=MsgID:HHHH` | Set stored response - gateway answers thermostat with fixed data for this MsgID | | `cr` | `CR=MsgID` | Clear stored response | | `rm` | `RM=MsgID:HHHH` | Set response modifier - modify boiler→thermostat response data for this MsgID | | `cm` | `CM=MsgID` | Clear response modifier | | `ui` | `UI=MsgID` | Mark MsgID as unknown - gateway responds UNKNOWN_DATAID to thermostat | | `ki` | `KI=MsgID` | Mark MsgID as known again - restore normal forwarding | Returns the updated override list after the command is queued. **OTGW32 only** - returns 503 on standard ESP8266+PIC hardware. operationId: manageOTDirectOverrides requestBody: required: true content: application/x-www-form-urlencoded: schema: type: object required: - action - msgid properties: action: type: string enum: [sr, cr, rm, cm, ui, ki] description: Override action to perform msgid: type: integer minimum: 0 maximum: 127 description: OpenTherm Message ID (0-127) value: type: string pattern: '^[0-9A-Fa-f]{4}$' description: 4-hex-digit data word (required for `sr` and `rm` actions) example: "3C00" responses: '200': description: Override command queued; response contains updated override list content: application/json: schema: type: object description: Updated override list '400': description: | Invalid or missing parameters. Possible causes: - `action` parameter missing or not one of the valid values - `msgid` missing or out of range (0-127) - `value` missing for `sr` or `rm` actions - `value` format invalid (must be exactly 4 hex digits) content: application/json: schema: $ref: '#/components/schemas/ApiError' '503': description: OT Direct hardware not available (standard PIC build) content: application/json: schema: $ref: '#/components/schemas/ApiError' '405': $ref: '#/components/responses/MethodNotAllowedJson' put: security: - basicAuth: [] tags: - OT Direct (OTGW32) summary: Manage OT Direct overrides (PUT alias) operationId: manageOTDirectOverridesPut requestBody: $ref: '#/paths/~1v2~1otdirect~1overrides/post/requestBody' responses: '200': description: Override command queued '400': $ref: '#/components/responses/BadRequestJson' '503': description: OT Direct hardware not available '405': $ref: '#/components/responses/MethodNotAllowedJson' # --- backward-compatibility aliases (prefer primary endpoints above) --- /v2/otgw/id/{msgid}: get: tags: - OpenTherm Data summary: Get OpenTherm message by ID (alias) description: | Alias for `/v2/otgw/messages/{msgid}`. **Prefer** `/v2/otgw/messages/{msgid}` for new integrations. operationId: getOTGWValueByIdAlias parameters: - name: msgid in: path required: true schema: type: integer minimum: 0 maximum: 127 responses: '200': description: Message value retrieved successfully content: application/json: schema: type: object '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/otgw/label/{msglabel}: get: tags: - OpenTherm Data summary: Get OpenTherm message by label description: | Retrieve the current value for an OpenTherm message using its human-readable label. Label matching is case-insensitive. operationId: getOTGWLabel parameters: - name: msglabel in: path required: true schema: type: string example: "boilertemperature" responses: '200': description: Message value retrieved successfully content: application/json: schema: type: object properties: label: type: string value: {} unit: type: string '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/otgw/command/{command}: post: tags: - OTGW Commands summary: Send OTGW command via URL path (alias) description: | Alias for `/v2/otgw/commands`. **Prefer** `/v2/otgw/commands` (JSON body) for new integrations. Returns 202 Accepted. operationId: sendOTGWCommandAlias security: - basicAuth: [] parameters: - name: command in: path required: true schema: type: string pattern: '^[A-Za-z]{2}=.+$' example: "TT=20.5" responses: '202': description: Command queued for processing content: application/json: schema: type: object properties: status: type: string enum: [queued] '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' '413': description: Command too long content: application/json: schema: $ref: '#/components/schemas/ApiError' put: security: - basicAuth: [] tags: - OTGW Commands summary: Send OTGW command via URL path (PUT alias) operationId: sendOTGWCommandAliasPut parameters: - name: command in: path required: true schema: type: string responses: '202': description: Command queued for processing '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/otgw/autoconfigure: post: security: - basicAuth: [] tags: - OTGW Commands summary: Trigger MQTT autodiscovery (alias) description: | Alias for `/v2/otgw/discovery`. **Prefer** `/v2/otgw/discovery` for new integrations. Returns 202 Accepted. operationId: autoConfigureAlias responses: '202': description: Discovery accepted for processing content: application/json: schema: type: object properties: status: type: string enum: [accepted] '405': $ref: '#/components/responses/MethodNotAllowedJson' put: security: - basicAuth: [] tags: - OTGW Commands summary: Trigger MQTT autodiscovery (PUT alias) operationId: autoConfigureAliasPut responses: '202': description: Discovery accepted for processing '405': $ref: '#/components/responses/MethodNotAllowedJson' # SAT (Smart Autotune Thermostat) endpoints /v2/sat: get: tags: - SAT summary: Get SAT status description: | Returns the full SAT runtime state. Equivalent to `GET /v2/sat/status`. operationId: getSatRoot responses: '200': description: SAT status content: application/json: schema: $ref: '#/components/schemas/SatStatus' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/status: get: tags: - SAT summary: Get SAT runtime status description: | Returns the full SAT runtime state as a flat JSON object with 132 fields covering control mode, setpoints, PID state, temperature sensors, cycle diagnostics, pressure, power, thermal model, comfort, and BLE inputs (schema `SatStatus`). Add `?detail=full` to get the compact health view instead: a SEPARATE 22-field object (schema `SatHealth`) focused on cycle/flame/device health, sync flags, error statistics, pressure trend, and auto-tune progress. It is not a superset of the standard status. operationId: getSatStatus parameters: - name: detail in: query required: false schema: type: string enum: [full] description: | Pass `full` to get the compact `SatHealth` object instead of the standard `SatStatus` JSON. responses: '200': description: | SAT status retrieved successfully. Returns `SatStatus` by default, or `SatHealth` when `?detail=full` is supplied. content: application/json: schema: anyOf: - $ref: '#/components/schemas/SatStatus' - $ref: '#/components/schemas/SatHealth' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/force-boiler: post: security: - basicAuth: [] tags: - SAT summary: Force boiler-present override (TEST/DEBUG hook) description: | **Test/debug hook — not for production use.** Asserts that a boiler is present so the SAT availability gate (edge auto-disable, REST 409, MQTT enable-reject) can be verified on the bench without real hardware (TASK-802). The override is transient: it lives in RAM only and is cleared on reboot. The value may be supplied in the body (`0`, `1`, `true`, `false`) or in the path as `/v2/sat/force-boiler/1`. operationId: setSatForceBoiler requestBody: required: false content: text/plain: schema: type: string enum: ["0", "1", "true", "false"] example: "1" responses: '200': description: Override applied content: application/json: schema: type: object properties: status: type: string enum: [ok] force_boiler_present: type: boolean example: status: ok force_boiler_present: true '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' put: security: - basicAuth: [] tags: - SAT summary: Force boiler-present override (PUT alias) operationId: setSatForceBoilerPut requestBody: $ref: '#/paths/~1v2~1sat~1force-boiler/post/requestBody' responses: '200': description: Override applied '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/target: post: security: - basicAuth: [] tags: - SAT summary: Set SAT target temperature description: | Sets the target room temperature for the SAT controller. The value is validated (5.0–30.0 °C) and persisted to flash. Body may be a plain numeric string (`"21.5"`) or a JSON object (`{"value":"21.5"}`). operationId: setSatTarget requestBody: required: true content: text/plain: schema: type: string example: "21.5" application/json: schema: type: object properties: value: type: string example: "21.5" responses: '200': description: Target temperature set content: application/json: schema: $ref: '#/components/schemas/StatusOk' '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/externaltemp: post: security: - basicAuth: [] tags: - SAT summary: Push indoor temperature from external sensor description: | Injects an indoor temperature reading from an external sensor. The reading expires after the configured `sensor_max_age` (default 6 hours). Body is a plain numeric string or JSON `{"value":"20.8"}`. operationId: setSatExternalTemp requestBody: required: true content: text/plain: schema: type: string example: "20.8" responses: '200': description: Indoor temperature accepted content: application/json: schema: $ref: '#/components/schemas/StatusOk' '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/externaloutdoor: post: security: - basicAuth: [] tags: - SAT summary: Push outdoor temperature from external source description: | Injects an outdoor temperature reading. Expires after sensor max age. Body is a plain numeric string or JSON `{"value":"8.0"}`. operationId: setSatExternalOutdoor requestBody: required: true content: text/plain: schema: type: string example: "8.0" responses: '200': description: Outdoor temperature accepted content: application/json: schema: $ref: '#/components/schemas/StatusOk' '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/pvsurplus: post: security: - basicAuth: [] tags: - SAT summary: Push PV-surplus power (TASK-640) description: | Injects a PV-surplus power reading in Watts. When the surplus exceeds the configured threshold for the hold-time and feature is enabled (`satpvboostenabled`), SAT applies an additive boost of `satpvboostdeltac` °C to the effective target. Reading expires after `sensor_max_age` (default 6 h). Body is a plain numeric string or JSON `{"value":"1800"}`. Range: 0 to 50000 W. operationId: setSatPvSurplus requestBody: required: true content: text/plain: schema: type: string example: "1800" responses: '200': description: PV-surplus value accepted content: application/json: schema: $ref: '#/components/schemas/StatusOk' '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/sim/event: post: security: - basicAuth: [] tags: - SAT summary: Inject a simulation scenario event (TASK-797) description: | Perturbs the synthetic room model so testers can exercise SAT responses on a bench without real hardware. Honoured ONLY while simulation is active (`satsimulation` enabled); returns HTTP 409 otherwise, so it can never perturb a real-boiler installation. Supported events: - `window_open` — multiplies the room cooling rate for `duration_s`. `value` = loss multiplier (default 3, clamped 1..20). - `solar_gain` — additive room warming for `duration_s`. `value` = °C/min (default 0.5, clamped 0..5). - `sensor_noise` — adds ± noise to flow/return sensor reads for `duration_s` (TASK-799), exercising staleness/EMA paths. `value` = amplitude °C (default 0.5, clamped 0..5); `value` 0 turns noise off. - `sensor_dropout` — drops the room sensor (room read returns NAN) for `duration_s` (TASK-799), exercising the ghost-Tr / fallback path. `value` 0 with no duration cancels an active dropout. - `dhw_demand` — triggers a DHW draw for `duration_s` (default 120 s, TASK-800): forces DHW-active (flame-steal, CH suppressed) so the cycle classifier sees DHW/MIXED cycles. `value` 0 + no duration cancels. `duration_s` is optional (default 600 s; 120 s for `dhw_demand`). `pressure_drop` is deferred (SATpressure coupling); `pv_surplus` has its own `/v2/sat/pvsurplus` endpoint. Unknown/unsupported events return 400. operationId: injectSatSimEvent requestBody: required: true content: application/json: schema: type: object required: [event] properties: event: type: string enum: [window_open, solar_gain, sensor_noise, sensor_dropout, dhw_demand] example: window_open value: type: number example: 3 duration_s: type: integer example: 600 responses: '200': description: Scenario event accepted content: application/json: schema: $ref: '#/components/schemas/StatusOk' '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' '409': description: Simulation inactive — enable satsimulation first content: application/json: schema: $ref: '#/components/schemas/ApiError' /v2/sat/reset_integral: post: security: - basicAuth: [] tags: - SAT summary: Reset PID integral accumulator description: | Resets the PID I-term to zero. Useful after large setpoint changes to prevent integral windup from causing overshoot. operationId: satResetIntegral responses: '200': description: Integral reset content: application/json: schema: $ref: '#/components/schemas/StatusOk' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/flush: post: security: - basicAuth: [] tags: - SAT summary: Flush short-lived SAT data description: | Clears short-lived SAT data including the PID integral accumulator and cycle statistics window. Useful after large configuration changes or when troubleshooting control behavior. operationId: satFlush responses: '200': description: Data flushed content: application/json: schema: type: object properties: result: type: string enum: [ok] flushed: type: array items: type: string example: result: ok flushed: ["pid", "cycles"] '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/window: post: security: - basicAuth: [] tags: - SAT summary: Set window open/closed state description: | Reports window open/closed state to SAT. When a window is open, SAT reduces the setpoint to avoid heating a ventilated room. Payload: `"open"`, `"closed"`, `"1"`, `"0"`, or `"ON"`. operationId: setSatWindow requestBody: required: true content: text/plain: schema: type: string example: "open" responses: '200': description: Window state accepted content: application/json: schema: $ref: '#/components/schemas/StatusOk' '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/preset: post: security: - basicAuth: [] tags: - SAT summary: Activate a named preset description: | Switches the SAT target temperature to the configured value for the named preset. Valid preset names: `comfort`, `eco`, `away`, `sleep`, `activity`, `home`. operationId: setSatPreset requestBody: required: true content: text/plain: schema: type: string enum: [comfort, eco, away, sleep, activity, home] example: "away" responses: '200': description: Preset activated content: application/json: schema: $ref: '#/components/schemas/StatusOk' '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/enable: post: security: - basicAuth: [] tags: - SAT summary: Enable or disable SAT controller description: | Enables or disables the SAT controller. Payload: `"1"`, `"true"`, `"on"` to enable; `"0"`, `"false"`, `"off"` to disable. operationId: setSatEnable requestBody: required: true content: text/plain: schema: type: string example: "1" responses: '200': description: SAT enabled/disabled content: application/json: schema: $ref: '#/components/schemas/StatusOk' '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/mode: post: security: - basicAuth: [] tags: - SAT summary: Set SAT control mode description: | Sets the SAT control mode. Valid values: `"continuous"`, `"pwm"`, `"auto"`, or the numeric equivalents `"0"`, `"1"`, `"2"`. operationId: setSatMode requestBody: required: true content: text/plain: schema: type: string enum: [continuous, pwm, auto, "0", "1", "2"] example: "continuous" responses: '200': description: Control mode set content: application/json: schema: $ref: '#/components/schemas/StatusOk' '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/humidity: post: security: - basicAuth: [] tags: - SAT summary: Push indoor humidity description: | Injects an indoor humidity reading (0-100 %). Used when humidity-based comfort adjustment is enabled. Body is a plain numeric string or JSON `{"value":"62.5"}`. operationId: setSatHumidity requestBody: required: true content: text/plain: schema: type: string example: "62.5" responses: '200': description: Humidity accepted content: application/json: schema: $ref: '#/components/schemas/StatusOk' '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/weather: get: tags: - SAT summary: Get weather integration status description: | Returns the current weather integration status as a JSON object, including the last received sun elevation and outdoor temperature from the weather source. operationId: getSatWeather responses: '200': description: Weather integration status content: application/json: schema: type: object description: Weather integration state (fields vary by implementation) '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/area/{index}: post: security: - basicAuth: [] tags: - SAT summary: Push area temperature for multi-area control description: | Injects a temperature reading for the specified area (0-3). When multi-area mode is enabled, SAT averages (or weights) the area temperatures as the room temperature input for the PID. Body is a plain numeric string or JSON `{"value":"21.3"}`. operationId: setSatAreaTemp parameters: - name: index in: path required: true schema: type: integer minimum: 0 maximum: 3 description: Area index (0-3) example: 0 requestBody: required: true content: text/plain: schema: type: string example: "21.3" responses: '200': description: Area temperature accepted content: application/json: schema: $ref: '#/components/schemas/StatusOk' '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/weather/needs-setup: get: tags: - SAT summary: Report whether the weather setup wizard should be shown description: | Returns whether the browser-side weather setup wizard should be triggered (TASK-511). Becomes `true` only after a 5-minute startup grace window AND when the canonical outside temperature (`OTcurrentSystemState.Toutside`, populated from OT MsgID 27) is still zero AND the SAT weather provider has not produced a valid reading. The `has_key` field surfaces whether an Open-Meteo / provider API key is already stored. operationId: getSatWeatherNeedsSetup responses: '200': description: Setup status retrieved successfully content: application/json: schema: type: object properties: needs_setup: type: boolean has_key: type: boolean example: needs_setup: false has_key: true '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/ble/discovery: get: tags: - SAT summary: BLE sensor roster (ESP32 only) description: | Streams the BLE roster as JSON (TASK-508). Each roster slot lists the discovered MAC, persistent label, last temperature/humidity, RSSI, and selection state. ESP32-only endpoint; the entire `/v2/sat/ble/*` family returns 404 from the SAT handler on ESP8266 builds. operationId: getSatBleDiscovery responses: '200': description: Roster retrieved successfully content: application/json: schema: type: object description: Roster payload as produced by `satBLERosterSendJSON()`. '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/ble/select: post: security: - basicAuth: [] tags: - SAT summary: Promote a roster MAC to the active BLE sensor (ESP32 only) description: | Promotes the named MAC from the roster to the active SAT BLE sensor slot (TASK-508). The MAC must already be present in the roster from a prior discovery cycle. operationId: setSatBleSelect requestBody: required: true content: application/json: schema: type: object required: [mac] properties: mac: type: string example: "AA:BB:CC:DD:EE:FF" responses: '200': description: Roster slot selected content: application/json: schema: $ref: '#/components/schemas/StatusOk' '400': $ref: '#/components/responses/BadRequestJson' '404': description: MAC not present in roster content: application/json: schema: $ref: '#/components/schemas/ApiError' '405': $ref: '#/components/responses/MethodNotAllowedJson' put: security: - basicAuth: [] tags: - SAT summary: Promote a roster MAC (PUT alias) operationId: setSatBleSelectPut requestBody: $ref: '#/paths/~1v2~1sat~1ble~1select/post/requestBody' responses: '200': description: Roster slot selected '400': $ref: '#/components/responses/BadRequestJson' '404': description: MAC not present in roster '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/ble/label: post: security: - basicAuth: [] tags: - SAT summary: Set the persistent label on a roster slot (ESP32 only) description: | Updates the user-visible label for the named MAC in the BLE roster (TASK-508). Label is persisted alongside the roster slot. operationId: setSatBleLabel requestBody: required: true content: application/json: schema: type: object required: [mac, label] properties: mac: type: string example: "AA:BB:CC:DD:EE:FF" label: type: string maxLength: 31 example: "Living room" responses: '200': description: Label updated content: application/json: schema: $ref: '#/components/schemas/StatusOk' '400': $ref: '#/components/responses/BadRequestJson' '404': description: MAC not present in roster content: application/json: schema: $ref: '#/components/schemas/ApiError' '405': $ref: '#/components/responses/MethodNotAllowedJson' put: security: - basicAuth: [] tags: - SAT summary: Set roster label (PUT alias) operationId: setSatBleLabelPut requestBody: $ref: '#/paths/~1v2~1sat~1ble~1label/post/requestBody' responses: '200': description: Label updated '400': $ref: '#/components/responses/BadRequestJson' '404': description: MAC not present in roster '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/ble/forget: post: security: - basicAuth: [] tags: - SAT summary: Drop a roster slot and its HA discovery (ESP32 only) description: | Removes the named MAC from the BLE roster and unpublishes the matching Home Assistant discovery config (TASK-508). Accepts POST, PUT, or DELETE; the body always carries the MAC. operationId: forgetSatBle requestBody: required: true content: application/json: schema: type: object required: [mac] properties: mac: type: string example: "AA:BB:CC:DD:EE:FF" responses: '200': description: Roster slot removed content: application/json: schema: $ref: '#/components/schemas/StatusOk' '400': $ref: '#/components/responses/BadRequestJson' '404': description: MAC not present in roster content: application/json: schema: $ref: '#/components/schemas/ApiError' '405': $ref: '#/components/responses/MethodNotAllowedJson' put: security: - basicAuth: [] tags: - SAT summary: Drop a roster slot (PUT alias) operationId: forgetSatBlePut requestBody: $ref: '#/paths/~1v2~1sat~1ble~1forget/post/requestBody' responses: '200': description: Roster slot removed '400': $ref: '#/components/responses/BadRequestJson' '404': description: MAC not present in roster '405': $ref: '#/components/responses/MethodNotAllowedJson' delete: tags: - SAT summary: Drop a roster slot (DELETE alias) operationId: forgetSatBleDelete requestBody: $ref: '#/paths/~1v2~1sat~1ble~1forget/post/requestBody' responses: '200': description: Roster slot removed '400': $ref: '#/components/responses/BadRequestJson' '404': description: MAC not present in roster '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/ble/bindkey: post: security: - basicAuth: [] tags: - SAT summary: Provision a roster slot's encrypted-MiBeacon bindkey (ESP32 only) description: | Sets the per-sensor encrypted-MiBeacon bindkey for the named MAC (TASK-930). If the MAC is not yet in the roster a slot is allocated for it (encrypted sensors cannot self-announce). An empty `key` clears the stored bindkey. **The bindkey is a write-only secret.** It is validated here, never logged, and never echoed back. The roster and discovery responses expose only a `has_bindkey` boolean, never the key itself. Gated on `HAS_SAT_BLE`; the whole `/v2/sat/ble/*` family returns 404 on builds without BLE. Validation: `key` must be empty (clears) or exactly 32 hex characters (else 400). operationId: setSatBleBindkey requestBody: required: true content: application/json: schema: type: object required: [mac, key] properties: mac: type: string example: "AA:BB:CC:DD:EE:FF" key: type: string pattern: '^([0-9A-Fa-f]{32})?$' description: Exactly 32 hex characters, or empty string to clear (write-only) responses: '200': description: Bindkey stored content: application/json: schema: $ref: '#/components/schemas/StatusOk' '400': $ref: '#/components/responses/BadRequestJson' '404': description: BLE not supported on this build content: application/json: schema: $ref: '#/components/schemas/ApiError' '405': $ref: '#/components/responses/MethodNotAllowedJson' '507': description: Roster full (no free slot to allocate for a new MAC) content: application/json: schema: $ref: '#/components/schemas/ApiError' put: security: - basicAuth: [] tags: - SAT summary: Provision a roster slot's bindkey (PUT alias) operationId: setSatBleBindkeyPut requestBody: $ref: '#/paths/~1v2~1sat~1ble~1bindkey/post/requestBody' responses: '200': description: Bindkey stored '400': $ref: '#/components/responses/BadRequestJson' '404': description: BLE not supported on this build '405': $ref: '#/components/responses/MethodNotAllowedJson' '507': description: Roster full (no free slot to allocate for a new MAC) /v2/sat/ble/roster: get: tags: - SAT summary: Get the SAT BLE roster (ESP32 only) description: | Returns the structured SAT BLE sensor roster (TASK-935/946): the 8 fixed slots plus the global name-prefix filter settings. Gated on `HAS_SAT_BLE`; returns 404 (`BLE not supported on this build`) on builds without BLE. **Bindkeys are write-only.** Each slot reports only `has_bindkey` (boolean) — the per-slot bindkey secret is NEVER emitted in the response. operationId: getSatBleRoster security: - basicAuth: [] responses: '200': description: Roster retrieved successfully content: application/json: schema: type: object properties: count: type: integer description: Number of populated roster slots name_prefix: type: string description: BLE advertised-name prefix filter name_filter_ingest: type: boolean description: Whether the name-prefix filter is applied on ingest slots: type: array description: Always 8 entries (slot indices 0-7) items: type: object properties: idx: type: integer minimum: 0 maximum: 7 mac: type: string description: Slot MAC (empty string when unset) label: type: string description: User-visible slot label has_bindkey: type: boolean description: True when a bindkey is stored for this slot (the secret itself is never returned) '404': description: BLE not supported on this build content: application/json: schema: $ref: '#/components/schemas/ApiError' '405': $ref: '#/components/responses/MethodNotAllowedJson' put: security: - basicAuth: [] tags: - SAT summary: Write a single BLE roster slot (ESP32 only) description: | Writes one roster slot (TASK-935/946). `idx` is required; at least one of `mac`, `label`, or `bindkey` must be provided. Parameters may be sent as query string or form body. The response never echoes user-supplied text; GET the roster to read values back. Gated on `HAS_SAT_BLE`; returns 404 on builds without BLE. Validation: - `idx` — required integer 0-7 (non-numeric → 400) - `mac` — empty (clears) or a 17-char colon-hex address `AA:BB:CC:DD:EE:FF` (else 400) - `label` — optional free text - `bindkey` — empty (clears) or exactly 32 hex characters (else 400); write-only operationId: setSatBleRosterSlot requestBody: required: true content: application/x-www-form-urlencoded: schema: type: object required: - idx properties: idx: type: integer minimum: 0 maximum: 7 mac: type: string pattern: '^(([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2})?$' description: 17-char colon-hex MAC, or empty string to clear label: type: string bindkey: type: string pattern: '^([0-9A-Fa-f]{32})?$' description: Exactly 32 hex characters, or empty string to clear (write-only) responses: '200': description: Slot written content: application/json: schema: type: object properties: status: type: string enum: [ok] idx: type: integer has_bindkey: type: boolean example: status: ok idx: 0 has_bindkey: true '400': $ref: '#/components/responses/BadRequestJson' '404': description: BLE not supported on this build content: application/json: schema: $ref: '#/components/schemas/ApiError' '405': $ref: '#/components/responses/MethodNotAllowedJson' post: security: - basicAuth: [] tags: - SAT summary: Write a single BLE roster slot (POST alias) operationId: setSatBleRosterSlotPost requestBody: $ref: '#/paths/~1v2~1sat~1ble~1roster/put/requestBody' responses: '200': description: Slot written '400': $ref: '#/components/responses/BadRequestJson' '404': description: BLE not supported on this build '405': $ref: '#/components/responses/MethodNotAllowedJson' delete: security: - basicAuth: [] tags: - SAT summary: Clear a single BLE roster slot (ESP32 only) description: | Clears the roster slot identified by `idx` (required integer 0-7; non-numeric → 400). Gated on `HAS_SAT_BLE`; returns 404 on builds without BLE. operationId: clearSatBleRosterSlot parameters: - name: idx in: query required: true schema: type: integer minimum: 0 maximum: 7 responses: '200': description: Slot cleared content: application/json: schema: type: object properties: status: type: string enum: [cleared] example: status: "cleared" '400': $ref: '#/components/responses/BadRequestJson' '404': description: BLE not supported on this build content: application/json: schema: $ref: '#/components/schemas/ApiError' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/markers: get: tags: - SAT summary: List heating-curve calibration markers description: | Returns the heating-curve calibration markers persisted to `/sat_markers.json` (TASK-586). Each marker is an object with `id`, `outside_temp`, `flow_temp`, `added` (unix epoch), and optional `label`. Returns an empty JSON array when the file does not yet exist. operationId: listSatMarkers responses: '200': description: Marker list retrieved successfully content: application/json: schema: type: array items: type: object properties: id: type: integer outside_temp: type: number flow_temp: type: number added: type: integer description: Unix epoch when the marker was created label: type: string '405': $ref: '#/components/responses/MethodNotAllowedJson' post: security: - basicAuth: [] tags: - SAT summary: Add a heating-curve calibration marker description: | Appends a marker to `/sat_markers.json` (TASK-586). The store keeps at most 20 markers; a 409 is returned when the limit is reached. The new `id` is `max(existing_id) + 1`. The `added` timestamp is the current epoch when the marker is written. operationId: addSatMarker requestBody: required: true content: application/json: schema: type: object required: [outside_temp, flow_temp] properties: outside_temp: type: number minimum: -15 maximum: 25 flow_temp: type: number minimum: 10 maximum: 90 label: type: string maxLength: 31 responses: '201': description: Marker created content: application/json: schema: type: object properties: status: type: string enum: [ok] id: type: integer example: status: ok id: 4 '400': $ref: '#/components/responses/BadRequestJson' '409': description: Marker store is full (20 markers maximum) content: application/json: schema: $ref: '#/components/schemas/ApiError' '405': $ref: '#/components/responses/MethodNotAllowedJson' '500': description: Cannot write `/sat_markers.json` to LittleFS content: application/json: schema: $ref: '#/components/schemas/ApiError' /v2/sat/markers/{id}: delete: tags: - SAT summary: Delete a heating-curve calibration marker description: | Removes the marker with the given numeric `id` from `/sat_markers.json` (TASK-586). operationId: deleteSatMarker parameters: - name: id in: path required: true schema: type: integer minimum: 1 responses: '200': description: Marker removed content: application/json: schema: $ref: '#/components/schemas/StatusOk' '400': $ref: '#/components/responses/BadRequestJson' '404': description: Marker id not found, or the markers file does not exist content: application/json: schema: $ref: '#/components/schemas/ApiError' '405': $ref: '#/components/responses/MethodNotAllowedJson' '500': description: Cannot write `/sat_markers.json` to LittleFS content: application/json: schema: $ref: '#/components/schemas/ApiError' /v2/sat/sensor-areas: get: tags: - SAT summary: Get Dallas-to-SAT-area mapping description: | Returns the persistent mapping of Dallas 1-Wire sensor addresses to the four SAT area slots (TASK-587). An empty `sensor` string means the slot is unmapped. Addresses are 16 uppercase hex characters. operationId: getSatSensorAreas responses: '200': description: Mapping retrieved successfully content: application/json: schema: type: object properties: areas: type: array items: type: object properties: index: type: integer minimum: 0 maximum: 3 sensor: type: string description: 16-hex-character Dallas address or empty string example: areas: - index: 0 sensor: "28FF64D1841703F1" - index: 1 sensor: "" - index: 2 sensor: "" - index: 3 sensor: "" '405': $ref: '#/components/responses/MethodNotAllowedJson' patch: tags: - SAT summary: Update one Dallas-to-SAT-area mapping description: | Updates one of the four mapping slots (TASK-587). The body specifies the slot `area` (0-3) and the Dallas sensor address. An empty string clears the slot. Addresses are validated as 16 hex characters and normalised to uppercase before persistence. Also accepts POST and PUT for browsers that cannot issue PATCH. operationId: setSatSensorArea requestBody: required: true content: application/json: schema: type: object required: [area] properties: area: type: integer minimum: 0 maximum: 3 sensor: type: string pattern: '^([0-9A-Fa-f]{16})?$' description: 16-hex-character Dallas address, or empty string to clear the slot responses: '200': description: Mapping updated content: application/json: schema: $ref: '#/components/schemas/StatusOk' '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' post: security: - basicAuth: [] tags: - SAT summary: Update one Dallas-to-SAT-area mapping (POST alias) operationId: setSatSensorAreaPost requestBody: $ref: '#/paths/~1v2~1sat~1sensor-areas/patch/requestBody' responses: '200': description: Mapping updated '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' put: security: - basicAuth: [] tags: - SAT summary: Update one Dallas-to-SAT-area mapping (PUT alias) operationId: setSatSensorAreaPut requestBody: $ref: '#/paths/~1v2~1sat~1sensor-areas/patch/requestBody' responses: '200': description: Mapping updated '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' /v2/sat/settings/{name}: post: security: - basicAuth: [] tags: - SAT summary: Update a SAT setting description: | Updates any named SAT setting. This mirrors all MQTT `sat/` command handlers. The setting name corresponds to the MQTT subtopic (e.g., `heating_curve`, `deadband`, `boiler_capacity`, `heating_system`, `overshoot_margin`, `manufacturer`, `simulation`, `auto_tune`, etc.). Special no-value commands (`reset_integral`, `ovp_start`, `ovp_stop`) are also accepted here without a body. See the [SAT MQTT topics reference](https://github.com/rvdbreemen/OTGW-firmware/blob/dev/backlog/docs/doc-1%20-%20sat-mqtt-topics.md) for the full list of setting names and valid payload formats. operationId: setSatSetting parameters: - name: name in: path required: true schema: type: string description: SAT setting name (e.g., `heating_curve`, `deadband`, `boiler_capacity`) example: "heating_curve" requestBody: required: false content: text/plain: schema: type: string example: "1.5" responses: '200': description: Setting updated content: application/json: schema: $ref: '#/components/schemas/StatusOk' '400': $ref: '#/components/responses/BadRequestJson' '405': $ref: '#/components/responses/MethodNotAllowedJson' components: securitySchemes: basicAuth: type: http scheme: basic description: | HTTP Basic Auth on protected/admin routes (ADR-056). Active ONLY when an HTTP password is configured; with no password set the device accepts the request without credentials (trusted-LAN model). Username is "admin". schemas: SensorStatus: type: object description: | Hardware sensor status. Returned by `GET /v2/sensors` and `GET /v2/sensors/status`. properties: sensors: type: object properties: dallas_enabled: type: boolean description: Whether Dallas 1-Wire temperature sensors are enabled in settings dallas_detected: type: boolean description: Whether at least one Dallas sensor was detected on the bus dallas_count: type: integer description: Number of detected Dallas sensors dallas_gpio: type: integer description: GPIO pin used for the 1-Wire bus dallas_poll_sec: type: integer description: Sensor polling interval in seconds simulated: type: boolean description: Whether sensor simulation mode is active (state.debug.bSensorSim) devices: type: object description: | Per-device readings. Only present when sensors are detected or simulation is active. Keys are 1-Wire addresses (16 hex characters); values are objects with `temp` (float, °C) and `epoch` (uint32, seconds since boot). additionalProperties: type: object properties: temp: type: number description: Temperature in °C epoch: type: integer description: Timestamp of last reading (seconds since boot) s0: type: object description: S0 pulse counter state properties: enabled: type: boolean gpio: type: integer description: GPIO pin for S0 input poll_sec: type: integer description: S0 reporting interval in seconds pulses: type: integer description: Pulse count in the current interval total: type: integer description: Total pulse count since boot power_kw: type: number description: Calculated power in kW epoch: type: integer description: Timestamp of last pulse (seconds since boot) StatusOk: type: object description: Standard success response for SAT write operations properties: status: type: string enum: [ok] example: status: ok SatStatus: type: object description: | SAT runtime status as a flat JSON object (no wrapper). Returned by `GET /v2/sat/status`. Contains 132 fields covering control mode, setpoints, PID state, temperature sensors, cycle diagnostics, pressure, power, thermal model, comfort, and BLE inputs. Field set may grow as the SAT engine evolves; consumers must ignore unknown keys. Add `?detail=full` for the compact health view (see `SatHealth`). properties: enabled: type: boolean description: "SAT control loop enabled" active: type: boolean description: "SAT actively driving the boiler setpoint" control_mode: type: integer description: "Control mode (0=off, 1=continuous, 2=PWM)" boiler_status: type: string description: "Boiler status label (off/idle/preheating/at_setpoint/modulating/etc.)" target_temp: type: number description: "Target room temperature (C)" room_temp: type: [number, "null"] description: "Room temperature used by the PID (C); null when no sensor" outside_temp: type: [number, "null"] description: "Outside temperature used by the heating curve (C); null when no OT-bus/external/weather source" heating_curve: type: number description: "Heating-curve flow temperature (C)" pid_output: type: number description: "PID output = curve + P + I + D (C)" final_setpoint: type: [number, "null"] description: "Final flow setpoint sent to the boiler (C); null when SAT is not active" error: type: number description: "PID error = target - room (C)" pid_p: type: number description: "Proportional PID term (C)" pid_i: type: number description: "Integral PID term (C)" pid_d: type: number description: "Derivative PID term (C)" kp: type: number description: "Active Kp gain" ki: type: number description: "Active Ki gain" kd: type: number description: "Active Kd gain" raw_derivative: type: number description: "Raw (unfiltered) derivative of the error" coefficient: type: number description: "Heating-curve coefficient (steepness)" deadband: type: number description: "PID deadband (C) around the target" overshoot_margin: type: number description: "Allowed overshoot above target before a cycle is classed as overshoot (C)" cycle_count: type: integer description: "Total boiler cycles counted" cycles_this_hour: type: integer description: "Boiler cycles in the trailing hour" last_cycle_class: type: integer description: "Last cycle classification code (0=none,1=good,2=overshoot,3=underheat,4=short,5=uncertain)" cycle_max_flow: type: number description: "Peak flow temperature in the current/last cycle (C)" cycle_overshoot_sec: type: integer description: "Seconds spent above target in the current/last cycle" duty_ratio: type: number description: "Flame-on duty ratio (0.0-1.0)" overshoot_fraction: type: number description: "Fraction of cycle time spent in overshoot (0.0-1.0)" underheat_fraction: type: number description: "Fraction of cycle time spent underheating (0.0-1.0)" cycle_phase: type: string description: "Current cycle phase label (idle/heating/cooling/etc.)" phase_duration_sec: type: integer description: "Seconds elapsed in the current cycle phase" pwm_duty: type: number description: "PWM duty cycle (0.0-1.0)" pwm_flame_req: type: boolean description: "PWM currently requesting flame on" active_preset: type: integer description: "Active preset index" mod_suppressed: type: boolean description: "Modulation suppression active" dhw_active: type: boolean description: "Domestic hot water draw active" dhw_setpoint: type: number description: "DHW setpoint (C)" dhw_config_tank: type: boolean description: "DHW configured as a storage tank" dhw_enable: type: boolean description: "DHW enabled" control_interval_sec: type: integer description: "Control loop interval (s)" fallback_active: type: boolean description: "Fallback control active (sensor/comms loss)" fallback_reason: type: integer description: "Fallback reason code" max_rel_modulation: type: integer description: "Maximum relative modulation allowed (%)" current_modulation: type: integer description: "Current relative modulation (%)" ovp_value: type: number description: "Overshoot-protection learned value (C)" ovp_enabled: type: boolean description: "Overshoot protection enabled" ovp_calib_phase: type: integer description: "OVP calibration phase" ovp_calib_max_temp: type: number description: "Max temperature seen during OVP calibration (C)" ovp_calib_samples: type: integer description: "OVP calibration sample count" heating_system: type: integer description: "Configured heating-system type code (0=auto)" heating_system_detected: type: integer description: "Detected heating-system type code" manufacturer: type: string description: "Boiler manufacturer label" manufacturer_setting: type: integer description: "Configured manufacturer code (0=auto)" manufacturer_detected: type: integer description: "Detected manufacturer (slave member) ID" slave_memberid: type: integer description: "OpenTherm slave member ID" max_setpoint_system: type: number description: "System maximum flow setpoint (C)" external_temp_valid: type: boolean description: "External room temperature input valid" external_outdoor_valid: type: boolean description: "External outdoor temperature input valid" pv_surplus_w: type: integer description: "Reported PV surplus (W)" pv_surplus_valid: type: boolean description: "PV surplus reading valid" pv_boost_active: type: boolean description: "PV boost currently applied" pv_boost_applied_c: type: number description: "PV boost added to setpoint (C)" pv_boost_enabled: type: boolean description: "PV boost feature enabled" safety_tripped: type: boolean description: "Safety shutdown active" valves_open: type: boolean description: "Zone valves reported open" window_open: type: boolean description: "Open-window detected" window_detection: type: boolean description: "Open-window detection enabled" push_setpoint: type: boolean description: "Setpoint pushed to boiler this cycle" flame_off_offset: type: number description: "Flame-off setpoint offset (C)" force_pwm: type: boolean description: "Force PWM mode regardless of conditions" flow_offset: type: number description: "Flow-temperature offset applied (C)" pressure: type: number description: "System water pressure (bar)" pressure_drop_rate: type: number description: "Pressure drop rate (bar/min)" pressure_alarm: type: boolean description: "Low/high pressure alarm active" modulation_reliable: type: boolean description: "Modulation feedback considered reliable" setpoint_mismatch: type: boolean description: "Boiler flow setpoint differs from commanded" curve_recommendation: type: string description: "Heating-curve tuning recommendation (insufficient/raise/lower/ok)" heating_curve_recommendation: type: string description: "Heating-curve recommendation (duplicate of curve_recommendation)" mean_error: type: number description: "Mean PID error over the sample window (C)" error_stddev: type: number description: "PID error standard deviation (C)" target_temp_step: type: number description: "Target temperature adjustment step (C)" power_kw: type: number description: "Estimated boiler power output (kW)" energy_kwh: type: number description: "Estimated cumulative energy (kWh)" boiler_capacity: type: number description: "Configured boiler capacity (kW)" boiler_rated_kw: type: number description: "Rated boiler power (kW)" boiler_efficiency: type: number description: "Assumed boiler efficiency (0.0-1.0)" energy_estimated_kwh: type: number description: "Model-estimated energy (kWh)" preset_sync: type: boolean description: "Preset synchronised with thermostat" thermal_coeff: type: number description: "Thermal model coefficient" thermal_drop_rate: type: number description: "Modelled room temperature drop rate (C/min)" thermal_model_valid: type: boolean description: "Thermal model has enough data to be valid" estimated_room: type: number description: "Model-estimated room temperature (C)" last_known_room: type: number description: "Last known room temperature (C)" solar_gain_active: type: boolean description: "Solar-gain compensation active" indoor_rise_rate: type: number description: "Observed indoor temperature rise rate (C/min)" summer_simmer: type: boolean description: "Summer simmer mode active" summer_active: type: boolean description: "Summer mode active" summer_hours_above: type: number description: "Hours outdoor temp stayed above the summer threshold" summer_threshold: type: number description: "Outdoor temperature threshold for summer mode (C)" summer_min_hours: type: integer description: "Minimum hours above threshold to enter summer mode" comfort_adjust: type: boolean description: "Humidity comfort adjustment active" humidity: type: integer description: "Relative humidity (%)" humidity_valid: type: boolean description: "Humidity reading valid" comfort_offset: type: number description: "Applied comfort setpoint offset (C)" comfort_ref_humidity: type: integer description: "Reference humidity for comfort adjustment (%)" comfort_max_offset: type: number description: "Maximum comfort offset (C)" simulation: type: boolean description: "Simulation mode active" sim_available: type: boolean description: "Simulation mode available on this build" auto_tune: type: boolean description: "Auto-tune enabled" auto_tune_active: type: boolean description: "Auto-tune currently running" auto_tune_cycles: type: integer description: "Auto-tune cycles completed" auto_tune_score: type: number description: "Auto-tune fit score" auto_tune_rate: type: number description: "Auto-tune learning rate" sensor_max_age: type: integer description: "Maximum sensor age before stale (s)" error_monitoring: type: boolean description: "Error monitoring enabled" auto_gains_value: type: number description: "Auto-gains aggressiveness value" auto_gains: type: boolean description: "Automatic PID gain scheduling enabled" kp_manual: type: number description: "Manual Kp gain (used when auto_gains off)" ki_manual: type: number description: "Manual Ki gain" kd_manual: type: number description: "Manual Kd gain" thermal_comfort: type: boolean description: "Thermal-comfort mode active" heating_mode: type: string description: "Heating mode label (comfort/eco/etc.)" cycles_per_hour: type: integer description: "Target maximum boiler cycles per hour" valve_offset: type: number description: "Valve compensation offset (C)" solar_freeze_integral: type: boolean description: "Freeze the PID integral during solar gain" multi_area: type: boolean description: "Multi-area control enabled" multi_area_count: type: integer description: "Number of configured areas" ble_enable: type: boolean description: "BLE thermostat input enabled" ble_failover: type: boolean description: "BLE failover enabled" ble_failover_active: type: boolean description: "BLE failover currently active" ble_temp_valid: type: boolean description: "BLE temperature reading valid" ble_sensor_count: type: integer description: "Number of BLE sensors seen" example: enabled: false active: false control_mode: 0 boiler_status: "off" target_temp: 20.0 room_temp: null outside_temp: null heating_curve: 0.0 pid_output: 0.0 final_setpoint: null error: 0.0 pid_p: 0.0 pid_i: 0.0 pid_d: 0.0 kp: 0.0 ki: 0.0 kd: 0.0 raw_derivative: 0.0 coefficient: 1.5 deadband: 0.1 overshoot_margin: 2.0 cycle_count: 0 cycles_this_hour: 0 last_cycle_class: 0 cycle_max_flow: 0.0 cycle_overshoot_sec: 0 duty_ratio: 0.0 overshoot_fraction: 0.0 underheat_fraction: 0.0 cycle_phase: "idle" phase_duration_sec: 0 pwm_duty: 0.0 pwm_flame_req: false active_preset: 0 mod_suppressed: false dhw_active: false dhw_setpoint: 0.0 dhw_config_tank: false dhw_enable: true control_interval_sec: 30 fallback_active: false fallback_reason: 0 max_rel_modulation: 100 current_modulation: 100 ovp_value: 0.0 ovp_enabled: false ovp_calib_phase: 0 ovp_calib_max_temp: 0.0 ovp_calib_samples: 0 heating_system: 0 heating_system_detected: 1 manufacturer: "Other" manufacturer_setting: 0 manufacturer_detected: 18 slave_memberid: 0 max_setpoint_system: 62.0 external_temp_valid: false external_outdoor_valid: false pv_surplus_w: 0 pv_surplus_valid: false pv_boost_active: false pv_boost_applied_c: 0.0 pv_boost_enabled: false safety_tripped: false valves_open: true window_open: false window_detection: false push_setpoint: false flame_off_offset: 0.0 force_pwm: false flow_offset: 2.0 pressure: 0.0 pressure_drop_rate: 0.0 pressure_alarm: false modulation_reliable: true setpoint_mismatch: false curve_recommendation: "insufficient" heating_curve_recommendation: "insufficient" mean_error: 0.0 error_stddev: 0.0 target_temp_step: 0.5 power_kw: 0.0 energy_kwh: 0.0 boiler_capacity: 24.0 boiler_rated_kw: 0.0 boiler_efficiency: 0.92 energy_estimated_kwh: 0.0 preset_sync: false thermal_coeff: 0.05 thermal_drop_rate: 0.0 thermal_model_valid: false estimated_room: 0.0 last_known_room: 0.0 solar_gain_active: false indoor_rise_rate: 0.0 summer_simmer: false summer_active: false summer_hours_above: 0.0 summer_threshold: 18.0 summer_min_hours: 6 comfort_adjust: false humidity: 0 humidity_valid: false comfort_offset: 0.0 comfort_ref_humidity: 50 comfort_max_offset: 1.0 simulation: false sim_available: true auto_tune: false auto_tune_active: false auto_tune_cycles: 0 auto_tune_score: 0.0 auto_tune_rate: 0.02 sensor_max_age: 21600 error_monitoring: false auto_gains_value: 2.0 auto_gains: true kp_manual: 5.0 ki_manual: 0.0005 kd_manual: 0.0 thermal_comfort: false heating_mode: "comfort" cycles_per_hour: 3 valve_offset: 0.0 solar_freeze_integral: true multi_area: false multi_area_count: 0 ble_enable: false ble_failover: true ble_failover_active: false ble_temp_valid: false ble_sensor_count: 0 SatHealth: type: object description: | Compact SAT health view returned by `GET /v2/sat/status?detail=full`. This is a SEPARATE 22-field object, not a superset of `SatStatus`: it focuses on cycle/flame/device health, sync flags, error statistics, pressure trend, and auto-tune progress. Consumers must ignore unknown keys. properties: sync_setpoint: type: boolean description: "Boiler flow setpoint in sync with the commanded value" sync_modulation: type: boolean description: "Boiler modulation in sync with the commanded value" sync_ch: type: boolean description: "CH-enable state in sync with the commanded value" pressure_smoothed: type: number description: "Smoothed system water pressure (bar)" pressure_drop_rate: type: number description: "Pressure drop rate (bar/min)" pressure_alarm: type: boolean description: "Low/high pressure alarm active" cycle_kind: type: string description: "Last cycle kind label (unknown/ch/dhw/mixed)" cycle_duration: type: integer description: "Last cycle duration (s)" cycle_count: type: integer description: "Total boiler cycles counted" cycle_fraction_ch: type: number description: "Fraction of the last cycle spent on CH (0.0-1.0)" cycle_fraction_dhw: type: number description: "Fraction of the last cycle spent on DHW (0.0-1.0)" error_mean: type: number description: "Mean PID error over the sample window (C)" error_stddev: type: number description: "PID error standard deviation (C)" error_samples: type: integer description: "Number of PID error samples collected" heating_curve_recommendation: type: string description: "Heating-curve tuning recommendation (insufficient/raise/lower/ok)" flame_health: type: boolean description: "Flame behaviour considered healthy" device_health: type: boolean description: "Overall device health OK" cycle_health: type: boolean description: "Cycling behaviour considered healthy" cycle_class: type: string description: "Last cycle classification (none/good/overshoot/underheat/short/uncertain)" auto_tune_score: type: number description: "Auto-tune fit score" auto_tune_cycles: type: integer description: "Auto-tune cycles completed" ovp_phase: type: integer description: "Overshoot-protection calibration phase" example: sync_setpoint: false sync_modulation: false sync_ch: false pressure_smoothed: 0.0 pressure_drop_rate: 0.0 pressure_alarm: false cycle_kind: "unknown" cycle_duration: 0 cycle_count: 0 cycle_fraction_ch: 0.0 cycle_fraction_dhw: 0.0 error_mean: 0.0 error_stddev: 0.0 error_samples: 0 heating_curve_recommendation: "insufficient" flame_health: true device_health: false cycle_health: true cycle_class: "none" auto_tune_score: 0.0 auto_tune_cycles: 0 ovp_phase: 0 ApiError: type: object description: Standard API error response (ADR-035) properties: error: type: object properties: status: type: integer description: HTTP status code message: type: string description: Human-readable error message required: - status - message example: error: status: 400 message: "Invalid message ID" OTDirectStatus: type: object description: | OTGW32 OT-direct runtime status object. Returned by `/v2/otdirect/status`, `/v2/otdirect/mode` (POST), and `/v2/otdirect/settings` (POST/PUT). All fields are read-only runtime state. properties: otdirect_status: type: object properties: mode: type: string enum: [gateway, monitor, bypass, master, loopback] description: | Current operating mode of the OT-direct engine. - `gateway` (default) - full gateway with scheduler, thermostat forwarding, and overrides - `monitor` - transparent pass-through; all frames forwarded unmodified - `bypass` - thermostat direct to boiler via hardware relay; OT-direct inactive (requires bypass relay) - `master` - OTGW32 is the sole OT master; scheduler only, no thermostat expected - `loopback` - internal test mode with simulated boiler responses; no hardware required bypass: type: boolean description: True when the bypass relay is active (thermostat direct to boiler) stepup: type: boolean description: True when the 24V step-up converter is on monitor_mode: type: boolean description: True when the engine is in transparent pass-through mode (equivalent to `mode == "monitor"`) master_mode: type: boolean description: True when OTGW32 is the sole OT master (equivalent to `mode == "master"`) thermostat_connected: type: boolean description: True when a thermostat frame has been received within the configured setback timeout setback_active: type: boolean description: True when thermostat is considered disconnected and the setback temperature override is engaged schedule_total: type: integer description: Total number of OpenTherm Message IDs in the polling schedule schedule_active: type: integer description: Number of schedule entries currently active (not disabled by the boiler) schedule_disabled: type: integer description: Number of schedule entries disabled because the boiler returned UNKNOWN_DATA_ID three times overrides_active: type: integer description: Number of active write-override slots (CS=/TT=/SH=/SW= etc.) ot_online: type: boolean description: True when the OT serial bus is alive (frames recently received) thermostat: type: boolean description: True when the thermostat OT bus side is active (from `state.otBus.bThermostatState`) boiler: type: boolean description: True when the boiler OT bus side is active (from `state.otBus.bBoilerState`) SimulationStatus: type: object properties: simulation: type: object properties: active: type: boolean description: Whether simulation mode is currently active file: type: string description: Path to the simulation log file interval_ms: type: integer description: Replay interval in milliseconds example: simulation: active: false file: "/otgw_simulation.log" interval_ms: 1000 responses: RateLimited: description: | Poll rate limit exceeded (ADR-172). This endpoint carries a per-endpoint poll budget. The budget is **global, not per client**: capping the aggregate load an ESP32 gateway accepts is the point, so a well-behaved client can still be paced because another client is polling. Sustained rate is one request per window with a burst of 2. Distinct from `503`: a `503` means the device as a whole is in trouble (too many concurrent requests, or a heap floor), while `429` means this caller exceeded a quota and the service is otherwise healthy. `Retry-After` is authoritative, but it is **not a CORS-safelisted response header**, so cross-origin callers cannot read it. The same value is repeated as the `retry_after` member of the RFC 9457 body for that case. headers: Retry-After: description: Seconds to wait before retrying (RFC 9110 §10.2.3). Rounded up, never 0. schema: type: integer example: 2 Cache-Control: description: Always `no-store`, so an intermediary cannot serve a stale rate-limit error. schema: type: string example: "no-store" RateLimit: description: | Remaining quota and reset hint. IETF `httpapi` Internet-Draft, not a standard as of July 2026 — sent as extra signal only. schema: type: string example: '"poll";r=0;t=2' RateLimit-Policy: description: Burst size and window for this endpoint's budget (same draft caveat). schema: type: string example: '"poll";q=2;w=2' content: application/problem+json: schema: type: object properties: type: type: string format: uri example: "https://github.com/rvdbreemen/OTGW-firmware/problems/rate-limit-exceeded" title: type: string example: "Rate limit exceeded" status: type: integer example: 429 retry_after: type: integer description: Seconds to wait. Mirrors the `Retry-After` header for cross-origin callers. example: 2 detail: type: string example: "This endpoint serves at most 1 request per 2 second(s) (burst 2). Retry after 2 second(s)." BadRequestJson: description: Bad request - invalid parameters content: application/json: schema: $ref: '#/components/schemas/ApiError' example: error: status: 400 message: "Invalid or missing message ID" MethodNotAllowedJson: description: | HTTP method not allowed. Includes `Allow` header listing valid methods per RFC 7231 §6.5.5. headers: Allow: description: Comma-separated list of valid HTTP methods for this endpoint schema: type: string example: "GET" content: application/json: schema: $ref: '#/components/schemas/ApiError' example: error: status: 405 message: "Method not allowed" NotFound: description: API endpoint not found content: application/json: schema: $ref: '#/components/schemas/ApiError' example: error: status: 404 message: "Endpoint not found" URITooLong: description: Request URI exceeds maximum length content: application/json: schema: $ref: '#/components/schemas/ApiError' example: error: status: 414 message: "URI too long" InternalServerError: description: Internal server error (typically low heap memory) content: application/json: schema: $ref: '#/components/schemas/ApiError' example: error: status: 500 message: "Internal server error (low heap)" PicUnavailable: description: | No PIC is present, so PIC gateway functions are disabled. Returned by all `/v2/pic/*` routes on hardware without a PIC (e.g. the OTGW32 / OT-Direct board, which talks OpenTherm directly and has no PIC co-processor). content: application/json: schema: $ref: '#/components/schemas/ApiError' example: error: status: 503 message: "No PIC detected - PIC functions disabled" x-api-design-notes: | **API Version**: v2 only. v0 and v1 have been removed and return 410 Gone. **Memory Constraints**: - The ESP32-S3 2.0.0 line has ~300 KB heap (ADR-128 dropped ESP8266) - API enforces a minimum free-heap floor before processing requests **Boolean Values**: - Since the ArduinoJson v7 migration (ADR-141, TASK-867) all REST endpoints emit proper JSON booleans (`true`/`false`). The former quoted-string booleans on `/v2/health` (`"true"`/`"false"`) are gone. **HTTP Methods**: - Every mutation endpoint documented with `post:` also accepts `PUT` as a transparent alias (identical body, identical effect). The firmware dispatch gates on `method == HTTP_POST || method == HTTP_PUT`, so a `put:` block is not duplicated per path; treat POST and PUT as interchangeable on all mutations. - The central auth gate (HTTP Basic) applies to both POST and PUT on every `/v2/*` route; DELETE is not centrally gated. **Error Handling**: - All errors return structured JSON: {"error":{"status":N,"message":"..."}} - All 405 responses include an `Allow` header listing valid methods (RFC 7231 §6.5.5) - Low heap conditions return 500 to prevent crashes **CORS Support**: - All responses include `Access-Control-Allow-Origin: *` - All endpoints respond to OPTIONS preflight with 204 No Content and CORS headers - CORS preflight response includes `Access-Control-Max-Age: 86400` (24h cache) **Polling Recommendations**: - Health endpoint: Poll every 30-60 seconds (each call writes a probe file to LittleFS) - Flash status: Poll every 1-2 seconds during upgrades - OpenTherm data: Poll every 5-10 seconds - PIC settings: On-demand only (triggers PR= readout cycle) **Home Assistant Integration**: - Use /v2/otgw/discovery to register entities - MQTT is the primary integration method - REST API provides fallback and manual control