openapi: 3.1.0 info: title: PVMonitor SGW Firmware API description: > REST API exposed by the ESP8266-based SMA inverter gateway (SGW). The device reads an SMA inverter via SMANet (UART, 1200 baud) and forwards the data over Wi-Fi to the pvmonitor backend server. In addition, it offers the local HTTP interface described here. All responses carry the header `Access-Control-Allow-Origin: *`. None of the endpoints require authentication. version: "1.0" servers: - url: http://{device-ip} description: Device IP address in station mode (assigned by DHCP) variables: device-ip: default: 192.168.x.x - url: http://192.168.4.1 description: > Device in onboarding access-point mode. The open access point `PVMONITOR_SGW` (no password, max. 1 client) is started when the button is held for ~5 s within the first 20 s after boot. It stays open for 15 minutes. tags: - name: System description: Device information and control - name: Inverter description: SMA inverter data (channels and measurements) - name: Configuration description: Device credentials and Wi-Fi settings - name: OTA description: Over-the-air firmware update - name: Logging description: Real-time log stream paths: /: get: summary: Serve embedded web UI description: > Returns the built-in `index.html` page that is compiled into the firmware. The response body is deflate-compressed. tags: [System] responses: "200": description: HTML page (deflate-compressed) headers: Content-Encoding: schema: type: string example: deflate content: text/html: schema: type: string format: binary /sysinfo: get: summary: Get device system information description: > Returns version, chip, flash, heap, connected inverter, and Wi-Fi details as a JSON object. tags: [System] responses: "200": description: System information content: application/json: schema: $ref: "#/components/schemas/SysInfo" example: version: "4.0.0.1" idf: "v3.4-dirty" name: "SGW_aa-bb-cc-dd-ee-ff" chip: "ESP8266 rev 1" flash: "4 MB external" heap: free: 32768 watermark: 28000 inverter: "SMA WR21TL06; 2000463498" wifi: ssid: "MyNetwork" bssid: "aa-bb-cc-dd-ee-ff" rssi: -65 security: 3 /reset: get: summary: Trigger device restart description: > Schedules a software reset (via `esp_restart`) to execute ~500 ms after the response is sent. tags: [System] responses: "200": description: Reset scheduled content: text/plain: schema: type: string example: "OK, Reset will take place soon..." /channels: get: summary: List all SMA inverter channels description: > Returns a JSON array of every channel known to the firmware: first all *parameter* channels, then all *instantaneous* channels. Each object carries the channel metadata, the type-specific fields (see `Channel`) and the most recently read value. The response uses chunked transfer encoding. If at least one channel exists, a trailing sentinel object `{"Name":"Dummy",…}` is appended to work around the trailing-comma limitation in the serialiser. Before the inverter has been read, the response is an empty array `[]`. tags: [Inverter] responses: "200": description: Array of channel descriptors headers: Transfer-Encoding: schema: type: string example: chunked content: application/json: schema: type: array items: oneOf: - $ref: "#/components/schemas/Channel" - $ref: "#/components/schemas/DummyChannel" example: - Name: "Pac" Group: "Instantanious" Type: "Analog" Format: "dword[0]" Value: 2450 Unit: "W" Gain: 1 Offset: 0 - Name: "Status" Group: "Instantanious" Type: "Status" Format: "dword[0]" Value: 1 Options: ["Stop", "Mpp", "Offset"] - Name: "Dummy" Group: "Dummy" Type: "Dummy" Format: "Dummy" Unit: "" /measurement: get: summary: Get current inverter measurements description: > Returns a flat JSON object whose keys are the names of all instantaneous channels and whose values are the last-read measurements. Values of *Status* channels are given as their decoded text; all other values are numbers. Before the inverter has been read, the response is an empty object `{}`. tags: [Inverter] responses: "200": description: Key/value map of current measurements content: application/json: schema: $ref: "#/components/schemas/Measurement" example: Pac: 2450 Uac: 230.1 Iac: 10.65 E-Total: 12345.6 Status: "Mpp" /log-stream: get: summary: Subscribe to live log stream (SSE) description: > Opens a Server-Sent Events (SSE) connection. The firmware forwards ESP-IDF log lines to this stream in real time. Only one concurrent client is supported; a new connection replaces the previous one. Log lines are only forwarded if the firmware was built with `APP_REDIRECT_LOG` defined (see `main/CMakeLists.txt`). Otherwise the connection is accepted, but no events are sent. tags: [Logging] responses: "200": description: SSE event stream content: text/event-stream: schema: type: string description: > One `data:` event per log line (no `event:` or `id:` fields) "500": description: Event stream could not be set up /credentials: put: summary: Update device credentials description: > Applies a JSON Merge Patch (RFC 7396) to the stored credentials object and persists the result to NVS flash. Only the provided fields are updated; omitted fields are left unchanged, fields set to `null` are removed. An empty body is accepted as a no-op merge. Changed Wi-Fi settings take effect after the next restart. Tokens are only used by the firmware if they start with `e` (as every JWT does). | Field | Description | |-------------|--------------------------------------------------------------| | `ssid` | Wi-Fi network name | | `password` | Wi-Fi password | | `pvm_token` | Bearer token for pvmonitor backend API calls (JWT) | | `ota_token` | Bearer token that enables OTA firmware polling (JWT) | tags: [Configuration] requestBody: description: Partial or full credentials object (JSON Merge Patch) required: false content: application/json: schema: $ref: "#/components/schemas/Credentials" examples: wifi_only: summary: Update Wi-Fi credentials only value: ssid: "MyNetwork" password: "secret" full: summary: Set all credentials value: ssid: "MyNetwork" password: "secret" pvm_token: "eyJhbGciOi..." ota_token: "eyJhbGciOi..." remove_token: summary: Remove the OTA token value: ota_token: null responses: "200": description: Credentials updated and persisted "400": description: Body is not empty and not a valid JSON object delete: summary: Erase all stored credentials description: > Deletes the entire credentials entry from NVS flash. After a subsequent restart the device falls back to the compiled-in default Wi-Fi SSID and password, and no tokens are set. tags: [Configuration] responses: "200": description: Credentials erased /firmware: options: summary: CORS preflight for firmware upload description: > Allows browsers to send `PUT /firmware` from a different origin. tags: [OTA] responses: "200": description: Preflight accepted headers: Access-Control-Allow-Methods: schema: type: string example: "PUT, OPTIONS" Access-Control-Allow-Headers: schema: type: string example: "Content-Type" put: summary: Upload new firmware (OTA) description: > Accepts a raw firmware binary via chunked transfer and writes it to the inactive OTA partition, which is then set as boot partition. The device does **not** restart automatically; call `GET /reset` to boot into the new firmware. The request must use `Transfer-Encoding: chunked`. tags: [OTA] requestBody: description: Raw firmware binary (ESP8266 app image) required: true content: application/octet-stream: schema: type: string format: binary responses: "200": description: Firmware written and boot partition set content: text/plain: schema: type: string example: "OK, firmware installed successfully." "500": description: OTA could not be started (`esp_ota_begin` failed) "501": description: Writing the firmware failed (`esp_ota_write` failed) "503": description: > Setting the boot partition failed (`esp_ota_set_boot_partition` failed, e.g. because the image is invalid) components: schemas: SysInfo: type: object properties: version: type: string description: Application firmware version (`...`) idf: type: string description: ESP8266_RTOS_SDK (IDF) version string name: type: string description: Device name derived from the station MAC address (`SGW_xx-xx-xx-xx-xx-xx`) chip: type: string description: Chip model and revision (e.g. "ESP8266 rev 1") flash: type: string description: Flash size and type (e.g. "4 MB external") heap: type: object properties: free: type: integer description: Current free heap bytes watermark: type: integer description: Minimum free heap bytes ever observed inverter: type: string description: > Connected SMA inverter as `SMA ; `. Before the inverter has been detected: `SMA ; 0`. wifi: type: object properties: ssid: type: string bssid: type: string description: Access-point MAC address (lower-case hex, dash-separated) rssi: type: integer description: Signal strength in dBm security: type: integer description: Authentication mode (maps to `wifi_auth_mode_t`) Channel: type: object description: > SMANet channel descriptor. Which fields follow `Value` depends on `Type` (and for Analog/Counter also on `Group`): | Type | Group = Parameter | other groups | |---------|------------------------|----------------------| | Analog | `Unit`, `LoVal`, `HiVal` | `Unit`, `Gain`, `Offset` | | Counter | `Unit`, `LoHiVal` | `Unit`, `Gain` | | Digital | `Text` | `Text` | | Status | `Options` | `Options` | required: [Name, Group, Type, Format, Value] properties: Name: type: string maxLength: 16 description: SMA channel name (e.g. "Pac", "E-Total") Group: type: string enum: [Parameter, Instantanious, Archive, Test, ""] description: Channel group Type: type: string enum: [Analog, Digital, Counter, Status, ""] description: Channel type Format: type: string description: > Data format as `[]`, where `` is one of `byte`, `word`, `dword`, `float4`, `float8`, `array8`…`array15` (e.g. "dword[0]") Value: type: number description: > Last known value. Analog/Counter: decimal number (max. 3 decimal places); Digital/Status: integer (for Status the index into `Options`) Unit: type: string maxLength: 8 description: Physical unit (Analog/Counter only), e.g. "W", "V", "kWh" Gain: type: number description: Gain factor (Analog/Counter, non-parameter channels) Offset: type: number description: Offset (Analog, non-parameter channels) LoVal: type: number description: Lower limit (Analog parameter channels) HiVal: type: number description: Upper limit (Analog parameter channels) LoHiVal: type: number description: Limit value (Counter parameter channels) Text: type: array description: Texts for the low and high state (Digital only) items: type: string minItems: 2 maxItems: 2 Options: type: array description: Texts of the possible states (Status only) items: type: string DummyChannel: type: object description: Trailing sentinel object of the `/channels` array properties: Name: const: Dummy Group: const: Dummy Type: const: Dummy Format: const: Dummy Unit: const: "" Measurement: type: object description: > Flat map of instantaneous channel name → current value. Keys depend on what channels the connected inverter exposes. Status channels have a string value (decoded state text), all others a numeric value. additionalProperties: oneOf: - type: number - type: string Credentials: type: object properties: ssid: type: ["string", "null"] description: Wi-Fi SSID password: type: ["string", "null"] description: Wi-Fi password pvm_token: type: ["string", "null"] description: JWT bearer token for pvmonitor backend API ota_token: type: ["string", "null"] description: JWT bearer token that enables OTA firmware polling