openapi: 3.1.0
info:
  title: AWTRIX NG HTTP API
  version: '1.0'
  description: "The AWTRIX NG device API. JSON operations use camelCase keys. Multipart\nuploads, raw\
    \ script source and the plain-text version route state their formats\non the operation.\n\n**Conventions**\n\
    - Colors are `\"#RRGGBB\"` strings on settings and display output. App config\n  descriptors return\
    \ colors as integers `0`-`16777215`. Input additionally accepts\n  `\"RRGGBB\"`, `[r,g,b]` and `[\"\
    HSV\",h,s,v]` (h 0-360, s/v 0-100).\n  Fields documented as *nullable* use `null` for \"inherit\"\
    /\"off\". Every\n  concrete color including pure black (`#000000`) and pure white\n  (`#FFFFFF`) is\
    \ a settable value distinct from `null`.\n- Durations are integer **milliseconds** with an `...Ms`\
    \ key suffix.\n- Names are matched **case-insensitively**: `effect`, `overlay`, `palette`,\n  `transitionEffect`\
    \ and the string enums (`transitionDirection`, `timeSeparatorMode`,\n  `dateOrder`, `dateSeparator`,\
    \ `dateYearMode`) accept any casing, so\n  `\"matrix\"`, `\"Matrix\"` and `\"MATRIX\"` are the same\
    \ name.\n  `GET /api/v1/capabilities` lists the spelling the API returns.\n- Send JSON request bodies\
    \ with `Content-Type: application/json`. The server\n  rejects a supplied non-JSON type on `PUT` and\
    \ `PATCH` with\n  `415 unsupportedMediaType`. Omitting the header bypasses this check.\n  `POST` is\
    \ not gated by this check. Script source and script-update\n  writes also bypass it, though script-update\
    \ still requires a JSON body.\n  Multipart routes use the format documented on the operation.\n- Clients\
    \ that cannot send `PATCH`, `PUT` or `DELETE` may send a `POST`\n  carrying `X-HTTP-Method-Override:\
    \ PATCH|PUT|DELETE`. The request is then\n  handled exactly as that method. The header is rejected\
    \ with\n  `400 invalidMethodOverride` on any other carrier method, for any other\n  value, and for\
    \ `PUT /api/v1/apps/script/{name}`.\n- On the writing `PUT` routes (`.../apps/pushed/{name}`,\n  `.../display/moodlight`,\
    \ `.../indicators/{id}`) an empty or `{}` body is\n  rejected with `422`: a JSON body is required.\
    \ Removing/turning off is\n  done only via the explicit `DELETE` route (or an empty MQTT payload).\n\
    - Errors always come back in one shape (see `Error`) with the proper status code:\n  400 malformed\
    \ JSON, 401 auth, 403 disabled in setup mode,\n  404 unknown resource/name, 405 wrong method, 415\
    \ wrong content type,\n  413 body too large, 422 validation failure (with `field`),\n  507 storage\
    \ full, too little free memory for the body, or an applied change not saved.\n  Any route answers\
    \ `400 invalidMethodOverride` to a bad override header and\n  `405 methodNotAllowed` to a method it\
    \ does not take.\n- A path that is no route answers `404 notFound` with `unknown route`.\n- A CORS\
    \ preflight `OPTIONS` on any path answers `204` with no body.\n- Optional HTTP Basic Auth is off by\
    \ default. Setting `authEnabled` enables\n  it for every route, including setup mode. Configure\n\
    \  `authUser` and `authPass` before enabling it.\n\nThe MQTT surface mirrors this API with identical\
    \ bodies. See\nhttps://blueforcer.github.io/awtrix-ng/reference/mqtt/.\n"
servers:
- url: http://{device}
  variables:
    device:
      default: awtrixng-a1b2c3.local
security:
- {}
- basicAuth: []
tags:
- name: device
- name: settings
- name: display
- name: apps
- name: notifications
- name: indicators
- name: sounds
- name: radio
- name: scripts
- name: system
- name: files
paths:
  /api/v1/device:
    get:
      tags:
      - device
      summary: Device state & statistics
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: The device state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceState'
  /api/v1/version:
    get:
      tags:
      - device
      summary: Firmware version
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: The firmware version.
          content:
            application/json:
              schema:
                type: object
                properties:
                  version:
                    type: string
  /api/v1/device/reboot:
    post:
      tags:
      - device
      summary: Reboot the device
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
  /api/v1/device/factory-reset:
    post:
      tags:
      - device
      summary: Wipe filesystem, WiFi credentials and settings, then reboot
      description: Not available over MQTT. Only from the device's own web page or a client that is not
        a browser.
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenOwnPage'
        '200':
          $ref: '#/components/responses/Ok'
  /update:
    post:
      tags:
      - system
      summary: Install the update file for this device
      description: 'Multipart upload of the file named by device.updateImage, in the field `firmware`.
        The file is checked before anything is installed, so a refused or interrupted upload leaves the
        device as it was. After the answer the device installs the file and restarts.


        Only from the device''s own web page or a client that is not a browser. This route uses multipart/form-data
        rather than the JSON body format.

        '
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                firmware:
                  type: string
                  format: binary
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: Accepted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  applying:
                    type: boolean
                    description: 'true: the clock installs the package (up to 8388608 bytes) and restarts.
                      The new release must keep running for a minute. A release that fails to start three
                      times leaves the clock in USB recovery. Poll device.update after reconnecting to
                      learn the result.

                      '
        '403':
          description: forbidden in setup mode, or forbiddenOrigin for a rejected browser origin.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '400':
          description: badRequest (no file, more than one file, or an incomplete upload), or a file this
            device does not take.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: notNewer, insufficientStorage or updateBusy.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: 'payloadTooLarge or insufficientMemory: package exceeds the size or available memory
            limit.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/restore:
    post:
      tags:
      - system
      summary: Restore a backup archive (multipart ZIP)
      description: 'Multipart upload of a backup `.zip` produced by the web UI. The archive

        is a store-only (uncompressed) ZIP. The device streams it, so its size

        is not bounded by the JSON body cap. Entry names decide what is applied:


        | entry | effect |

        |---|---|

        | `manifest.json` | validated first. Must name app `awtrix-ng` |

        | `config/wifi.json` | `{wifiSsid, wifiPass}` merged into the config |

        | `config/system.json` | rest of the device config (validated like PUT /system) |

        | `config/settings.json` | display/behavior settings (applied live + persisted) |

        | `ICONS/*`, `MELODIES/*`, `PALETTES/*`, `MP3/*`, `SCRIPTS/*` | stored on the device |

        | `SCRIPTS/<name>/<sound>.mp3` | a script''s own sound, checked like any MP3. Nothing else under
        `SCRIPTS/` may sit in a folder |

        | `apploop.json` | app rotation order |


        Content is checked per folder exactly like `POST /api/v1/files`. A

        traversal path, a foreign app or a bad-CRC entry is skipped with a

        warning rather than trusted. Restore also works in setup mode, so a new

        or reset clock gets its Wi-Fi and settings back from the backup alone.

        Reboot afterwards to apply restored system settings.

        '
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '507':
          description: insufficientStorage, restored, not saved yet. Free storage and repeat before rebooting.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/ForbiddenOwnPage'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: Applied. Per-category counts and any skip warnings.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  applied:
                    type: object
                    properties:
                      wifi:
                        type: integer
                      system:
                        type: integer
                      settings:
                        type: integer
                      appLoop:
                        type: integer
                      radioStations:
                        type: integer
                      icons:
                        type: integer
                      iconOrigins:
                        type: integer
                      melodies:
                        type: integer
                      palettes:
                        type: integer
                      mp3:
                        type: integer
                        description: MP3 files, the scripts' own sounds included
                      scripts:
                        type: integer
                      skipped:
                        type: integer
                  warnings:
                    type: array
                    items:
                      type: string
        '400':
          description: 'The archive was rejected outright (not a zip, no manifest, or a manifest for a
            different app): `ok` is false and `error` explains. An upload without a file gets the normal
            error body instead: `badRequest`, `no file received`.

            '
          content:
            application/json:
              schema:
                oneOf:
                - type: object
                  required:
                  - ok
                  - error
                  properties:
                    ok:
                      type: boolean
                      enum:
                      - false
                    error:
                      type: string
                    applied:
                      type: object
                    warnings:
                      type: array
                      items:
                        type: string
                - $ref: '#/components/schemas/Error'
  /api/v1/settings:
    get:
      tags:
      - settings
      summary: Read all settings
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: All settings.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Settings'
    patch:
      tags:
      - settings
      summary: Update settings (any subset, all or nothing)
      description: 'Validates every field first. On error nothing is applied and a 422

        names the offending field. Returns the full resulting settings.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Settings'
      responses:
        '415':
          $ref: '#/components/responses/WrongContentType'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: The complete resulting settings
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Settings'
        '400':
          $ref: '#/components/responses/BadJson'
        '422':
          $ref: '#/components/responses/Invalid'
  /api/v1/settings/reset:
    post:
      tags:
      - settings
      summary: Reset all settings to defaults and reboot
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
  /api/v1/display:
    get:
      tags:
      - display
      summary: Display runtime state
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: The display state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Display'
    patch:
      tags:
      - display
      summary: Set display power and/or the global overlay
      description: 'Applies completely or not at all: a rejected field leaves the other

        fields unchanged.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                power:
                  type: boolean
                overlay:
                  type:
                  - string
                  - 'null'
                  description: 'Weather overlay name, matched case-insensitively against

                    `capabilities.overlays`. Unknown names are rejected with 422.

                    `null` (or `""`) clears it, and clearing also resets

                    `overlaySettings`.

                    '
                overlaySettings:
                  $ref: '#/components/schemas/EffectSettings'
      responses:
        '400':
          $ref: '#/components/responses/BadJson'
        '415':
          $ref: '#/components/responses/WrongContentType'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
        '422':
          $ref: '#/components/responses/Invalid'
  /api/v1/display/moodlight:
    put:
      tags:
      - display
      summary: Enable the mood light (a JSON body is required)
      description: '**A JSON body is required.** An empty body or exactly `{}` is rejected

        with `422`. Use `DELETE` (or an empty MQTT payload) to turn the mood

        light off. A request sent with a non-JSON `Content-Type` is rejected

        with `415`.


        `kelvin` **wins** over `color`: when `kelvin` is present,

        `color` is ignored entirely. An unparseable `color` is rejected with

        `422` (`field: "color"`) and nothing is stored.


        **Both `color` and `brightness` are sticky.** A payload that omits

        either one keeps the value it had, so `{"brightness":30}` dims without

        touching the color and `{"color":"#FF0000"}` recolors without touching

        the level. Until a color has ever been set the mood light is white, and

        the starting brightness is `120`.


        `brightness` is 0-255. A larger value is not rejected: it wraps around,

        so 300 becomes 44.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              minProperties: 1
              properties:
                color:
                  $ref: '#/components/schemas/Color'
                  description: Ignored when `kelvin` is present.
                kelvin:
                  type: integer
                  minimum: 1000
                  maximum: 40000
                  description: Limited to 1000-40000. Wins over `color` when both are sent.
                brightness:
                  type: integer
                  minimum: 0
                  maximum: 255
                  description: '0-255. A larger value wraps around (300 becomes 44). Kept when absent.
                    Starts at 120.

                    '
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
        '415':
          $ref: '#/components/responses/WrongContentType'
        '422':
          $ref: '#/components/responses/Invalid'
    delete:
      tags:
      - display
      summary: Disable the mood light
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
  /api/v1/display/screen:
    get:
      tags:
      - display
      summary: Current screen content
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: The picture on the panel.
          content:
            application/json:
              schema:
                type: object
                properties:
                  width:
                    type: integer
                  height:
                    type: integer
                  pixels:
                    type: array
                    items:
                      type: integer
                      description: packed 0xRRGGBB
  /api/v1/apps:
    get:
      tags:
      - apps
      summary: Full app inventory (the arranged apps in order, then the rest)
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: All apps.
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    name:
                      type: string
                    enabled:
                      type: boolean
                      description: 'Whether the app runs at all. Independent of `inLoop`, which says whether
                        it is drawn, and of `present`, which says whether it is there right now. All three
                        agree for every app except a headless script, which runs without ever taking a
                        turn, and a pushed app between two pushes, which is on and keeps its place with
                        nothing to draw.

                        '
                    inLoop:
                      type: boolean
                    present:
                      type: boolean
                      description: 'Whether the app is on the device right now. False for a name the device
                        holds a place for while the app itself is away.

                        '
                    slot:
                      type:
                      - integer
                      - 'null'
                      description: '0-based place in the order the user arranged. Kept while the app is
                        away. Null when the app has no place of its own.

                        '
                    origin:
                      type:
                      - string
                      - 'null'
                      enum:
                      - builtin
                      - pushed
                      - script
                      - module
                      - null
                      description: 'Where the app''s content comes from. `builtin` is firmware code. `pushed`
                        is a JSON spec sent from outside and held in RAM only. `script` is Berry source
                        stored on the device. `module` is a stored Berry file that other scripts import:
                        it is listed here because it shares the file collection, but it never draws, so
                        it carries no `enabled`, `inLoop` or `slot`.

                        '
                    import:
                      type: string
                      description: 'Modules only. The name scripts write in their `import` line: the file
                        name unless the header''s `@module` gives another.

                        '
                    icon:
                      type: string
                      description: 'Pushed apps only, and only when the spec set a non-empty icon. Omitted
                        otherwise.

                        '
                    skipped:
                      type: boolean
                      description: 'Script apps only. True when the app''s own `should_show()` last answered
                        false, so the rotation walks past it. Independent of `inLoop`: the app is still
                        in the rotation and can still be switched to by name. Reports the last answer
                        given, not a fresh one.

                        '
                    headless:
                      type: boolean
                      description: 'Script apps only. True when the script carries `@headless true` and
                        so never draws: it has no place in the rotation, and `draw()`, `should_show()`
                        and `duration()` are never called.

                        '
                    ondemand:
                      type: boolean
                      description: 'Script apps only. True when the script carries `@ondemand`: it is
                        not in the rotation and runs only after it was started from the device menu or
                        with `PUT /api/v1/apps/active`. `inLoop` is true while it runs.

                        '
                    config:
                      type: boolean
                      description: 'True when the app has settings a client can change: a built-in under
                        `/api/v1/apps/builtin/{name}/config`, a script or module, whose file declares
                        `@config` lines, under `/api/v1/apps/{name}/config`. A module''s values are shared
                        by every app that imports it. Omitted for pushed apps.

                        '
                    error:
                      description: 'Scripts and modules only. `null` while the script works, otherwise
                        the error it stopped on. A script app shows it as `ERR:<name>`. Omitted for other
                        origins and on a build with no scripting platform.

                        '
                      oneOf:
                      - type: 'null'
                      - $ref: '#/components/schemas/ScriptError'
                    meta:
                      $ref: '#/components/schemas/ScriptMeta'
  /api/v1/apps/active:
    put:
      tags:
      - apps
      summary: Switch to an app
      description: 'Naming an `@ondemand` script starts it: it is loaded fresh and has the

        display to itself until another app is switched to, `next` or

        `previous` is called, or select is held on the device. It fails with

        507 or 503 when there is not enough memory to start it.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - name
              properties:
                name:
                  type: string
                fast:
                  type: boolean
                  description: true = jump instantly, false = animated transition
      responses:
        '415':
          $ref: '#/components/responses/WrongContentType'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
        '404':
          $ref: '#/components/responses/NotFound'
        '503':
          $ref: '#/components/responses/Unavailable'
        '507':
          $ref: '#/components/responses/InsufficientStorage'
  /api/v1/apps/next:
    post:
      tags:
      - apps
      summary: Next app
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
  /api/v1/apps/previous:
    post:
      tags:
      - apps
      summary: Previous app
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
  /api/v1/apps/order:
    put:
      tags:
      - apps
      summary: Set which apps are on, and the order of the ones that draw
      description: '`order` is what runs, in the order it draws. `disabled` is the complete

        list of switched-off apps: every app it does not name is switched on,

        also one that was off before. To switch one app and leave the others as

        they are, use `PUT /api/v1/apps/{name}/enabled`.


        `disabled` is always required. `order` is optional and requires `disabled`

        beside it. `{"disabled":[...]}` alone sets the switched-off apps and

        keeps the order. A body without `disabled`, or one that is not an

        object, is 400.


        A switched-off script runs nothing at all. A headless script never

        draws, so naming it in `order` keeps it running without giving it a

        place in the rotation. A name keeps its place even when no such app

        exists yet, and a name in `disabled` stays off while the app is absent.

        The same app may appear several times in `order` to show it more than

        once per cycle (each entry gets its own place).

        A 507 means the change is active but not saved. Free storage and repeat

        the request before rebooting.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - disabled
              properties:
                order:
                  type: array
                  items:
                    type: string
                disabled:
                  type: array
                  items:
                    type: string
      responses:
        '415':
          $ref: '#/components/responses/WrongContentType'
        '507':
          description: insufficientStorage, applied, not saved yet
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
        '400':
          $ref: '#/components/responses/BadJson'
  /api/v1/apps/{name}/enabled:
    put:
      tags:
      - apps
      summary: Switch one app on or off
      description: '`true` switches the app on, `false` switches it off. Every other app

        stays as it is. A switched-off app keeps its place in the order and

        returns to it when switched on again. The name may belong to an app

        that is not there yet: when it arrives, it is on or off as set. The

        switch is kept after a restart. An app that already is on or off

        answers 200 too.

        A 507 means the change is active but not saved. Free storage and repeat

        the request before rebooting.

        '
      parameters:
      - name: name
        in: path
        required: true
        schema:
          type: string
          pattern: ^[A-Za-z0-9_-]{1,32}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: boolean
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
        '400':
          description: '`name` is not `[A-Za-z0-9_-]{1,32}` (`invalidName`)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '415':
          $ref: '#/components/responses/WrongContentType'
        '422':
          description: The body is not `true` or `false` (`validationFailed`, `must be true or false`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '507':
          description: insufficientStorage, applied, not saved yet
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/apps/pushed/{name}:
    put:
      tags:
      - apps
      summary: Create or update a pushed app (a JSON body is required)
      description: 'Payload fields follow the AWTRIX pushed-app schema (text, icon, icons,

        textColor, draw, effect, ...). An array payload creates indexed apps

        `{name}0..n`. Non-object elements are skipped. Time fields are

        milliseconds: `durationMs`, `lifetimeMs`, `textBlinkMs`, `textFadeMs`.


        **A JSON body is required.** An empty body or exactly `{}` is rejected

        with `422`. Use the `DELETE` route (or an empty MQTT payload) to remove

        an app and its indexed children. A request sent with a non-JSON

        `Content-Type` is rejected with `415`.


        `effect` and `overlay` names are checked against the lists

        returned by `GET /api/v1/capabilities` (case-insensitively). An

        unknown name is rejected with `422 validationFailed` and

        `field: "effect"` / `"overlay"`, and NOTHING is stored: an array

        payload is all-or-nothing here too.


        50 pushed apps may be resident at once. Creating a new app beyond that

        cap is rejected with `507 insufficientStorage`. An array payload is

        all-or-nothing: if the whole set will not fit, the entire request is

        rejected and nothing is stored.


        A pushed app is not saved on the device: it lasts until it is

        replaced, deleted, expired by `lifetimeMs`, or the device restarts.

        Content that must come back by itself after a reboot belongs in a

        script (`PUT /api/v1/apps/script/{name}`).


        Removal is not here: it is `DELETE /api/v1/apps/{name}`, which

        removes any kind of app.

        '
      parameters:
      - name: name
        in: path
        required: true
        schema:
          type: string
          pattern: ^[A-Za-z0-9_-]{1,32}$
        description: 'Checked after the method and before the body. A name outside the pattern is rejected
          with `400 invalidName` and `field: "name"`.

          '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
              - type: object
                minProperties: 1
                description: One pushed app stored under {name}.
                allOf:
                - properties:
                    layout:
                      $ref: '#/components/schemas/NativeLayout'
                      description: 'A prepared layout made of regions instead of the classic visual fields.
                        It cannot mix with them. Duration, lifetime and repeat stay in the outer object.

                        '
                properties:
                  icons:
                    $ref: '#/components/schemas/PlacedIcons'
                  iconGap:
                    $ref: '#/components/schemas/IconGap'
                  scroll:
                    $ref: '#/components/schemas/Scroll'
                    description: 'Overrides the device''s `scroll` setting field by field. An omitted
                      field keeps the configured default.

                      '
                  textOffsetX:
                    type: integer
                    description: 'X shift applied after positioning, and part of every scroll anchor,
                      so it lengthens each cycle by the same amount in every mode. X axis only.

                      '
              - type: array
                items:
                  type: object
                  properties:
                    icons:
                      $ref: '#/components/schemas/PlacedIcons'
                    iconGap:
                      $ref: '#/components/schemas/IconGap'
                description: Indexed apps {name}0, {name}1, ...
      responses:
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
        '400':
          description: 'Body is not valid JSON (`invalidJson`), or `{name}` is not `[A-Za-z0-9_-]{1,32}`
            (`invalidName`, `field: "name"`).

            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '415':
          $ref: '#/components/responses/WrongContentType'
        '422':
          $ref: '#/components/responses/Invalid'
        '507':
          $ref: '#/components/responses/InsufficientStorage'
  /api/v1/apps/{name}:
    delete:
      tags:
      - apps
      summary: Delete an app, whatever kind it is
      description: 'Works for any kind of app, and calling it twice is safe. For a pushed app this erases
        the exact

        name plus the indexed children an array payload sent to `{name}`

        created (`{name}0`, `{name}1`, ...). It is the array push that ties

        them together, not the spelling: an app pushed to `{name}1` directly is

        its own app and is left alone. For a script it erases the source and its

        persisted store: the only way to reset that store. Its own sounds in

        `/SCRIPTS/<name>/` stay until `DELETE /api/v1/apps/script/{name}/sounds`.

        A script installed again under the name uses them. A

        built-in or an unknown name is also `200`: nothing is removed and

        nothing is an error.

        '
      parameters:
      - name: name
        in: path
        required: true
        schema:
          type: string
          pattern: ^[A-Za-z0-9_-]{1,32}$
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
        '400':
          description: '`name` is not `[A-Za-z0-9_-]{1,32}` (`invalidName`)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/v1/notifications:
    post:
      tags:
      - notifications
      summary: Show a notification
      description: 'Accepts all pushed-app fields plus `name`, `hold`, `stack`, `wakeup` and

        `sound`. `sound` is the sound of POST /api/v1/audio/play: a stored

        name, an object with one source key, or a list of 1 to 4 of them. It plays as an alert when the
        notification appears. With

        `loop: true` it repeats while the notification is shown and stops when it

        leaves. `""` and `null` mean no sound. An invalid `sound` refuses the

        whole notification with 422 and `field` under `sound` (for example

        `sound.rtttl`, `sound[1].file`). A name that is not stored, or a sound

        the clock cannot play, is not an error: the notification shows without

        sound. `durationMs` defaults to the global app time.


        `effect` and `overlay` names are checked against the lists

        returned by `GET /api/v1/capabilities` (case-insensitively). An

        unknown name is rejected with `422 validationFailed` and

        `field: "effect"` / `"overlay"`, and the notification is not queued.


        Up to 32 notifications can be stacked. A `stack` request beyond that cap

        is rejected with `507 insufficientStorage`.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                icons:
                  $ref: '#/components/schemas/PlacedIcons'
                iconGap:
                  $ref: '#/components/schemas/IconGap'
                sound:
                  description: A sound as on POST /api/v1/audio/play. `""` or null means no sound.
                  oneOf:
                  - $ref: '#/components/schemas/AudioPlay'
                  - type: 'null'
      responses:
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
        '400':
          $ref: '#/components/responses/BadJson'
        '422':
          $ref: '#/components/responses/Invalid'
        '507':
          $ref: '#/components/responses/InsufficientStorage'
  /api/v1/notifications/active:
    delete:
      tags:
      - notifications
      summary: Dismiss the current notification
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
  /api/v1/notifications/{name}:
    delete:
      tags:
      - notifications
      summary: Dismiss the notification pushed under this name
      description: 'Removes the notification whose payload carried this `name`, wherever it

        sits in the queue: it need not be the one on screen, and removing a

        waiting one leaves the current one running.


        Lets several senders share a device without dismissing each other''s

        messages. The unnamed dismiss always removes the one on screen. The name

        identifies, it does not protect: anyone who can reach the API can

        dismiss a name they know. Use HTTP auth to restrict callers.


        `active` is reserved for the route above, so a notification cannot be

        addressed under that name.

        '
      parameters:
      - name: name
        in: path
        required: true
        schema:
          type: string
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
        '404':
          description: No queued notification carries that name.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/indicators/{id}:
    put:
      tags:
      - indicators
      summary: 'Set a corner indicator (1..3): a JSON body is required'
      description: 'Corner indicators are shown on the right edge of the panel: id 1

        top, id 2 middle, id 3 bottom. `blinkMs` gives a 50% on/off blink and

        `fadeMs` a breathing fade. The state is also echoed in

        `GET /api/v1/device` and published over MQTT/Home Assistant (three HA

        lights).


        **A JSON body is required.** An empty body or exactly `{}` is rejected

        with `422`. Use the `DELETE` route (or an empty MQTT payload) to turn it

        off. A request sent with a non-JSON `Content-Type` is rejected with

        `415`.


        Only `color` changes the on/off state: a `color` of 0 (black) or `null`

        turns the indicator off and keeps the stored color. Any other color

        turns it on. A payload without `color` leaves both untouched.


        `blinkMs` and `fadeMs` are 0 when absent, so a request without them

        gives a steady light.

        '
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
          minimum: 1
          maximum: 3
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              minProperties: 1
              properties:
                color:
                  $ref: '#/components/schemas/Color'
                  description: A color of 0 or null turns the indicator off and keeps the stored color.
                blinkMs:
                  type: integer
                  minimum: 0
                  maximum: 65535
                  description: 0 when absent. 0-65535. A value outside that range is stored as 0.
                fadeMs:
                  type: integer
                  minimum: 0
                  maximum: 65535
                  description: 0 when absent. 0-65535. A value outside that range is stored as 0.
      responses:
        '400':
          $ref: '#/components/responses/BadJson'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
        '404':
          $ref: '#/components/responses/NotFound'
        '415':
          $ref: '#/components/responses/WrongContentType'
        '422':
          $ref: '#/components/responses/Invalid'
    delete:
      tags:
      - indicators
      summary: Turn a corner indicator off
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
          minimum: 1
          maximum: 3
      responses:
        '404':
          description: '`notFound`, `id must be 1..3`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
  /api/v1/audio/melodies:
    get:
      tags:
      - sounds
      summary: Every melody on the device
      description: 'One request for the whole editor. `notes` and `durationMs` come from

        parsing `rtttl`. A file that does not parse is listed with

        `valid: false` plus `error` and `index` rather than hidden, so you can

        repair it in the editor.

        '
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: The melody list
          content:
            application/json:
              schema:
                type: object
                properties:
                  melodies:
                    type: array
                    items:
                      type: object
                      properties:
                        name:
                          type: string
                        rtttl:
                          type: string
                          description: The stored melody, verbatim
                        bytes:
                          type: integer
                        notes:
                          type: integer
                        durationMs:
                          type: integer
                        valid:
                          type: boolean
                        error:
                          type: string
                        index:
                          type: integer
                  usedBytes:
                    type: integer
                  totalBytes:
                    type: integer
  /api/v1/audio/melodies/{name}:
    parameters:
    - name: name
      in: path
      required: true
      schema:
        type: string
        pattern: ^[A-Za-z0-9_-]{1,24}$
    put:
      tags:
      - sounds
      summary: Save a melody
      description: 'Stores `/MELODIES/{name}.txt`. The RTTTL title is normalised to `{name}`

        - a two-part `defaults:notes` string gets the name put in front, a

        three-part one has its title replaced: so a file cannot come to claim a

        name other than the one it is filed under.


        An unparseable melody is rejected with `422`. The message carries the

        reason and the byte offset. An MP3 and a melody never share a name: when

        `/MP3/{name}.mp3` exists the answer is `409 nameTaken`, `name taken`.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - rtttl
              properties:
                rtttl:
                  type: string
                  maxLength: 512
      responses:
        '400':
          description: '`invalidJson`, `invalid JSON` with `field: "rtttl"`: the body is not valid JSON.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '415':
          $ref: '#/components/responses/WrongContentType'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '201':
          $ref: '#/components/responses/Ok'
        '200':
          $ref: '#/components/responses/Ok'
        '409':
          description: '`nameTaken`, `name taken`: an MP3 of that name exists'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          $ref: '#/components/responses/Invalid'
        '507':
          $ref: '#/components/responses/InsufficientStorage'
    delete:
      tags:
      - sounds
      summary: Delete a melody
      description: 'There is no rename route: PUT the new name, then DELETE the old one.

        '
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/v1/audio:
    get:
      tags:
      - radio
      summary: What each group plays, and the station list
      description: What the radio, the app and the alert group play, and the station list. The same document
        is published, retained, on `<P>/state/audio` on every change of any group.
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: Status and stations
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AudioState'
  /api/v1/audio/play:
    post:
      tags:
      - sounds
      - radio
      summary: Play a sound
      description: 'The body is a sound: a stored name (`"ding"`, short for

        `{"file":"ding"}`), an object with exactly one source key plus `loop`,

        or a list of 1 to 4 strings or objects. A list plays its first entry

        the clock can play: it has the hardware, and for `file` the file is

        stored. The source keys are in `SoundObject`.


        Everything sent here plays as an alert, at `volume` × `alertVolume`:

        it replaces an alert that is playing. `loop: true` repeats it until

        POST /api/v1/audio/stop stops the `alert` group or everything, or a

        new alert replaces it.


        The body is validated before stored files and audio hardware are checked.

        See [Audio playback errors](https://blueforcer.github.io/awtrix-ng/reference/errors/#audio-playback)

        for all statuses, fields and messages. Inside a list, `field` starts with

        the entry position, for example `[1].rtttl`.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AudioPlay'
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
        '400':
          $ref: '#/components/responses/BadJson'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Invalid'
        '503':
          $ref: '#/components/responses/Unavailable'
  /api/v1/audio/stop:
    post:
      tags:
      - sounds
      - radio
      summary: Stop whatever is playing
      description: 'With no body or `{}` this stops everything. `group` stops one group:

        `alert` the alert that is playing, `app` every sound of the scripts,

        `radio` the radio. Another value answers 422 `must be alert, app or radio`.

        '
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AudioStop'
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
        '400':
          $ref: '#/components/responses/BadJson'
        '422':
          $ref: '#/components/responses/Invalid'
  /api/v1/audio/clip:
    post:
      tags:
      - sounds
      summary: Play a recording once
      description: 'The body is the audio file itself, with any Content-Type, up to 2 MiB:

        WAV with 16-bit PCM, mono or stereo, at 16000, 22050, 24000, 32000,

        44100 or 48000 Hz, or MP3 (MPEG-1, 2 or 2.5 layer III, an ID3 tag in

        front is fine). The clip is not stored. A body larger than the device

        accepts can answer 413 before routing.


        A clip is an alert, like a sound from POST /api/v1/audio/play. It plays

        at `volume` × `alertVolume` and replaces an alert that is playing, and a

        radio station pauses and comes back afterwards. POST

        /api/v1/audio/stop stops it.


        An empty body answers 422 `body required`, a file that

        is neither answers 422 `not WAV or MP3`, a WAV in another format 422

        `unsupported WAV`, and while the speaker cannot play the answer is 503

        `speaker unavailable`.

        '
      requestBody:
        required: true
        content:
          audio/wav:
            schema:
              type: string
              format: binary
          audio/mpeg:
            schema:
              type: string
              format: binary
          application/octet-stream:
            schema:
              type: string
              format: binary
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
        '404':
          $ref: '#/components/responses/NotFound'
        '413':
          description: payloadTooLarge
        '422':
          $ref: '#/components/responses/Invalid'
        '503':
          $ref: '#/components/responses/Unavailable'
  /api/v1/audio/stations:
    put:
      tags:
      - radio
      summary: Replace the station list
      description: 'The whole list at once. There is no route for a single station. The

        clock keeps the list in alphabetical order by name.


        Validation is all-or-nothing, and a rejected list leaves the stored one

        untouched. The error names the offending row as `stations[N].field`.

        A 507 means the change is active but not saved. Free storage and repeat

        the request before rebooting.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
              - $ref: '#/components/schemas/RadioStations'
              - type: array
                maxItems: 32
                items:
                  $ref: '#/components/schemas/RadioStation'
      responses:
        '415':
          $ref: '#/components/responses/WrongContentType'
        '507':
          description: insufficientStorage, applied, not saved yet
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
        '400':
          $ref: '#/components/responses/BadJson'
        '422':
          $ref: '#/components/responses/Invalid'
    get:
      tags:
      - radio
      summary: Read the stored station list
      responses:
        '200':
          description: Stored stations.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RadioStations'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/v1/apps/script-update/{name}:
    put:
      tags:
      - scripts
      summary: Update a script only if its source has not changed
      description: 'Requires `scriptUpdates` in capabilities. The check and the install

        happen in one step, so no other change can come in between. Use

        `expected_source: null` to create a separate copy only if the name does

        not exist. An existing script keeps settings that still fit. If the new

        source fails to compile or to set up, the old script is restored. Errors

        that happen later while it runs are not reported here. The whole JSON

        body must fit in the memory the device has free for script sources.

        '
      parameters:
      - name: name
        in: path
        required: true
        schema:
          type: string
          pattern: ^[A-Za-z0-9_-]{1,32}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - expected_source
              - source
              properties:
                expected_source:
                  type:
                  - string
                  - 'null'
                  description: Exact current source, or null to create only.
                source:
                  type: string
                  minLength: 1
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '400':
          $ref: '#/components/responses/BadJson'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Source changed or the copy name exists (`scriptChanged`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          $ref: '#/components/responses/Invalid'
        '503':
          description: Scripting unavailable or temporarily busy.
        '507':
          description: Insufficient script capacity.
  /api/v1/apps/script/{name}:
    get:
      tags:
      - scripts
      summary: One script's raw Berry source
      description: 'Answers the script''s source as `text/plain`, byte for byte as it was

        installed, so an editor can round-trip it straight back into `PUT`.


        There is no separate script inventory: `GET /api/v1/apps` lists every

        app with an `origin`, and a script entry carries its `error` state and

        its `@name`/`@desc`/`@author`/`@version`/`@icons` header in `meta`. `error` is

        `null` while the script works. Otherwise it is the error the matrix is

        showing as `ERR:<name>`. The script is tried again when you send a new

        version, save its settings or data, or restart the device. If it fails

        again, the new error is shown. The listing does not include the source.

        '
      parameters:
      - name: name
        in: path
        required: true
        schema:
          type: string
          pattern: ^[A-Za-z0-9_-]{1,32}$
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: The raw source
          content:
            text/plain:
              schema:
                type: string
        '400':
          description: '`name` is not `[A-Za-z0-9_-]{1,32}` (`invalidName`)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
        '503':
          description: This build has no scripting platform (`unavailable`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    put:
      tags:
      - scripts
      summary: Install or replace a script
      description: 'The body is the raw Berry source, NOT JSON. `{name}` becomes the app id

        in the rotation as well as the filename under `/SCRIPTS`, so it is

        limited to `[A-Za-z0-9_-]{1,32}`.


        Any `Content-Type` is accepted, or none. For example, send the source

        with `Content-Type: text/plain` and `curl --data-binary`.


        **A script that does not compile still installs.** The source is stored,

        the app joins the rotation, and it shows `ERR:<name>`. The reply is

        `200` with `error` carrying the compiler message. `error` is `null` when

        the script works.


        Replacing an installed script restarts it: its subscriptions, running

        requests and values held in memory are dropped. Its saved store is

        kept. The script keeps its position in the rotation.


        A new name is rejected with `507` when the script memory limit is

        reached, and nothing is stored. Replacing an existing one always works. Modules

        share the same limit. The name of a built-in app, such as `Time`, is

        rejected with `422` and nothing is stored.


        A source whose header carries `@module` installs as a module instead of

        an app: it is stored and listed the same way, but it never draws and is

        reached by other scripts with `import`. Its import name is the file name

        unless `@module <name>` gives another. A name that is not an identifier,

        is already taken, or belongs to a built-in module is rejected with `422`

        and nothing is stored.


        Removal is `DELETE /api/v1/apps/{name}`, the route for any kind of app.

        It erases the source and the saved store, but not the script''s sounds.

        Replacing the source keeps the sounds too.

        '
      parameters:
      - name: name
        in: path
        required: true
        schema:
          type: string
          pattern: ^[A-Za-z0-9_-]{1,32}$
      requestBody:
        required: true
        content:
          text/plain:
            schema:
              type: string
              description: Berry source.
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: Installed (check `error` for the compile result)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  name:
                    type: string
                  error:
                    description: '`null` when the script works, otherwise the error it stopped on.'
                    oneOf:
                    - type: 'null'
                    - $ref: '#/components/schemas/ScriptError'
        '400':
          description: '`name` is not `[A-Za-z0-9_-]{1,32}` (`invalidName`)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'
        '415':
          $ref: '#/components/responses/WrongContentType'
        '422':
          $ref: '#/components/responses/Invalid'
        '500':
          description: This build has no scripting platform (`internalError`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '507':
          $ref: '#/components/responses/InsufficientStorage'
  /api/v1/apps/script/{name}/sounds:
    parameters:
    - name: name
      in: path
      required: true
      description: The script's install name.
      schema:
        type: string
        pattern: ^[A-Za-z0-9_-]{1,32}$
    get:
      tags:
      - scripts
      - sounds
      summary: A script's own sounds, with the SHA-256 of each
      description: 'A script''s MP3s live in `/SCRIPTS/<name>/<sound>.mp3`, next to its

        source. The running script plays them by name: `sound.play("boost")`

        plays `boost.mp3` from its own folder, and `/MP3/boost.mp3` when the

        folder has none. From outside, `{"file":"<name>/boost"}` on POST

        /api/v1/audio/play plays it. A script''s sounds may share a name with a

        melody or an MP3 in /MP3. Replacing the script''s source keeps them, and so does

        `DELETE /api/v1/apps/{name}`: sounds left from a deleted script stay

        listable here until they are deleted.


        `sha256` is worked out from the stored bytes, so an installer can tell

        which sounds it still has to send. A script without sounds answers

        `files: []`.

        '
      responses:
        '200':
          description: The script's sounds and whole-filesystem usage.
          content:
            application/json:
              schema:
                type: object
                properties:
                  files:
                    type: array
                    items:
                      type: object
                      properties:
                        name:
                          type: string
                          description: File name with the .mp3
                        size:
                          type: integer
                        sha256:
                          type: string
                          pattern: ^[0-9a-f]{64}$
                  usedBytes:
                    type: integer
                  totalBytes:
                    type: integer
        '400':
          description: '`name` is not `[A-Za-z0-9_-]{1,32}` (`invalidName`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags:
      - scripts
      - sounds
      summary: Delete all of a script's sounds
      description: Deletes every sound and the folder, and stops any that is playing. The script itself
        stays. `200` also when there was nothing to delete.
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '400':
          description: '`name` is not `[A-Za-z0-9_-]{1,32}` (`invalidName`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      tags:
      - scripts
      - sounds
      summary: Upload one sound into a script's folder
      description: The script must be installed first. The filename is 1–32 characters A–Z, a–z, 0–9,
        underscore or hyphen followed by .mp3, and a sound of the same name is replaced. The first chunk
        must identify MP3 audio. The multipart field name is not significant. Send one file. A failed
        write keeps a sound already stored under that name.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '400':
          description: invalidName (the script's or the file's name), or badRequest (no file received).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '415':
          description: 'unsupportedMediaType: not an MP3.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '507':
          description: 'insufficientStorage, write failed: the file could not be saved. The previous version
            is kept.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/apps/script/{name}/sounds/{sound}:
    delete:
      tags:
      - scripts
      - sounds
      summary: Delete one of a script's sounds
      description: The folder goes with the script's last sound. The script need not be installed, so
        sounds kept from a deleted script are deleted the same way.
      parameters:
      - name: name
        in: path
        required: true
        schema:
          type: string
          pattern: ^[A-Za-z0-9_-]{1,32}$
      - name: sound
        in: path
        required: true
        schema:
          type: string
          pattern: ^[A-Za-z0-9_-]{1,32}$
        description: Name without the .mp3 extension.
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '400':
          description: invalidName.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/v1/apps/{name}/config:
    get:
      tags:
      - scripts
      summary: The settings a script offers, with their current values
      description: 'A script declares its user-changeable values as `@config` lines in its

        header, and this route hands them back as field descriptors: what to

        show, what it holds now, and what it would fall back to. `GET

        /api/v1/apps` already flags which scripts have any, under `config`, so a

        client only asks for the ones that do.


        A script that declares none answers `200` with an empty `fields` list,

        not a `404`. `warnings` carries the `@config` lines the device could not

        read, each with its line number: a malformed one is skipped rather than

        failing the install, so this is where the author finds out.


        The values live in the script''s saved store, and the script can change

        them itself. So `value` can differ from `default` without anyone using

        this API. When a new source does not declare a setting, its stored

        value is removed.

        '
      parameters:
      - name: name
        in: path
        required: true
        schema:
          type: string
          pattern: ^[A-Za-z0-9_-]{1,32}$
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: The declared settings
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScriptConfig'
        '400':
          description: '`name` is not `[A-Za-z0-9_-]{1,32}` (`invalidName`)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
        '503':
          description: This build has no scripting platform (`unavailable`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '507':
          $ref: '#/components/responses/InsufficientStorage'
    patch:
      tags:
      - scripts
      summary: Change a script's settings
      description: 'Send only the keys to change. Every other setting keeps its value, and

        so does anything else the script has stored.


        **All or nothing.** One bad field rejects the whole body with `422` and

        `field` naming it: nothing is written and the script is not disturbed.

        Rejected are: a key the script never declared (`unknown setting`), a

        value of the wrong type, a `select` value that is not on the offered

        list, and text longer than the field allows. A `number` outside its

        `min`/`max` is clamped to the range instead of rejected. A script that

        declares no settings answers `422` with `no settings`.


        A `color` takes either the number (`0`-`16777215`) or an HTML-style

        `"#RRGGBB"` string, and is always stored as the number.


        **Saving restarts the script**, exactly as re-uploading its source

        would: `init()` and `setup()` run again and see the new values, values

        held in memory and subscriptions are dropped, the app keeps its place in the

        rotation. The reply therefore carries the same `error` field as

        `PUT /api/v1/apps/script/{name}`: a `200` with an `error` object

        means the settings were applied and the restarted script then threw.

        '
      parameters:
      - name: name
        in: path
        required: true
        schema:
          type: string
          pattern: ^[A-Za-z0-9_-]{1,32}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: 'Setting key to new value. Keys must be ones the script declared. An unknown
                key is a `422`, never silently ignored.

                '
              additionalProperties:
                oneOf:
                - type: string
                - type: number
                - type: boolean
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: Applied (check `error` for what the restarted script did)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppConfigWriteResult'
        '400':
          description: Malformed JSON (`invalidJson`) or a name outside `[A-Za-z0-9_-]{1,32}` (`invalidName`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '415':
          $ref: '#/components/responses/WrongContentType'
        '422':
          $ref: '#/components/responses/Invalid'
        '503':
          description: Scripting is switched off (`unavailable`), or busy (`serviceBusy`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '507':
          $ref: '#/components/responses/InsufficientStorage'
  /api/v1/apps/builtin/{name}/config:
    parameters:
    - name: name
      in: path
      required: true
      schema:
        type: string
        pattern: ^[A-Za-z0-9_-]{1,32}$
    get:
      tags:
      - apps
      summary: The settings of a built-in app, with their current values
      description: 'The built-in apps offer their settings here as field descriptors.

        `GET /api/v1/apps` flags them with `config`. A switched-off app can

        still be configured. A built-in without settings answers `200` with an

        empty `fields` list. `404` (`no such app`) when the device has no such

        built-in, it cannot run here, or a pushed app has its name.

        '
      responses:
        '200':
          description: The built-in app's fields, defaults and current values
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BuiltinAppConfig'
        '400':
          description: '`name` is not `[A-Za-z0-9_-]{1,32}` (`invalidName`)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags:
      - apps
      summary: Change a built-in app's settings
      description: 'Send only the settings to change, each at its descriptor''s `path`.

        `path: ["weekdayBar", "show"]` is sent as `{"weekdayBar":{"show":false}}`.

        Everything else keeps its value. The change shows at once and is saved

        with the other settings.


        **All or nothing.** One bad field rejects the whole body with `422` and

        `field` naming it: a setting the app does not offer on this device

        (`unknown setting`), a value of the wrong type or outside its range. An

        app without settings answers `422` with `no settings`.


        A `color` takes the number (`0`-`16777215`), `"#RRGGBB"` or the other

        color forms. `null` only where the descriptor is `nullable`. A `days`

        value is an array of lowercase weekday names.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BuiltinAppConfigPatch'
      responses:
        '200':
          description: Applied, with error null
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppConfigWriteResult'
        '400':
          description: Malformed JSON (`invalidJson`) or a name outside `[A-Za-z0-9_-]{1,32}` (`invalidName`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '415':
          $ref: '#/components/responses/WrongContentType'
        '422':
          $ref: '#/components/responses/Invalid'
  /api/v1/apps/{name}/data:
    get:
      tags:
      - scripts
      summary: What a script saved with store.set()
      description: 'The script''s saved values, key by key. The values of its `@config`

        settings are left out: they are under `/api/v1/apps/{name}/config`:

        and so is a key the script cleared with `store.set(key, nil)`. A script

        that saved nothing answers `{}`.

        '
      parameters:
      - name: name
        in: path
        required: true
        schema:
          type: string
          pattern: ^[A-Za-z0-9_-]{1,32}$
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: The saved values
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '400':
          description: '`name` is not `[A-Za-z0-9_-]{1,32}` (`invalidName`)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
        '503':
          description: This build has no scripting platform (`unavailable`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '507':
          $ref: '#/components/responses/InsufficientStorage'
    patch:
      tags:
      - scripts
      summary: Change what a script saved
      description: 'Send only the keys to change. A value replaces the stored one or adds

        the key, `null` removes it, and every key left out keeps its value.

        A key that is one of the script''s `@config` settings is refused with

        `422`. Change it through `/config`.


        **Saving restarts the script**, as saving its settings does: `init()`

        and `setup()` run again and read the new values. A `200` with an

        `error` object means the values were saved and the restarted script

        then threw.

        '
      parameters:
      - name: name
        in: path
        required: true
        schema:
          type: string
          pattern: ^[A-Za-z0-9_-]{1,32}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Stored key to its new value, or `null` to remove it.
              additionalProperties: true
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: Applied (check `error` for what the restarted script did)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  name:
                    type: string
                  error:
                    description: The restarted script's error. `null` when it works.
                    oneOf:
                    - type: 'null'
                    - $ref: '#/components/schemas/ScriptError'
        '400':
          description: Malformed JSON (`invalidJson`) or a name outside `[A-Za-z0-9_-]{1,32}` (`invalidName`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '415':
          $ref: '#/components/responses/WrongContentType'
        '422':
          $ref: '#/components/responses/Invalid'
        '503':
          description: Scripting is switched off (`unavailable`), or busy (`serviceBusy`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '507':
          $ref: '#/components/responses/InsufficientStorage'
  /api/v1/oauth:
    get:
      tags:
      - scripts
      summary: List script sign-ins
      description: 'Available when `capabilities.oauth` is true. Lists installed scripts that

        declare a sign-in provider. All OAuth responses have `Cache-Control: no-store`.

        See [Script sign-in](https://blueforcer.github.io/awtrix-ng/reference/http/#script-sign-in).

        '
      responses:
        '404':
          $ref: '#/components/responses/NotOnThisDevice'
        '200':
          description: Callback address and script sign-ins
          content:
            application/json:
              schema:
                type: object
                required:
                - redirectUri
                - apps
                properties:
                  redirectUri:
                    type: string
                    format: uri
                  apps:
                    type: array
                    items:
                      $ref: '#/components/schemas/OAuthStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/v1/oauth/{name}:
    parameters:
    - $ref: '#/components/parameters/OAuthName'
    get:
      tags:
      - scripts
      summary: Read a script sign-in
      description: The client secret and tokens are never returned. Responses are not cached.
      responses:
        '200':
          description: Script sign-in status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      tags:
      - scripts
      summary: Save sign-in client credentials
      description: 'Available when `capabilities.oauth` is true. Changes require the device''s

        own Origin and `X-Awtrix-OAuth: 1`. Omitted fields keep their values.

        An empty client secret keeps the saved secret. `clearSecret: true` removes

        it. A new client ID signs out the script. Responses are not cached.

        The JSON object must fit within 8192 bytes. Each string field is limited to 4096 UTF-8 bytes.

        '
      parameters:
      - $ref: '#/components/parameters/OAuthOrigin'
      - $ref: '#/components/parameters/OAuthHeader'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                clientId:
                  type: string
                  maxLength: 4096
                clientSecret:
                  type: string
                  maxLength: 4096
                  writeOnly: true
                clearSecret:
                  type: boolean
                  default: false
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '400':
          $ref: '#/components/responses/BadJson'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/OAuthForbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Invalid'
    delete:
      tags:
      - scripts
      summary: Sign out a script
      description: Keeps the client ID and client secret. Responses are not cached.
      parameters:
      - $ref: '#/components/parameters/OAuthOrigin'
      - $ref: '#/components/parameters/OAuthHeader'
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/OAuthForbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/v1/oauth/{name}/start:
    parameters:
    - $ref: '#/components/parameters/OAuthName'
    post:
      tags:
      - scripts
      summary: Start a script sign-in
      description: 'Send an empty JSON object or no body. Open the returned URL in the same

        browser. The sign-in expires after ten minutes. Responses are not cached.

        '
      parameters:
      - $ref: '#/components/parameters/OAuthOrigin'
      - $ref: '#/components/parameters/OAuthHeader'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              example: {}
      responses:
        '200':
          description: Provider sign-in address
          content:
            application/json:
              schema:
                type: object
                required:
                - url
                properties:
                  url:
                    type: string
                    format: uri
        '400':
          $ref: '#/components/responses/BadJson'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/OAuthForbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Invalid'
  /api/v1/oauth/{name}/code:
    parameters:
    - $ref: '#/components/parameters/OAuthName'
    post:
      tags:
      - scripts
      summary: Complete a script sign-in
      description: 'Submit the provider''s code and state. The JSON object must fit within

        8192 bytes. Each string is limited to 4096 UTF-8 bytes. A 202 response accepts the request. Poll
        the script status

        for signedIn or error. Responses are not cached.

        '
      parameters:
      - $ref: '#/components/parameters/OAuthOrigin'
      - $ref: '#/components/parameters/OAuthHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - code
              - state
              properties:
                code:
                  type: string
                  maxLength: 4096
                state:
                  type: string
                  maxLength: 4096
      responses:
        '202':
          $ref: '#/components/responses/Ok'
        '400':
          $ref: '#/components/responses/BadJson'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/OAuthForbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Invalid'
  /api/v1/scripts/shared:
    get:
      tags:
      - scripts
      summary: What the scripts have published to each other
      description: 'The `shared` module''s whole key/value space. Scripts write BARE keys,

        which are filed under the writing app, and read QUALIFIED ones

        (`owner.key`): so a value''s origin is always exactly who wrote it and

        no app can overwrite another''s. Values are scalars only. A script

        publishes `json.dump(...)` when it needs structure.


        Nothing here survives a reboot, and nothing survives its author:

        removing a script (or re-saving it, which restarts it) drops

        everything it had published.


        Read-only: only scripts can write here.

        '
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: Grouped by owner, ordered by key within each. `[]` when empty.
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  required:
                  - owner
                  - key
                  - type
                  - value
                  - ageMs
                  properties:
                    owner:
                      type: string
                      description: 'Install name of the script that wrote it: the only one that may.'
                    key:
                      type: string
                      description: 'The bare key inside that namespace, `[A-Za-z0-9_-]{1,24}`. Scripts
                        address it as `owner.key`. Keys cannot contain a dot.

                        '
                    type:
                      type: string
                      enum:
                      - int
                      - real
                      - bool
                      - string
                    value:
                      description: The value in its own JSON type. A non-finite real is `null`.
                      type:
                      - integer
                      - number
                      - boolean
                      - string
                      - 'null'
                    ageMs:
                      type: integer
                      description: 'Milliseconds since it was last written. Nothing expires on its own:
                        this is how a reader tells a live value from one whose provider quietly stopped
                        updating.

                        '
        '503':
          description: This build has no scripting platform (`unavailable`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/capabilities:
    get:
      tags:
      - device
      summary: What this device supports
      description: 'Effect, transition, overlay and palette names, sound outputs and sensors. Lists every
        name in the spelling the API returns. Requests may use any casing.

        '
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: What this device supports.
          content:
            application/json:
              schema:
                type: object
                properties:
                  effects:
                    type: array
                    items:
                      type: string
                  paletteEffects:
                    type: array
                    items:
                      type: string
                    description: 'The subset of `effects` that honors the app''s `palette`. Scene-style
                      effects that draw fixed colors (PingPong, Matrix, LookingEyes) are absent from it.
                      `blend` applies when `palette` is supplied too. It interpolates between palette
                      entries.

                      '
                  transitions:
                    type: array
                    items:
                      type: string
                  overlays:
                    type: array
                    items:
                      type: string
                  audio:
                    type: object
                    description: What this device can play. Every flag is always present.
                    required:
                    - mp3
                    - rtttl
                    - song
                    - speech
                    - track
                    - radio
                    - url
                    - effect
                    - clip
                    properties:
                      mp3:
                        type: boolean
                        allOf:
                        - description: Stored MP3s play on the speaker
                      rtttl:
                        type: boolean
                        allOf:
                        - description: Melodies play on the speaker
                      song:
                        type: boolean
                        allOf:
                        - description: The synthesizer plays song text
                      speech:
                        type: boolean
                        allOf:
                        - description: The clock reads text aloud. A voice is installed
                      track:
                        type: boolean
                        allOf:
                        - description: Always false
                      radio:
                        type: boolean
                        allOf:
                        - description: Internet radio plays
                      url:
                        type: boolean
                        allOf:
                        - description: '`file` takes an http(s):// address'
                      effect:
                        type: boolean
                        allOf:
                        - description: A script's effects and background music play over each other
                      clip:
                        type: boolean
                        allOf:
                        - description: A WAV or MP3 sent to POST /api/v1/audio/clip plays once
                  microphone:
                    type: boolean
                    allOf:
                    - description: Scripts and music visualizations can hear the microphone. It says the
                        microphone exists, not that it works at this moment.
                  sensors:
                    type: object
                    description: 'Which sensors this device has. Without a light sensor `autoBrightness`
                      has no effect and the panel shows `brightness`.

                      '
                    properties:
                      light:
                        type: boolean
                        allOf:
                        - description: Always false. This clock has no light sensor.
                  clockFaces:
                    type: array
                    items:
                      type: string
                      enum:
                      - sheet
                      - ring
                      - flap
                      - month
                      - big
                    description: 'The values the `clockFace` setting accepts.

                      '
                  palettes:
                    type: array
                    items:
                      type: string
                    description: 'The built-in palette names, which always resolve. Uploaded palettes
                      are not listed here. Read them from GET /api/v1/files?dir=/PALETTES. A file may
                      carry a built-in''s name and replaces it while it exists.

                      '
                  gpio:
                    oneOf:
                    - type: 'null'
                      description: Always null. The pins of this clock are fixed.
                  platform:
                    type: object
                    properties:
                      id:
                        type: string
                        description: Platform identity.
                  scriptUpdates:
                    type: boolean
                    description: The compare-and-update script route is available.
                  ble:
                    type: boolean
                    enum:
                    - true
                    description: Present only where scripts can import ble. Radio health is reported by
                      ble.state().
                  gamepad:
                    type: boolean
                    enum:
                    - true
                    description: Present only while scripts can import gamepad, which needs scripting
                      on. /api/v1/gamepad exists. A Bluetooth gamepad can be paired where ble is true
                      as well.
                  gamepadRemote:
                    type: boolean
                    enum:
                    - true
                    description: Present only while a phone can be a gamepad, which needs scripting on.
                      POST /api/v1/gamepad/remote exists.
                  mqttTls:
                    type: boolean
                    enum:
                    - true
                    description: Present only where MQTT can connect over TLS. /api/v1/mqtt/tls exists.
                  voice:
                    type: boolean
                    enum:
                    - true
                    description: Present only where Home Assistant Voice can be set up. /api/v1/voice
                      exists.
                  bootSound:
                    type: boolean
                    enum:
                    - true
                    description: Present only where the clock plays a sound at power-on. The bootSound
                      setting switches it.
                  enlargeApps:
                    type: boolean
                    enum:
                    - true
                    description: Present only where pushed apps and notifications can be shown at double
                      size. The enlargeApps setting switches it.
                  oauth:
                    type: boolean
                    enum:
                    - true
                    description: Present only while scripts can import oauth, which needs scripting on.
                      /api/v1/oauth exists.
                  crypto:
                    type: boolean
                    enum:
                    - true
                    description: Present only while scripts can import crypto
                    which needs scripting on.: null
                  tcp:
                    type: boolean
                    enum:
                    - true
                    description: Present only while scripts can import tcp
                    which needs scripting on.: null
                  display:
                    type: object
                    properties:
                      width:
                        type: integer
                      height:
                        type: integer
                      requestedWidth:
                        type: integer
                      requestedHeight:
                        type: integer
                      minWidth:
                        type: integer
                      maxWidth:
                        type: integer
                      minHeight:
                        type: integer
                      maxHeight:
                        type: integer
                      maxPixels:
                        type: integer
                      configurable:
                        type: boolean
                      restartRequired:
                        type: boolean
                      ready:
                        type: boolean
                    allOf:
                    - description: 'Active dimensions: 52×16.'
                  fonts:
                    type: array
                    items:
                      type: object
                      properties:
                        name:
                          type: string
                        ascent:
                          type: integer
                        descent:
                          type: integer
                        lineHeight:
                          type: integer
                  layout:
                    type: boolean
                    description: Present and true while scripts can use prepared layouts, which needs
                      scripting on.
                  layouts:
                    type: object
                    description: The limits of prepared layouts.
                    properties:
                      version:
                        type: integer
                      limits:
                        type: object
                        properties:
                          regions:
                            type: integer
                          scrollers:
                            type: integer
                          assets:
                            type: integer
                          chartPoints:
                            type: integer
                          textBytes:
                            type: integer
                          preparedBytes:
                            type: integer
                          scriptHandles:
                            type: integer
                          scriptHandlesPerScript:
                            type: integer
  /api/v1/system:
    get:
      tags:
      - system
      summary: Device configuration (network, MQTT, NTP, auth, hardware)
      description: 'Secrets (`wifiPass`, `mqttPass`, `authPass`) are omitted from the response (every
        other configuration field is returned) unless `?secrets=1` is passed, which includes them so a
        backup can round-trip the wifi/MQTT/auth credentials. In setup mode, any `secrets` query parameter
        makes the whole request fail with `403 forbidden`. A request for secrets from a web page of another
        site answers `403 forbiddenOrigin` and carries no CORS headers.

        '
      parameters:
      - name: secrets
        in: query
        required: false
        schema:
          type: boolean
        description: 'Include the secret fields in the response (backup export). Any presence of this
          parameter is refused in setup mode, even when false. Behind HTTP auth when configured.

          '
      responses:
        '403':
          $ref: '#/components/responses/ForbiddenOwnPage'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: The device configuration.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SystemConfigRead'
    put:
      tags:
      - system
      summary: Update device configuration (partial. Most changes apply after reboot)
      description: 'In setup mode only string fields `wifiSsid`, `wifiPass` and

        `hostname` are accepted. Other fields, duplicate fields and malformed

        setup bodies are refused with `403 forbidden`.


        Partial merge. Every numeric field is range/type checked BEFORE

        anything is stored, with the ranges given in `SystemConfig`. The first

        offender is rejected with `422 validationFailed` and `field` naming it,

        and nothing is saved.


        Unknown keys are ignored and strings are not range-checked.


        The three secret fields honor skip-empty: sending `""` for `wifiPass`,

        `mqttPass` or `authPass` leaves the stored secret alone rather than

        clearing it. Non-secret strings can be cleared with `""`.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SystemConfig'
      responses:
        '415':
          $ref: '#/components/responses/WrongContentType'
        '403':
          $ref: '#/components/responses/ForbiddenOwnPage'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: The resulting configuration (secrets omitted)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SystemConfigRead'
        '400':
          description: Malformed JSON (`invalidJson`), or a pin map the chip does not allow (`invalidPinConfig`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          $ref: '#/components/responses/Invalid'
  /api/v1/system/wifi-scan:
    get:
      tags:
      - system
      summary: Scan for WiFi networks (async)
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '202':
          description: 'Scan running: poll again'
          content:
            application/json:
              schema:
                type: object
                properties:
                  scanning:
                    type: boolean
        '200':
          description: The networks found.
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    ssid:
                      type: string
                    rssi:
                      type: integer
                    enc:
                      type: boolean
  /api/v1/logs:
    get:
      tags:
      - system
      summary: Incremental device log
      parameters:
      - name: after
        in: query
        schema:
          type: integer
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: The log lines.
          content:
            application/json:
              schema:
                type: object
                properties:
                  next:
                    type: integer
                  lines:
                    type: array
                    items:
                      type: string
  /api/v1/gamepad:
    description: Only where capabilities.gamepad is true. Elsewhere 404.
    get:
      tags:
      - system
      summary: Both gamepad slots, their players and the phones that play
      description: 'Always two slots, empty ones included. A single ready gamepad is player 1. With two,
        the one whose first input arrived first is player 1. The slot id is not the player number. `remotes`
        lists the phones that play.

        '
      responses:
        '404':
          $ref: '#/components/responses/NotOnThisDevice'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: Both gamepad slots.
          content:
            application/json:
              schema:
                type: object
                required:
                - devices
                properties:
                  devices:
                    type: array
                    minItems: 2
                    maxItems: 2
                    items:
                      $ref: '#/components/schemas/GamepadDevice'
                  remotes:
                    type: array
                    maxItems: 2
                    description: One entry for each phone that plays. `[]` when none does.
                    items:
                      type: object
                      required:
                      - session
                      - name
                      - player
                      properties:
                        session:
                          type: integer
                          description: The session number of that phone.
                        name:
                          type: string
                          description: The phone's name.
                        player:
                          type: integer
                          enum:
                          - 1
                          - 2
  /api/v1/gamepad/pair:
    description: Only where capabilities.gamepad and capabilities.ble are true. Elsewhere 404.
    post:
      tags:
      - system
      summary: Pair a gamepad into a free slot
      description: 'Looks for a new gamepad in pairing mode for a minute. Gamepads that are paired already
        are not taken. Calling again during the search answers the same slot without starting the minute
        over. The other gamepad stays connected. GET /api/v1/gamepad follows the result.

        '
      responses:
        '404':
          $ref: '#/components/responses/NotOnThisDevice'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: The search runs for the returned slot.
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - id
                properties:
                  ok:
                    type: boolean
                    enum:
                    - true
                  id:
                    type: integer
                    enum:
                    - 1
                    - 2
        '409':
          description: 'gamepadsFull, "no free slot": both slots hold a gamepad. Forget one first.'
        '503':
          $ref: '#/components/responses/Unavailable'
  /api/v1/gamepad/{id}:
    description: Only where capabilities.gamepad and capabilities.ble are true. Elsewhere 404.
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: integer
        enum:
        - 1
        - 2
      description: The slot, not a player number.
    delete:
      tags:
      - system
      summary: Forget the gamepad in this slot, with its pairing
      description: The other gamepad stays connected. An empty slot answers 200 as well.
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
        '404':
          description: 'notFound, "no such gamepad": the id is not 1 or 2.'
  /api/v1/gamepad/remote:
    description: Only where capabilities.gamepadRemote is true. Elsewhere 404.
    post:
      tags:
      - system
      summary: Make a phone the gamepad of one player
      description: 'Without `player` the clock picks a player that no phone and no ready Bluetooth gamepad
        plays, else one that no phone plays, else player 1. A phone that asks for a player another phone
        plays takes it over, and the other phone''s token stops working.


        The phone then sends UDP datagrams of exactly 32 bytes to `port`, numbers little-endian, on every
        change and at least every 100 ms: byte 0 the version `1`, bytes 1-16 the token as raw bytes, bytes
        17-20 a u32 sequence number that must grow, bytes 21-24 a u32 of held buttons (bit n = button
        n: A 0, B 1, X 3, Y 4, L1 6, R1 7, L2 8, R2 9, SELECT 10, START 11, HOME 12, L3 13, R3 14), byte
        25 the D-pad as i8 (`-1` released, `0`-`7` clockwise from up), bytes 26-29 the sticks lx, ly,
        rx, ry (`128` centre, `0` up or left), bytes 30-31 the triggers lt, rt (`0` to `255`). After one
        second without a valid datagram the session ends.

        '
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 32
                  description: The phone's name. "Phone" when missing or empty.
                player:
                  type: integer
                  enum:
                  - 1
                  - 2
      responses:
        '404':
          $ref: '#/components/responses/NotOnThisDevice'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: The session runs. Send the controls to this port with this token.
          content:
            application/json:
              schema:
                type: object
                required:
                - port
                - token
                - player
                - session
                properties:
                  port:
                    type: integer
                    enum:
                    - 4214
                  token:
                    type: string
                    pattern: ^[0-9a-f]{32}$
                  player:
                    type: integer
                    enum:
                    - 1
                    - 2
                  session:
                    type: integer
        '400':
          description: invalidJson, invalidName (a name that is not text or too long) or invalidPlayer
            (`must be 1..2`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/Unavailable'
  /api/v1/gamepad/remote/{session}:
    parameters:
    - name: session
      in: path
      required: true
      schema:
        type: integer
        minimum: 1
      description: The number from POST /api/v1/gamepad/remote.
    delete:
      tags:
      - system
      summary: End one phone's session
      responses:
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
        '404':
          description: 'notFound, "no such session": no phone plays under that number.'
  /api/v1/mqtt/tls:
    description: Only where capabilities.mqttTls is true. Elsewhere 404.
    get:
      tags:
      - system
      summary: How the MQTT client trusts its broker over TLS
      responses:
        '404':
          $ref: '#/components/responses/NotOnThisDevice'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: How the broker is trusted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MqttTls'
  /api/v1/mqtt/tls/ca:
    description: Only where capabilities.mqttTls is true. Elsewhere 404.
    put:
      tags:
      - system
      summary: Upload the CA the MQTT broker's certificate must come from
      description: Replaces public certificate authorities and mqttTlsPin from the next connection attempt
        on.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - certificate
              properties:
                certificate:
                  type: string
                  maxLength: 65536
                  description: One or more CA certificates in PEM.
      responses:
        '415':
          $ref: '#/components/responses/WrongContentType'
        '404':
          $ref: '#/components/responses/NotOnThisDevice'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: Saved. How the broker is trusted now.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MqttTls'
        '422':
          $ref: '#/components/responses/Invalid'
        '507':
          description: 'insufficientStorage: not saved'
    delete:
      tags:
      - system
      summary: Back to public certificate authorities and the trusted fingerprint
      description: Also deletes an unusable CA.
      responses:
        '404':
          $ref: '#/components/responses/NotOnThisDevice'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: Deleted. How the broker is trusted now.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MqttTls'
  /api/v1/voice:
    description: Only where capabilities.voice is true. Elsewhere 404.
    get:
      tags:
      - system
      summary: Home Assistant Voice settings and connection
      responses:
        '404':
          $ref: '#/components/responses/NotOnThisDevice'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: The Home Assistant Voice settings.
          content:
            application/json:
              schema:
                type: object
                required:
                - config
                - state
                - error
                - pipelines
                properties:
                  config:
                    type: object
                    required:
                    - enabled
                    - url
                    - pipeline
                    - device
                    - tokenSet
                    properties:
                      enabled:
                        type: boolean
                      url:
                        type: string
                        description: Home Assistant address. "" when none is set.
                      pipeline:
                        type: string
                        description: Assist pipeline ID. "" for the default.
                      device:
                        type: string
                        description: Home Assistant device ID whose area is the room for requests. ""
                          when none is set.
                      tokenSet:
                        type: boolean
                        description: The token itself is never returned.
                  state:
                    type: string
                    enum:
                    - offline
                    - connecting
                    - ready
                    - starting
                    - listening
                    - processing
                    - speaking
                    - error
                  error:
                    type: string
                    description: 'Why the last attempt failed. "" when all is well. One of connectionLost,
                      tokenRejected, haError, pipelineIncomplete, nothingUnderstood, audioTooSlow, timeout,
                      microphoneFailed, playbackFailed, microphoneUpdate, configUnavailable. Later versions
                      may add codes.

                      '
                  pipelines:
                    type: array
                    items:
                      type: object
                    description: Home Assistant's Assist pipelines once connected.
    post:
      tags:
      - system
      summary: Change the Home Assistant Voice settings
      description: 'Only from the device''s own web page: needs X-Awtrix-Voice: 1 and an Origin matching
        the device. Left-out fields keep their value. enabled needs an address and a token. A new address
        needs the token again.

        '
      parameters:
      - name: X-Awtrix-Voice
        in: header
        required: true
        schema:
          type: string
          enum:
          - '1'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                enabled:
                  type: boolean
                url:
                  type: string
                  maxLength: 512
                  description: Origin only, for example http://homeassistant.local:8123.
                pipeline:
                  type: string
                  maxLength: 128
                device:
                  type: string
                  maxLength: 64
                  pattern: ^[A-Za-z0-9]*$
                  description: Home Assistant device ID. "" for none.
                token:
                  type: string
                  maxLength: 4096
                  description: A long-lived access token.
                clearToken:
                  type: boolean
                  description: true deletes the saved token.
      responses:
        '404':
          $ref: '#/components/responses/NotOnThisDevice'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
        '403':
          description: 'forbiddenOrigin: not sent from the device''s own web page.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          $ref: '#/components/responses/Invalid'
  /api/v1/icons/origins:
    description: 'Records which published icon a local icon came from. Kept on the device and available
      offline.

      Overwriting an icon keeps the recorded SHA256, so you can detect local edits. Deleting an icon

      removes its record. These are user-supplied links, not signatures or proof of authorship.

      All methods are blocked in setup mode, including GET.

      At most 64 records and 16 KiB in total.

      '
    get:
      tags:
      - files
      summary: List installed icon origins
      responses:
        '403':
          $ref: '#/components/responses/Forbidden'
        '200':
          description: Existing icon files with saved origins, no pagination within the hard limit
          headers:
            Cache-Control:
              schema:
                type: string
                const: no-store
          content:
            application/json:
              schema:
                type: object
                required:
                - icons
                properties:
                  icons:
                    type: array
                    maxItems: 64
                    items:
                      $ref: '#/components/schemas/IconOrigin'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          description: 'storageError: unreadable or corrupt origin metadata'
    put:
      tags:
      - files
      summary: Associate an existing local icon with a published original
      description: Creates or replaces the whole record for this filename. Sending it twice is safe. Maximum
        body 1024 bytes.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IconOrigin'
      responses:
        '415':
          $ref: '#/components/responses/WrongContentType'
        '200':
          $ref: '#/components/responses/Ok'
        '400':
          description: invalidOrigin
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '413':
          description: payloadTooLarge
        '507':
          description: 'insufficientStorage: record or byte limit reached'
        '500':
          description: 'storageError: the origin records could not be read or saved'
    delete:
      tags:
      - files
      summary: Remove a local origin association without deleting the image
      parameters:
      - name: name
        in: query
        required: true
        schema:
          type: string
          pattern: ^[A-Za-z0-9_-]{1,32}\.(gif|jpg)$
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '400':
          description: invalidName
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          description: 'storageError: the origin records could not be read or saved'
  /api/v1/icons/rename:
    post:
      tags:
      - files
      summary: Rename an icon
      description: 'The icon''s origin record moves to the new name. Apps, notifications and scripts that
        use the

        old name show no icon until they are changed. Blocked in setup mode.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - from
              - to
              properties:
                from:
                  type: string
                  pattern: ^[A-Za-z0-9_-]{1,32}\.(gif|jpg)$
                  example: mail.gif
                to:
                  type: string
                  pattern: ^[A-Za-z0-9_-]{1,32}\.(gif|jpg)$
                  description: Same extension as from
                  example: letter.gif
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '400':
          description: 'invalidJson, or invalidName: a name is not valid or the extension differs'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: 'nameTaken: an icon with the new name exists as .gif or .jpg'
        '500':
          description: 'internalError or storageError: the icon keeps its name'
  /api/v1/files:
    get:
      tags:
      - files
      summary: List files (icons, melodies, palettes, sounds)
      parameters:
      - name: dir
        in: query
        schema:
          type: string
          default: /ICONS
      responses:
        '400':
          $ref: '#/components/responses/InvalidPath'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          description: The files in the folder.
          content:
            application/json:
              schema:
                type: object
                properties:
                  files:
                    type: array
                    items:
                      type: object
                      properties:
                        name:
                          type: string
                        size:
                          type: integer
                  usedBytes:
                    type: integer
                  totalBytes:
                    type: integer
    post:
      tags:
      - files
      summary: Upload a file (multipart) into ?dir=
      parameters:
      - name: dir
        in: query
        schema:
          type: string
          default: /ICONS
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '400':
          description: '`invalidPath` for a folder outside the asset folders, `invalidName` for an MP3
            name that is not valid, or `badRequest`, `no file received`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '415':
          description: '`unsupportedMediaType`: the file does not suit its folder, for example `expected
            GIF or JPEG` in /ICONS.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
        '409':
          description: '`nameTaken`, `name taken`: an MP3 for /MP3 whose name a melody has, or a melody
            for /MELODIES whose name an MP3 has'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '507':
          description: 'insufficientStorage, write failed: the file could not be saved. The previous version
            is kept.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      tags:
      - files
      summary: Delete a file
      parameters:
      - name: path
        in: query
        required: true
        schema:
          type: string
      responses:
        '400':
          $ref: '#/components/responses/InvalidPath'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '200':
          $ref: '#/components/responses/Ok'
        '404':
          $ref: '#/components/responses/NotFound'
  /version:
    get:
      tags:
      - device
      summary: Read the version as plain text
      responses:
        '200':
          description: Version string.
          content:
            text/plain:
              schema:
                type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/v1/audio/mp3:
    get:
      tags:
      - sounds
      summary: List stored MP3 files, and each script's own sounds
      responses:
        '200':
          description: The MP3s in /MP3, the sounds of each installed script that has any, and whole-filesystem
            usage.
          content:
            application/json:
              schema:
                type: object
                properties:
                  files:
                    type: array
                    items:
                      type: object
                      properties:
                        name:
                          type: string
                        size:
                          type: integer
                  scripts:
                    type: array
                    description: One entry for each script folder with at least one sound.
                    items:
                      type: object
                      properties:
                        name:
                          type: string
                          description: The script's install name
                        title:
                          type: string
                          description: Its @name header, or the install name without one
                        orphan:
                          type: boolean
                          description: True for sounds whose script was deleted without them
                        files:
                          type: array
                          items:
                            type: object
                            properties:
                              name:
                                type: string
                              size:
                                type: integer
                  usedBytes:
                    type: integer
                  totalBytes:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
      - sounds
      summary: Upload one stored MP3
      description: The filename is 1–32 characters A–Z, a–z, 0–9, underscore or hyphen followed by .mp3.
        The first chunk must identify MP3 audio. The multipart field name is not significant. Send one
        file. An MP3 and a melody never share a name, so a name a melody has answers 409.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '400':
          description: invalidName.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: 'nameTaken, "name taken": a melody of that name exists.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '415':
          description: 'unsupportedMediaType: not an MP3.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '507':
          description: 'insufficientStorage, write failed: the file could not be saved. The previous version
            is kept.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/audio/mp3/{name}:
    delete:
      tags:
      - sounds
      summary: Delete a stored MP3
      parameters:
      - name: name
        in: path
        required: true
        schema:
          type: string
          pattern: ^[A-Za-z0-9_-]{1,32}$
        description: Name without the .mp3 extension.
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '400':
          description: invalidName.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/v1/audio/mp3/rename:
    post:
      tags:
      - sounds
      summary: Rename a stored MP3
      description: 'Renames one MP3 in /MP3. Sounds in a script''s own folder cannot be renamed. Alarms,

        notifications and scripts that play the old name stay silent until they are changed.

        Blocked in setup mode.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - from
              - to
              properties:
                from:
                  type: string
                  pattern: ^[A-Za-z0-9_-]{1,32}$
                  example: ding
                to:
                  type: string
                  pattern: ^[A-Za-z0-9_-]{1,32}$
                  example: bell
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '400':
          description: 'invalidJson, or invalidName: a name is not valid'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: 'nameTaken: an MP3 or a melody with the new name exists'
        '500':
          description: 'internalError: the MP3 keeps its name'
components:
  parameters:
    OAuthName:
      name: name
      in: path
      required: true
      schema:
        type: string
        pattern: ^[A-Za-z0-9_-]{1,32}$
      description: Installed script with a sign-in provider.
    OAuthOrigin:
      name: Origin
      in: header
      required: true
      schema:
        type: string
      description: The scheme, host and port of the device's web UI.
    OAuthHeader:
      name: X-Awtrix-OAuth
      in: header
      required: true
      schema:
        type: string
        enum:
        - '1'
  securitySchemes:
    basicAuth:
      type: http
      scheme: basic
      description: 'HTTP Basic, realm "AWTRIX NG". Enabled only when the `authEnabled` device

        configuration flag is true. The shipping default is false, so the whole

        API is open. Once on, it applies in every mode, including

        setup mode. A failure answers 401 with the standard JSON

        error body (code `unauthorized`) plus a `WWW-Authenticate` header.

        Applies to every route below, and to `GET /` and the static asset

        directories.

        '
  responses:
    NotOnThisDevice:
      description: '`notFound`, `unknown route`: this device does not have the feature.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    PayloadTooLarge:
      description: '`payloadTooLarge`: the body is larger than this device takes. Nothing was applied.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    InvalidPath:
      description: '`invalidPath`: the path is outside the asset folders or contains `..`.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    OAuthForbidden:
      description: 'Forbidden in setup mode, or forbiddenOrigin when the Origin does not match

        this device or X-Awtrix-OAuth is missing or is not 1.

        See [Setup mode](https://blueforcer.github.io/awtrix-ng/reference/errors/#provisioning-lockdown-403).

        '
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Basic auth is enabled and credentials are missing or wrong
      headers:
        WWW-Authenticate:
          schema:
            type: string
          description: Basic realm="AWTRIX NG"
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Ok:
      description: Success
      content:
        application/json:
          schema:
            type: object
            properties:
              ok:
                type: boolean
    BadJson:
      description: Malformed JSON body
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Invalid:
      description: Validation failed (error.field names the offender)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Unknown app/sound/file
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: Request is unavailable in setup mode (`Wi-Fi setup only`)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ForbiddenOwnPage:
      description: '`forbidden` in setup mode, or `forbiddenOrigin` for a request from a web page of another
        site: only the device''s own web page and clients that are not browsers may use this route'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    WrongContentType:
      description: A supplied Content-Type is not application/json. An omitted header is accepted.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    InsufficientStorage:
      description: A store or queue is full, or a change could not be saved. See the error reference for
        applied-state details.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unavailable:
      description: 'The feature is not present on this build or this hardware. Unlike a 500 nothing went
        wrong, and unlike serviceBusy retrying will not help.

        '
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    OAuthStatus:
      type: object
      required:
      - name
      - provider
      - scope
      - pkce
      - clientId
      - clientSecretSet
      - state
      properties:
        name:
          type: string
        provider:
          type: string
          description: Host of the provider's sign-in page.
        scope:
          type: string
        pkce:
          type: boolean
        clientId:
          type: string
        clientSecretSet:
          type: boolean
        state:
          type: string
          enum:
          - signedOut
          - pending
          - signedIn
          - error
        error:
          type: string
          description: Present after a sign-in failure.
        invalid:
          type: string
          description: Present when the script's sign-in declaration is unusable.
    GamepadDevice:
      type: object
      required:
      - id
      - state
      - name
      - address
      - player
      properties:
        id:
          type: integer
          enum:
          - 1
          - 2
          description: The slot.
        state:
          type: string
          enum:
          - unpaired
          - pairing
          - waiting
          - connecting
          - ready
          description: unpaired is an empty slot, pairing looks for a new gamepad, waiting is paired but
            not connected.
        name:
          type: string
          description: Bluetooth name. "" for an empty slot.
        address:
          type: string
          description: Bluetooth address. "" for an empty slot.
        player:
          type:
          - integer
          - 'null'
          enum:
          - 1
          - 2
          - null
          description: The player while ready. Null otherwise.
    IconOrigin:
      type: object
      required:
      - name
      - hub
      - slug
      - sha256
      properties:
        name:
          type: string
          pattern: ^[A-Za-z0-9_-]{1,32}\.(gif|jpg)$
          description: Existing local filename including extension
        hub:
          type: string
          maxLength: 240
          pattern: ^https://[^@?#%\\ ]+/icons/$
          description: HTTPS icon base. No credentials, query, fragment, escapes or dot-segments
        slug:
          type: string
          pattern: ^[a-z0-9_-]{1,32}$
        sha256:
          type: string
          pattern: ^[a-f0-9]{64}$
          description: SHA256 of local bytes when linked. Not updated by normal file overwrite
    ScriptMeta:
      type: object
      description: 'Scripts and modules only, in `GET /api/v1/apps`. The `@name`/`@desc`/`@author`/`@version`/`@icons`/`@requires`/`@needs`/`@display`
        header comments the source declared. Each string is `""` when absent. Omitted for other origins
        and on a build with no scripting platform.

        '
      properties:
        name:
          type: string
          description: 'The `@name` header: presentation only, not the install name.'
        desc:
          type: string
        author:
          type: string
        version:
          type: string
        icons:
          type: array
          items:
            type: string
            pattern: ^[A-Za-z0-9_-]{1,32}$
          maxItems: 32
          description: 'The icon database IDs from the `@icons` header lines: what the script draws with
            `icon()` and what a client offers to install into `/ICONS`. `[]` when the header is absent.

            '
        requires:
          type: array
          maxItems: 8
          description: 'The `@requires` header lines: the scripts and modules this script needs on the
            same display. `[]` when the header names none.

            '
          items:
            type: object
            required:
            - name
            - missing
            properties:
              name:
                type: string
                pattern: ^[A-Za-z0-9_-]{1,32}$
                description: A script's install name or a module's import name.
              hub:
                type: string
                pattern: ^[A-Za-z0-9]{12}$
                description: Its AWTRIX Hub ID, when the line gives one.
              missing:
                type: boolean
                description: 'True while no installed script has this name and no installed module is
                  imported under it.

                  '
        needs:
          type: array
          maxItems: 8
          description: 'The capabilities the `@needs` header lines ask for. `[]` when the header names
            none. The display only reports. It installs and runs the script either way.

            '
          items:
            type: object
            required:
            - name
            - missing
            properties:
              name:
                type: string
                pattern: ^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$
                description: 'A boolean of `GET /api/v1/capabilities` at this dotted path, for example
                  `audio.rtttl`.

                  '
              missing:
                type: boolean
                description: True while this display does not have it.
        display:
          description: 'The smallest panel from the `@display` header line, or null when the script names
            none and runs on any panel.

            '
          oneOf:
          - type: 'null'
          - type: object
            required:
            - width
            - height
            - fits
            properties:
              width:
                type: integer
                minimum: 1
              height:
                type: integer
                minimum: 1
              fits:
                type: boolean
                description: True when this display's panel is at least that large.
    BuiltinAppConfig:
      type: object
      description: 'What `GET /api/v1/apps/builtin/{name}/config` answers: the settings a built-in app
        offers on this device, each with its factory value and the value it holds now.

        '
      required:
      - name
      - fields
      - warnings
      properties:
        name:
          type: string
          description: The built-in app's name.
        fields:
          type: array
          items:
            $ref: '#/components/schemas/BuiltinAppConfigField'
        warnings:
          type: array
          maxItems: 0
          items:
            type: string
    BuiltinAppConfigField:
      type: object
      required:
      - key
      - type
      - path
      - default
      - value
      properties:
        key:
          type: string
          description: The setting's name. A weekday bar member is dotted, such as `weekdayBar.show`.
        type:
          type: string
          enum:
          - bool
          - select
          - color
          - days
        group:
          type: string
          enum:
          - time
          - calendar
          - date
          - weekday
          description: The part of the form the setting belongs to. Omitted for apps with one part.
        path:
          type: array
          minItems: 1
          items:
            type: string
          description: 'Where the value goes in a `PATCH`. [weekdayBar, show] means send {"weekdayBar":{"show":false}}.

            '
          example:
          - weekdayBar
          - show
        nullable:
          type: boolean
          description: 'Present and true on colors that may be null, which means "use the global text
            color". 0 is black.

            '
        options:
          type: array
          description: '`select`: the accepted values, numbers or strings. Send a value with its JSON
            type.

            '
          items:
            type:
            - string
            - number
        default:
          $ref: '#/components/schemas/BuiltinAppConfigValue'
          description: The factory value.
        value:
          $ref: '#/components/schemas/BuiltinAppConfigValue'
          description: The value it holds now.
    BuiltinAppConfigValue:
      description: 'A `bool` is a boolean, a `select` value has the JSON type of its options, a `color`
        is a number 0-16777215 or null where `nullable`, and `days` is an array of lowercase weekday names,
        Sunday first.

        '
      oneOf:
      - type: string
      - type: number
      - type: boolean
      - type: 'null'
      - type: array
        items:
          type: string
          enum:
          - sunday
          - monday
          - tuesday
          - wednesday
          - thursday
          - friday
          - saturday
    BuiltinAppConfigPatch:
      type: object
      description: 'The settings to change, each at its descriptor''s path. Only settings the app offers
        on this device are accepted.

        '
      additionalProperties:
        oneOf:
        - type: string
        - type: number
        - type: boolean
        - type: 'null'
        - $ref: '#/components/schemas/WeekdayBar'
      examples:
      - time24h: false
        weekdayBar:
          show: false
          weekendDays:
          - friday
          - saturday
      - timeColor: null
    AppConfigWriteResult:
      type: object
      required:
      - ok
      - name
      - error
      properties:
        ok:
          type: boolean
          const: true
        name:
          type: string
        error:
          description: 'Always null for a built-in app. For a script, null while it runs, or the error
            the restarted script stopped on: the settings were saved either way.

            '
          oneOf:
          - type: 'null'
          - $ref: '#/components/schemas/ScriptError'
    ScriptError:
      type: object
      description: The error a script or module stopped on.
      required:
      - message
      properties:
        message:
          type: string
          description: The error text, without the line number.
        line:
          type: integer
          minimum: 1
          description: Line in the script source, counting from 1. Omitted when unknown.
        hook:
          type: string
          description: 'The function that failed, such as `setup`, `draw` or `on_button`. Omitted when
            unknown. Never sent for a module.

            '
    ScriptConfig:
      type: object
      description: 'What `GET /api/v1/apps/{name}/config` answers: the settings a script declared in its
        header, each with the value it currently holds.

        '
      required:
      - name
      - fields
      - warnings
      properties:
        name:
          type: string
          description: The script's install name.
        fields:
          type: array
          maxItems: 12
          items:
            $ref: '#/components/schemas/ScriptConfigField'
        warnings:
          type: array
          description: '`@config` lines the device could not read, each prefixed with its line number.
            A malformed line is skipped, never fatal.

            '
          items:
            type: string
    ScriptConfigField:
      type: object
      description: 'One setting. `key` is what the script reads and what a `PATCH` sends. The optional
        members are present only when the declaration gave them.

        '
      required:
      - key
      - type
      - label
      - default
      - value
      properties:
        key:
          type: string
          pattern: ^[A-Za-z_][A-Za-z0-9_]{0,23}$
          description: The name the script reads its value under.
        type:
          type: string
          enum:
          - bool
          - text
          - number
          - slider
          - select
          - color
        label:
          type: string
          description: 'What to show, exactly as the script author wrote it. AWTRIX ships no translation
            for it: it is one string, in whatever language the author chose.

            '
        help:
          type: string
          description: One line of explanation under the label.
        unit:
          type: string
          description: '`number`/`slider`: shown after the control.'
        group:
          type: string
          description: 'Optional category heading from `group=` in the declaration, clipped to 48 bytes.
            Shown as written without translation. Fields with the same nonempty group share a collapsible
            section. Omitted or empty groups leave the field outside sections. Presentation only: stored
            values and PATCH keys are unchanged.

            '
        min:
          type: number
        max:
          type: number
        step:
          type: number
        maxlen:
          type: integer
          description: '`text`: longest value accepted, at most 256.'
        options:
          type: array
          maxItems: 12
          description: '`select`: the accepted values. A `PATCH` outside this list is a 422.'
          items:
            type: string
        default:
          description: 'What the header declared, so a client can offer a reset. A `color` is a number
            0-16777215. Every other type is the JSON type its name suggests.

            '
          oneOf:
          - type: string
          - type: number
          - type: boolean
        value:
          description: 'What the setting holds now: the default until somebody changes it. Same type as
            `default`.

            '
          oneOf:
          - type: string
          - type: number
          - type: boolean
    Error:
      type: object
      properties:
        error:
          type: object
          required:
          - code
          - message
          properties:
            code:
              type: string
              description: 'The error code. The list covers every clock: a code that belongs to a feature
                this clock does not have never comes back.

                '
              enum:
              - badRequest
              - invalidOrigin
              - storageError
              - invalidJson
              - invalidPath
              - invalidPinConfig
              - invalidName
              - invalidMethodOverride
              - invalidPlayer
              - wrongChip
              - invalidPackage
              - wrongTarget
              - unauthorized
              - forbidden
              - forbiddenOrigin
              - notFound
              - methodNotAllowed
              - unsupportedMediaType
              - payloadTooLarge
              - validationFailed
              - scriptChanged
              - gamepadsFull
              - scanUnavailable
              - notNewer
              - updateBusy
              - nameTaken
              - insufficientMemory
              - internalError
              - serviceBusy
              - unavailable
              - notSupported
              - insufficientStorage
            message:
              type: string
            field:
              type: string
    Color:
      description: '"#RRGGBB" (output). Input also accepts "RRGGBB", [r,g,b], ["HSV",h,s,v]'
      type:
      - string
      - array
      - integer
    NullableColor:
      description: 'Color or null (inherit/off). The null state is tracked separately from the color value,
        so every concrete color is representable: `#000000` stores and reads back as `#000000`, and `#FFFFFF`
        is a real value: only an explicit `null` means inherit/off.

        '
      type:
      - string
      - array
      - integer
      - 'null'
    IconGap:
      type: integer
      minimum: 0
      maximum: 128
      default: 1
      description: 'Empty columns between the right edge of the `icon` and the text, bars and line chart,
        counted from the icon''s own width. `0` puts the text right against the icon. Scrolling text is
        cut off at the far side of the gap. Anything but an integer from 0 to 128 rejects the request
        with 422 validationFailed and `field: "iconGap"`.

        '
    PlacedIcons:
      type: array
      maxItems: 4
      description: 'Additional independently animated icons at absolute pixel positions.

        Each GIF uses its own colors and frame timings. Available on pushed apps

        and notifications. Drawn in array order after the icon field, text and

        decorations, before overlays. Later icons cover earlier ones.

        They reserve no text columns and do not follow

        iconMode. Negative positions clip at the panel edge. Empty or omitted

        means no additional icons. Invalid entries or more than four icons

        reject the entire request with 422 validationFailed and an icons field

        path. Each icon uses its own image dimensions, bounded by the panel.

        '
      example:
      - icon: weather
        x: 0
        y: 0
      - icon: mail
        x: 16
        y: 0
      items:
        type: object
        additionalProperties: false
        required:
        - icon
        properties:
          icon:
            type: string
            minLength: 1
            allOf:
            - description: 'Icon ID up to 64 characters, the image as a data URL (data:image/gif.Base64,…
                or data:image/jpeg.Base64,…), or an http(s) address of a JPEG, PNG or GIF, up to 2048
                characters.

                '
          x:
            type: integer
            minimum: -65535
            maximum: 65535
            default: 0
          y:
            type: integer
            minimum: -65535
            maximum: 65535
            default: 0
    Scroll:
      description: 'Text motion. The same object is accepted in a pushed-app payload, a notification payload
        and the device settings. In the settings every field is concrete, in a payload every field is
        optional and each omitted one falls back to the device default **on its own**, so `{"mode":"bounce"}`
        keeps the configured speed. The string shorthand `"bounce"` is exactly `{"mode":"bounce"}`. Values
        are resolved when the page is drawn, so changing a device default also moves apps that are already
        pushed. An unknown field, an unknown enum value or a negative number is rejected with `422 validationFailed`
        and the offending key in `field` (for example `"scroll.speed"`), on every route that takes a `scroll`.
        Nothing is stored when it trips.

        '
      oneOf:
      - type: string
        enum:
        - static
        - wrap
        - loop
        - bounce
        description: 'Shorthand for `{"mode": ...}`.'
      - type: object
        additionalProperties: false
        properties:
          mode:
            type: string
            enum:
            - static
            - wrap
            - loop
            - bounce
            default: wrap
            description: 'static = no motion, overflow clipped. Wrap = run off the far edge, then restart
              at the start anchor. Loop = continuous marquee with no empty seam. Bounce = sweep back and
              forth, holding at both turning points.

              '
          direction:
            type: string
            enum:
            - left
            - right
            default: left
            description: 'Travel direction. `right` mirrors the whole geometry (the rest, entry and exit
              anchors swap ends) rather than only negating the velocity.

              '
          entry:
            type: string
            enum:
            - inline
            - offscreen
            default: inline
            description: '`offscreen` starts the text outside the panel and skips the initial hold. Ignored
              when `mode` is `static`.

              '
          whenFits:
            type: string
            enum:
            - static
            - scroll
            default: static
            description: Whether text that already fits the panel still animates.
          speed:
            type: integer
            minimum: 0
            default: 100
            description: 'Percent of the 21 px/s base rate. `0` freezes the text. There is no upper clamp.
              `200` is the sharpest a scroll gets on an 8 px panel.

              '
          gap:
            type: integer
            minimum: 0
            default: 8
            description: '`loop` only: pixels between repetitions.'
          holdMs:
            type: integer
            minimum: 0
            default: 1000
            description: 'How long the text rests before it starts moving, and at each `bounce` turning
              point. `0` removes the pause.

              '
    NativeLayout:
      type: object
      additionalProperties: false
      required:
      - version
      - regions
      description: 'A layout made of regions. Boxes, fonts and images are checked against the active display.
        Content outside a region is clipped. An invalid update keeps the previous content. Limits are
        in capabilities.layouts.

        '
      properties:
        version:
          type: integer
          enum:
          - 1
        backgroundColor:
          allOf:
          - $ref: '#/components/schemas/Color'
          default: '#000000'
        effect:
          type: string
          minLength: 1
          maxLength: 32
          description: 'Background effect from capabilities.effects, drawn instead of backgroundColor.
            Cannot be combined with backgroundColor.

            '
        effectSpeed:
          type: number
          minimum: 0.1
          maximum: 10
          default: 1
          description: Needs `effect`.
        overlay:
          type: string
          minLength: 1
          maxLength: 32
          description: 'Overlay from capabilities.overlays, drawn on top of all regions instead of the
            global overlay.

            '
        palette:
          $ref: '#/components/schemas/NativePalette'
          description: 'Used by the effect, the overlay and every region whose color is "palette" and
            that has no palette of its own.

            '
        paletteBlend:
          type: boolean
          default: true
          description: Needs `palette`.
        paletteSpan:
          type: integer
          minimum: 0
          maximum: 65535
          default: 0
          description: Needs `palette`.
        paletteSpeed:
          type: number
          minimum: 0
          maximum: 10
          default: 0
          description: Needs `palette`.
        regions:
          type: array
          minItems: 1
          maxItems: 16
          items:
            $ref: '#/components/schemas/NativeRegion'
    NativeRegion:
      type: object
      additionalProperties: false
      description: 'Exactly one content field. The device enforces string limits in UTF-8 bytes, so multibyte
        strings may reach the byte limit before maxLength. Text/image alignment defaults to center. Alignment
        is accepted but unused for chart/progress. Image color is accepted but unused.

        '
      required:
      - id
      - box
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 64
          description: Unique ID. At most 64 UTF-8 bytes.
        box:
          type: array
          minItems: 4
          maxItems: 4
          prefixItems:
          - type: integer
            minimum: 0
            maximum: 32767
          - type: integer
            minimum: 0
            maximum: 32767
          - type: integer
            minimum: 1
            maximum: 32767
          - type: integer
            minimum: 1
            maximum: 32767
          items: false
          description: '[x,y,width,height] in native pixels. Must fit entirely inside the active display.'
        text:
          description: 'Up to 8192 UTF-8 bytes summed across the layout. A list of fragments colors each
            part on its own. A fragment without color uses the region color.

            '
          oneOf:
          - type: string
            maxLength: 8192
          - type: array
            minItems: 1
            items:
              type: object
              additionalProperties: false
              required:
              - text
              properties:
                text:
                  type: string
                color:
                  $ref: '#/components/schemas/Color'
        textCase:
          type: string
          enum:
          - inherit
          - upper
          - asTyped
          default: inherit
          description: '`inherit` follows the uppercase setting.'
        textBlinkMs:
          type: integer
          minimum: 0
          maximum: 1000000
          description: Blink period of text.
        textFadeMs:
          type: integer
          minimum: 0
          maximum: 1000000
          description: Fade period of text. Wins over textBlinkMs.
        draw:
          type: array
          minItems: 1
          description: 'Draw commands as in the pushed-app `draw` field. Coordinates count from the top-left
            corner of the box, and everything outside the box is cut off. A command without color uses
            the region color.

            '
          items:
            type: array
        icon:
          type: string
          minLength: 1
          maxLength: 8192
          description: 'Icon ID (up to 64 characters), the image as a data URL (data:image/gif.Base64,…
            or data:image/jpeg.Base64,…), or an http(s) address. At most 8192 bytes. Native dimensions
            must fit the active display. The region may clip them. A picture from an address fills the
            region.

            '
        font:
          type: string
          minLength: 1
          maxLength: 96
          default: small
          description: Available name from capabilities.fonts. At most 96 UTF-8 bytes. Used by text and
            draw.
        color:
          description: 'A color, or "palette" for text, chart and progress: the color then comes from
            the region''s palette, or else the layout''s.

            '
          type:
          - string
          - array
          - integer
        textColor:
          description: The same field as `color`, under its pushed-app name. Set only one of them.
          type:
          - string
          - array
          - integer
        palette:
          $ref: '#/components/schemas/NativePalette'
        paletteBlend:
          type: boolean
          default: true
          description: Needs `palette`.
        paletteSpan:
          type: integer
          minimum: 0
          maximum: 65535
          default: 0
          description: Needs `palette`.
        paletteSpeed:
          type: number
          minimum: 0
          maximum: 10
          default: 0
          description: Needs `palette`.
        trackColor:
          allOf:
          - $ref: '#/components/schemas/Color'
          default: '#202020'
        align:
          type: string
          enum:
          - start
          - center
          - end
          default: center
        valign:
          type: string
          enum:
          - start
          - center
          - end
          default: center
        scroll:
          $ref: '#/components/schemas/NativeScroll'
        repeat:
          type: integer
          minimum: 0
          maximum: 1000000
          description: Omitted inherits outer repeat. Zero does not hold the page for this region.
        progress:
          type: integer
          minimum: 0
          maximum: 100
        chart:
          type: object
          additionalProperties: false
          required:
          - values
          description: Omit both bounds to autoscale including zero. Otherwise min must be less than max.
          dependentRequired:
            min:
            - max
            max:
            - min
          properties:
            values:
              type: array
              minItems: 1
              maxItems: 128
              items:
                type: integer
                minimum: -1000000000
                maximum: 1000000000
            type:
              type: string
              enum:
              - line
              - bar
              default: line
            min:
              type: integer
              minimum: -1000000000
              maximum: 1000000000
            max:
              type: integer
              minimum: -1000000000
              maximum: 1000000000
      oneOf:
      - required:
        - text
      - required:
        - icon
      - required:
        - chart
      - required:
        - progress
      - required:
        - draw
      allOf:
      - if:
          anyOf:
          - required:
            - scroll
          - required:
            - repeat
          - required:
            - textCase
          - required:
            - textBlinkMs
          - required:
            - textFadeMs
        then:
          required:
          - text
      - if:
          required:
          - font
        then:
          anyOf:
          - required:
            - text
          - required:
            - draw
      - if:
          required:
          - trackColor
        then:
          required:
          - progress
    NativePalette:
      description: 'A palette name from capabilities.palettes or a stored palette file, a list of up to
        16 colors, or a list of up to 16 {color, pos} stops with pos 0 to 100.

        '
      oneOf:
      - type: string
        minLength: 1
      - type: array
        minItems: 1
        maxItems: 16
    NativeScroll:
      description: 'Native text motion, measured by elapsed time. Every omitted field and null inherit
        the corresponding device default. Speed 100 is about 20.83 pixels/second. Explicit counts outside
        the bounds are rejected. Inherited counts are clamped to these bounds without changing settings.

        '
      oneOf:
      - type: 'null'
      - type: string
        enum:
        - static
        - wrap
        - loop
        - bounce
      - type: object
        additionalProperties: false
        properties:
          mode:
            type: string
            enum:
            - static
            - wrap
            - loop
            - bounce
          direction:
            type: string
            enum:
            - left
            - right
          entry:
            type: string
            enum:
            - inline
            - offscreen
          whenFits:
            type: string
            enum:
            - static
            - scroll
          speed:
            type: integer
            minimum: 0
            maximum: 1000000
          gap:
            type: integer
            minimum: 0
            maximum: 32767
          holdMs:
            type: integer
            minimum: 0
            maximum: 1000000
    WeekdayBar:
      description: 'The seven-segment weekday bar the clock draws. A column is colored on two independent
        axes (weekend or workday, today or not) so `activeColor` is today on a workday, `inactiveColor`
        another workday, `weekendActiveColor` today when today is a weekend day and `weekendInactiveColor`
        another weekend day. The two weekend colors **default to the workday colors**, so the bar looks
        the same until you set them. In the settings every field is concrete. A `PATCH` merges field by
        field, so `{"weekdayBar":{"weekendDays":["friday","saturday"]}}` leaves the other six untouched.
        An unknown field, a wrong type or an unknown weekday name is rejected with `422 validationFailed`
        and the offending key in `field` (for example `"weekdayBar.weekendDays"`). Nothing is applied
        when it trips.

        '
      type: object
      additionalProperties: false
      properties:
        show:
          type: boolean
          default: true
          description: 'Draw the bar. Where it sits depends on the clock layout.

            '
        startOnMonday:
          type: boolean
          default: true
          description: 'Monday is the first column. `false` starts the week on Sunday. This only rotates
            the display order: weekend membership is decided on the calendar day, so a Sunday-first week
            puts a Saturday/Sunday weekend on the first and the last column instead of the last two.

            '
        weekendDays:
          type: array
          default:
          - sunday
          - saturday
          description: 'Which calendar days count as weekend. Any subset in any order. An empty array
            means no weekend at all and every column uses the workday colors. In some countries, for example,
            the weekend is Friday and Saturday. Read back in calendar order, Sunday first.

            '
          items:
            type: string
            enum:
            - sunday
            - monday
            - tuesday
            - wednesday
            - thursday
            - friday
            - saturday
        activeColor:
          $ref: '#/components/schemas/Color'
          default: '#FFFFFF'
          description: Today, when today is a workday. Not nullable.
        inactiveColor:
          $ref: '#/components/schemas/Color'
          default: '#666666'
          description: Any other workday. Not nullable.
        weekendActiveColor:
          $ref: '#/components/schemas/Color'
          default: '#FFFFFF'
          description: Today, when today is a weekend day. Not nullable.
        weekendInactiveColor:
          $ref: '#/components/schemas/Color'
          default: '#666666'
          description: Any other weekend day. Not nullable.
    RadioStation:
      type: object
      required:
      - name
      - url
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 24
        url:
          type: string
          maxLength: 255
          description: http:// or https://
    RadioStations:
      type: object
      properties:
        stations:
          type: array
          maxItems: 32
          items:
            $ref: '#/components/schemas/RadioStation'
    MqttTls:
      type: object
      required:
      - ca
      - pending
      properties:
        ca:
          type: string
          enum:
          - public
          - uploaded
          - unusable
          description: 'public: public certificate authorities and mqttTlsPin count. Uploaded: only the
            uploaded CA. Unusable: the uploaded CA cannot be read, no broker is accepted until it is uploaded
            again or deleted.'
        pending:
          type:
          - string
          - 'null'
          pattern: ^[0-9a-f]{64}$
          description: SHA-256 of a refused broker certificate that can be trusted with mqttTlsPin.
    AudioPlay:
      description: 'A sound: a stored name (short for {"file": name}), one SoundObject, or a list of 1
        to 4 of them. A list plays its first entry the clock can play.

        '
      oneOf:
      - type: string
        description: 'A stored name, the same as {"file": name}'
      - $ref: '#/components/schemas/SoundObject'
      - type: array
        minItems: 1
        maxItems: 4
        items:
          oneOf:
          - type: string
          - $ref: '#/components/schemas/SoundObject'
    SoundObject:
      type: object
      description: Exactly one source key, plus `loop`.
      additionalProperties: false
      properties:
        file:
          type: string
          allOf:
          - description: A stored name (/MP3/<name>.mp3, else /MELODIES/<name>.txt), "Script/name" for
              a script's own sound, or an http(s):// address downloaded and played (up to 4 MB). A failed
              download is reported afterwards in alert.error on GET /api/v1/audio. Names are 1 to 32 of
              [A-Za-z0-9_-].
        rtttl:
          type: string
          maxLength: 512
          description: An RTTTL melody
        song:
          type: string
          description: Song text for the synthesizer. Plays once, or repeats with loop
        speech:
          type: string
          minLength: 1
          description: Text read aloud once a voice is installed, 1 to 512 bytes of UTF-8
        nextBar:
          type: boolean
          description: 'Scripts only, with a looping song: starts it at the next bar line of the playing
            song'
        station:
          description: 'Internet radio, at volume × radioVolume: a name from the stored list, a position
            in it (from 0), or a stream address. A playlist address (.m3u, .pls) plays its first entry.
            A 200 means tuning started. A stream that turns out to be unplayable is reported in radio.error.
            Not in a list and not in a notification.'
          oneOf:
          - type: string
          - type: integer
            minimum: 0
        loop:
          type: boolean
          default: false
          description: Repeat until stopped or replaced by a new alert. In a notification, while it is
            shown
      oneOf:
      - required:
        - file
      - required:
        - rtttl
      - required:
        - song
      - required:
        - speech
      - required:
        - station
    AudioStop:
      type: object
      additionalProperties: false
      properties:
        group:
          type: string
          enum:
          - alert
          - app
          - radio
          description: The group to stop. Without it, everything stops.
    AudioGroupState:
      type: object
      properties:
        playing:
          type: boolean
        name:
          type: string
          description: 'What the group played last: the `file` value as sent, else the source key'
        error:
          type: string
          description: Why the last sound did not play. Cleared by the next sound of the group
    AudioState:
      type: object
      properties:
        radio:
          type: object
          properties:
            playing:
              type: boolean
            station:
              type: string
              description: The station name
              or the address of a stream played by address: null
            title:
              type: string
              description: Last track title from the stream, UTF-8
            error:
              type: string
              description: Why playback stopped. Cleared on the next successful play
            underruns:
              type: integer
              description: Audible dropouts since boot. Read it as a difference across two polls
            decodeUs:
              type: integer
              description: Average time in microseconds to decode one piece of audio
            starvedMs:
              type: integer
              description: Milliseconds the player spent waiting for stream data. A high value points
                to a slow network rather than slow decoding. Counts from boot like underruns. It does
                not reset per station.
            bufferBytes:
              type: integer
              description: Bytes received from the stream and not played yet
        app:
          allOf:
          - $ref: '#/components/schemas/AudioGroupState'
          description: What scripts play.
        alert:
          allOf:
          - $ref: '#/components/schemas/AudioGroupState'
          description: What notifications, POST /api/v1/audio/play and the other alerts play.
        stations:
          type: array
          items:
            $ref: '#/components/schemas/RadioStation'
    Settings:
      type: object
      description: 'The saved preferences of this device. A key it does not have fails validation on a
        direct write. A backup restore skips such keys and restores the remaining preferences.

        '
      properties:
        autoBrightness:
          type: boolean
          description: Follow the light sensor. No effect without one (capabilities.sensors.light)
        brightness:
          type: integer
          minimum: 0
          maximum: 255
        autoTransition:
          type: boolean
        textColor:
          $ref: '#/components/schemas/Color'
        transitionEffect:
          type: string
          description: One of capabilities.transitions (for example "Slide", "Random")
        transitionDirection:
          type: string
          enum:
          - normal
          - reverse
          default: normal
          description: 'Reverses the movement of direction-aware transitions without changing app order.
            Symmetric transitions are unaffected.

            '
        transitionDurationMs:
          type: integer
          minimum: 0
        appDurationMs:
          type: integer
          minimum: 0
        timeMode:
          type: integer
          minimum: 0
          maximum: 6
          description: Clock layout 0-6. A higher value is rejected with 422.
        clockFace:
          type: string
          enum:
          - sheet
          - ring
          - flap
          - month
          - big
          description: The face of the clock.
        calendarHeaderColor:
          $ref: '#/components/schemas/Color'
        calendarTextColor:
          $ref: '#/components/schemas/Color'
        calendarBodyColor:
          $ref: '#/components/schemas/Color'
        calendarAnimation:
          type: boolean
          description: The sheet tears off when the clock appears and at midnight.
        time24h:
          type: boolean
          description: false = 12-hour clock
        timeLeadingZero:
          type: boolean
          description: 07:05 instead of 7:05
        timeShowSeconds:
          type: boolean
          description: only clock layouts with room for the seconds show them
        timeShowAmPm:
          type: boolean
          description: 12-hour clock only. Dropped while seconds are shown
        timeSeparatorMode:
          type: string
          enum:
          - steady
          - blink
          - pulse
          description: How the clock's colon behaves (blink = hard 1 s toggle, pulse = soft fade)
        dateOrder:
          type: string
          enum:
          - dayMonthYear
          - monthDayYear
          - yearMonthDay
        dateSeparator:
          type: string
          enum:
          - dot
          - slash
          - dash
          description: Ignored when dateMonthNames is set
        dateYearMode:
          type: string
          enum:
          - none
          - twoDigit
          - fourDigit
        dateShowWeekday:
          type: boolean
          description: short weekday name before the date
        dateMonthNames:
          type: boolean
          description: '"31 Dec" instead of "31.12"'
        useCelsius:
          type: boolean
        blockNavigation:
          type: boolean
        bootSound:
          type: boolean
          description: The sound at power-on, played at the alert volume.
        uppercase:
          type: boolean
        enlargeApps:
          type: boolean
          description: Pushed apps and notifications without a layout are drawn at double size, unless
            an icon is bigger than 26×8.
        weekdayBar:
          $ref: '#/components/schemas/WeekdayBar'
          description: 'The clock''s weekday bar. A `PATCH` with it sets the Date app''s bar too. Every
            field is concrete here. A `PATCH` may carry any subset of them.

            '
        dateWeekdayBar:
          $ref: '#/components/schemas/WeekdayBar'
          description: 'The Date app''s weekday bar. A `PATCH` with it sets only this bar, also when `weekdayBar`
            is in the same request.

            '
        timeColor:
          $ref: '#/components/schemas/NullableColor'
        dateColor:
          $ref: '#/components/schemas/NullableColor'
        humidityColor:
          $ref: '#/components/schemas/NullableColor'
        temperatureColor:
          $ref: '#/components/schemas/NullableColor'
        batteryColor:
          $ref: '#/components/schemas/NullableColor'
        scroll:
          $ref: '#/components/schemas/Scroll'
          description: 'Device-wide text motion. Every field is concrete here. A payload''s own `scroll`
            overrides it field by field.

            '
        volume:
          type: integer
          minimum: 0
          maximum: 100
          allOf:
          - description: Master volume, default 90. Every group plays at volume × group volume / 100.
              The knob sets it
        radioVolume:
          type: integer
          minimum: 0
          maximum: 100
          description: The radio group's share of the master volume, default 80
        appVolume:
          type: integer
          minimum: 0
          maximum: 100
          description: The app group (everything a script plays), share of the master volume, default
            100
        alertVolume:
          type: integer
          minimum: 0
          maximum: 100
          description: The alert group (notification sounds, POST /api/v1/audio/play and the other alerts),
            share of the master volume, default 100
        musicSource:
          type: string
          enum:
          - auto
          - playback
          - microphone
          default: auto
          description: What music visualizers and music.pitch() react to. playback is what the speaker
            plays, microphone what the microphone hears, auto the speaker while something plays and otherwise
            the microphone.
        saturation:
          type: integer
          minimum: 0
          maximum: 100
          description: Color saturation of the whole panel in percent. 100 leaves colors untouched, 0
            shows grays
        gamma:
          type: number
          exclusiveMinimum: 0
        colorCorrection:
          $ref: '#/components/schemas/NullableColor'
          description: 'RGB output correction. `null` = correction off. `#FFFFFF` is a settable value.

            '
        colorTint:
          $ref: '#/components/schemas/NullableColor'
          description: 'RGB output tint. `null` = off. `#FFFFFF` is a settable value.

            '
    SystemConfig:
      type: object
      description: 'Device configuration. The JSON key is the field name for

        every one. Unknown keys are silently ignored on PUT.

        '
      properties:
        wifiSsid:
          type: string
          default: ''
        wifiPass:
          type: string
          default: ''
          writeOnly: true
          description: 'Secret: omitted from GET. An empty string is ignored on PUT.'
        netStatic:
          type: boolean
          default: false
          description: static IP instead of DHCP
        ip:
          type: string
          default: ''
          description: Dotted quad, optionally with a CIDR suffix (192.168.1.50/24) that is split and
            stored as ip + subnet.
        gateway:
          type: string
          default: ''
        subnet:
          type: string
          default: ''
        dns1:
          type: string
          default: ''
        dns2:
          type: string
          default: ''
        mqttEnabled:
          type: boolean
          default: false
          description: 'Master switch for the MQTT client. Runs only while true. False keeps the settings
            below but never connects. Setting it true requires a non-empty mqttHost, else 422 validationFailed.

            '
        mqttHost:
          type: string
          default: ''
        mqttPort:
          type: integer
          default: 1883
          minimum: 1
          maximum: 65535
        mqttUser:
          type: string
          default: ''
        mqttPass:
          type: string
          default: ''
          writeOnly: true
          description: 'Secret: omitted from GET. An empty string is ignored on PUT.'
        mqttPrefix:
          type: string
          default: ''
          description: empty falls back to the device uid
        mqttTls:
          type: boolean
          default: false
          description: 'Connect over TLS. The broker is accepted when its certificate chains to a public
            certificate authority and names mqttHost, or matches mqttTlsPin. An uploaded broker CA (PUT
            /api/v1/mqtt/tls/ca) replaces both. Applies after a restart.

            '
        mqttTlsPin:
          type: string
          default: ''
          pattern: ^([0-9a-f]{64})?$
          description: 'SHA-256 of the trusted broker certificate, as GET /api/v1/mqtt/tls reports it
            pending. Empty trusts none. Applies at the next connection attempt.

            '
        haDiscovery:
          type: boolean
          default: false
          description: Home Assistant auto-discovery
        haPrefix:
          type: string
          default: homeassistant
        ntpServer:
          type: string
          default: pool.ntp.org
          description: Applied immediately. The clock syncs again with the new server.
        tz:
          type: string
          default: CET-1CEST,M3.5.0,M10.5.0/3
          description: POSIX TZ string, daylight-saving rules included. Applied immediately, no reboot.
        tzName:
          type: string
          default: Europe/Berlin
          description: IANA zone tz was picked from. Only the web UI uses it, to show the chosen city.
        hostname:
          type: string
          default: ''
          description: empty becomes awtrixng-<uid>
        authEnabled:
          type: boolean
          default: false
          description: 'Master switch for HTTP Basic auth over the whole API. Setting it true requires
            a non-empty authUser AND authPass, else 422 validationFailed. False keeps the stored credentials
            but serves the API without a login.

            '
        authUser:
          type: string
          default: ''
          description: HTTP Basic username. Gated by authEnabled, not by emptiness.
        authPass:
          type: string
          default: ''
          writeOnly: true
          description: 'Secret: omitted from GET. An empty string is ignored on PUT.'
        lowBatteryThreshold:
          type: integer
          default: 0
          minimum: 0
          maximum: 100
          description: 'Percent. When batteryPercent drops below it, GET /api/v1/device reports lowBattery=true
            (and a Home Assistant "Low battery" binary_sensor trips). 0 = off.

            '
        swapButtons:
          type: boolean
          default: false
        buttonCallback:
          type: string
          default: ''
          allOf:
          - description: HTTP webhook URL called on every button press and release, and when the knob
              is used
        mirrorShare:
          type: boolean
          default: false
          description: 'Let other clocks with the same panel size show this display (UDP 4212). Any device
            on the network can watch. Applies at once.

            '
        mirrorShareApps:
          type: string
          default: '*'
          description: 'Apps to share, comma separated and matched without regard to case. "*" shares
            every app, an empty string none.

            '
        mirrorShareNotifications:
          type: boolean
          default: true
          description: Share notifications too.
        mirrorFrom:
          type: string
          default: ''
          description: 'IP address or host name of the clock whose display this clock shows. Empty mirrors
            nothing. Applies at once.

            '
        mirrorFromApps:
          type: string
          default: '*'
          description: 'Apps of that clock to show, comma separated and matched without regard to case.
            "*" shows every app, an empty string none.

            '
        mirrorFromNotifications:
          type: boolean
          default: true
          description: Show that clock's notifications too.
        statsInterval:
          type: integer
          default: 10000
          minimum: 1000
          maximum: 600000
          description: milliseconds
        debugMode:
          type: boolean
          default: false
        scriptingEnabled:
          type: boolean
          default: true
          description: 'Whether Berry scripts can run at all. Off: no script runs, and script updates
            and script settings answer 503. Installed scripts stay stored. Applies after a reboot.

            '
    SystemConfigRead:
      description: 'The configuration as returned by GET/PUT: identical to SystemConfig minus the three
        secrets, which are never read back.

        '
      allOf:
      - $ref: '#/components/schemas/SystemConfig'
      not:
        anyOf:
        - required:
          - wifiPass
        - required:
          - mqttPass
        - required:
          - authPass
    EffectSettings:
      type: object
      description: 'Tuning shared by background effects and weather overlays. Every field is

        optional. An omitted field leaves the effect''s own choice in place.

        '
      properties:
        speed:
          type: number
          minimum: 0.1
          maximum: 10
          default: 1
          description: 'Multiplier on the effect''s own base speed. A value out of range is

            clamped, not rejected.

            '
        palette:
          oneOf:
          - type: string
          - type: array
            maxItems: 16
          - type: 'null'
          description: 'Palette name (a /PALETTES file first, then a built-in) or up to 16 color stops.
            A stop may be a bare color, spread evenly, or {"color": <color>, "pos": 0-100} to place it
            at a percentage of the ramp. The two forms cannot be mixed in one array.

            '
        blend:
          type: boolean
          default: true
          description: 'Interpolate between palette entries instead of 16 hard bands. Inert

            unless `palette` is supplied too.

            '
    Display:
      type: object
      properties:
        power:
          type: boolean
        brightness:
          type: integer
          description: effective brightness after auto-brightness
        overlay:
          type:
          - string
          - 'null'
        overlaySettings:
          allOf:
          - $ref: '#/components/schemas/EffectSettings'
          description: 'Always reported. `speed` falls back to 1 and `palette` to null when

            they were never set.

            '
        moodlight:
          type:
          - object
          - 'null'
          properties:
            color:
              type: string
            brightness:
              type: integer
    DeviceState:
      type: object
      properties:
        version:
          type: string
        uid:
          type: string
        boardType:
          type: string
          description: Board identity.
          allOf:
          - const: tc002
        soc:
          type: string
          allOf:
          - description: The processor type, for example armv7l.
        updateImage:
          type: string
          allOf:
          - const: awtrix-ng-tc002.awup
          description: 'The release file this device updates from: the one `POST /update` accepts. The
            web UI uses it to offer the right download.

            '
        ipAddress:
          type: string
        macAddress:
          type: string
          example: A4:CF:12:0B:3C:7D
          description: The Wi-Fi MAC address the clock joins networks with, in upper case with colons.
        hostname:
          type: string
          description: 'The name AWTRIX actually answers to on the network and over mDNS. When `hostname`
            in `/api/v1/system` is empty this is the name derived from the MAC address, so the two differ
            by design.

            '
        wifiRssi:
          type: integer
        uptimeSeconds:
          type: integer
        resetReason:
          type: string
          description: Boot or runtime-start reason. Values depend on the platform.
        freeHeapBytes:
          type: integer
          allOf:
          - description: Available system memory.
        minFreeHeapBytes:
          type: integer
          description: 'The lowest free RAM seen since boot. It only goes down, so it also

            catches a short dip between two polls. Watch it to spot a slow

            leak. Resets on reboot.

            '
        scriptingRunning:
          type: boolean
          description: 'Whether scripts are running at all. False when scriptingEnabled is off: installed
            scripts stay listed and editable, none of them runs.

            '
        scriptHeapPool:
          type: string
          description: Which memory scripts run in.
          allOf:
          - const: system
        scriptHeapBudgetBytes:
          type: integer
          allOf:
          - description: How much memory all scripts together may use before further installs are refused.
              A quarter of the memory available at start, at most 4 MiB.
        fps:
          type: integer
          description: measured display frames per second
        brightness:
          type: integer
          description: effective brightness after auto-brightness
        batteryPercent:
          type: integer
          description: Battery charge in percent. Present only with a battery.
        batteryVoltage:
          type: number
          description: Battery voltage in volts, rounded to two decimals. Present only with battery support.
        lowBattery:
          type: boolean
          description: 'True when batteryPercent has dropped below the configured lowBatteryThreshold.
            Always false when the threshold is 0 (off).

            '
        usbPower:
          type: boolean
          description: 'True while the clock is on USB power and charges its battery. Present once the
            clock has reported its power supply.

            '
        matrixPower:
          type: boolean
        currentApp:
          type: string
        indicators:
          type: array
          items:
            type: object
            properties:
              'on':
                type: boolean
              color:
                type: string
              blinkMs:
                type: integer
              fadeMs:
                type: integer
        messageCount:
          type: integer
          description: MQTT commands received since boot (HTTP does not count)
        wifi:
          type: object
          description: 'State of the WiFi station connection, in the same shape as `mqtt`. `host` is the
            network name (SSID) and `endpoint` the address AWTRIX holds on it. `state` is "disabled" when
            no network is stored.

            '
          properties:
            enabled:
              type: boolean
              description: A network name is stored.
            state:
              type: string
              enum:
              - disabled
              - offline
              - connecting
              - connected
            host:
              type: string
              description: The network name (SSID) AWTRIX joined.
            endpoint:
              type: string
              description: 'The address AWTRIX holds on that network. Empty string while not joined.

                '
            attempts:
              type: integer
              description: Consecutive failed attempts. Zero while connected.
            retryInMs:
              type: integer
              description: Milliseconds until the next attempt. Zero while connected.
            connects:
              type: integer
              description: 'Successful associations since boot. A number that keeps climbing is an access
                point that keeps dropping the device.

                '
            error:
              type:
              - string
              - 'null'
              description: Why the connection is not up right now. Null when it is.
              enum:
              - hostNotFound
              - badCredentials
              - timeout
              - lost
              - null
            lastError:
              type:
              - string
              - 'null'
              description: 'The last reason this link went down, kept after it recovers. While WiFi is
                down this API is unreachable, so this is what you read afterwards. Null until something
                goes wrong.

                '
              enum:
              - hostNotFound
              - badCredentials
              - timeout
              - lost
              - null
        mqtt:
          type: object
          description: 'State of the broker connection. Present whether or not MQTT is enabled. `state`
            is "disabled" when it is off.

            '
          properties:
            enabled:
              type: boolean
              description: Mirrors mqttEnabled in the system configuration.
            state:
              type: string
              enum:
              - disabled
              - offline
              - connecting
              - connected
            host:
              type: string
              description: The configured broker host, as entered.
            endpoint:
              type: string
              description: 'The address and port actually in use, once the host has been resolved. Empty
                string before that.

                '
            attempts:
              type: integer
              description: Consecutive failed attempts. Zero while connected.
            retryInMs:
              type: integer
              description: 'Milliseconds until the next attempt. Zero while connected, and while an attempt
                is in progress.

                '
            connects:
              type: integer
              description: Successful connections since boot.
            error:
              type:
              - string
              - 'null'
              description: Why the connection is not up right now. Null when it is.
              enum:
              - noWifi
              - hostNotFound
              - refused
              - badCredentials
              - rejected
              - timeout
              - lost
              - null
            lastError:
              type:
              - string
              - 'null'
              description: 'The last reason this link went down, kept after it recovers. Null until something
                goes wrong.

                '
              enum:
              - noWifi
              - hostNotFound
              - refused
              - badCredentials
              - rejected
              - timeout
              - lost
              - null
        mirror:
          type: object
          description: What display mirroring is doing.
          properties:
            sharing:
              type: boolean
              description: This clock shares its display and is on the network.
            viewers:
              type: integer
              description: Clocks watching this display right now.
            source:
              type: string
              description: The clock this clock mirrors
              as entered in mirrorFrom.: null
            state:
              type: string
              description: What mirroring the source clock does right now.
              enum:
              - 'off'
              - offline
              - resolving
              - notFound
              - waiting
              - idle
              - filtered
              - sizeMismatch
              - noMemory
              - showing
            sourceWidth:
              type: integer
              description: Panel width the source clock reported. Absent until it answered.
            sourceHeight:
              type: integer
              description: Panel height the source clock reported. Absent until it answered.
        update:
          type: object
          properties:
            state:
              type: string
            release:
              type: string
            error:
              type: string
          description: Update progress, most recently accepted release and error.
