{
  "asyncapi": "3.0.0",
  "id": "urn:com:wieoapps:samklang:server",
  "info": {
    "title": "Samklang Server API",
    "version": "1",
    "description": "Samklang Server is open: anyone may write their own client for it. Everything\nthe Samklang apps do, they do through this API: the iPhone app, the Mac app\nand the Mac's Samklang Server window are all just clients of it. You can\nbuild your own app, desktop UI or automation the same way.\n\nThe server makes every decision itself. A client shows the state it is\nsent and sends commands back; it never has to work anything out. (Even\ncalibration works like that: the app only records sound, and the server\ndoes the rest.)\n\n## Overview\n\n| | |\n|---|---|\n| Transport | TLS over TCP, port **7090** (`-remote-port`). Not HTTP |\n| Discovery | Bonjour / mDNS, `_samklang._tcp` |\n| Framing | one JSON object per line (`\\n`), both ways, UTF-8 |\n| Protocol version | `1` |\n| Security | a self-signed certificate you pin, pairing once (the first client of a new server needs no code; later ones a 4-digit code), then a token |\n\nA connection lives as long as you like. The server pushes the full state\nwhenever something changes (and at once when you connect); you send\ncommands whenever you like on the same connection.\n\n## Finding a server\n\nBrowse for `_samklang._tcp`. Each server advertises its name (the\ncomputer's name, or `-server-name`) and these TXT records:\n\n| Key | Example | Meaning |\n|---|---|---|\n| `v` | `1` | protocol version; skip servers with a version you don't know |\n| `id` | `3f2ac81e90b4d7c2` | the first 16 hex digits of the certificate's SHA-256 fingerprint |\n| `ver` | `0.9.3` | the server's own version |\n| `os` | `linux` | `darwin`, `linux` or `windows` |\n\n`id` stays the same for as long as the server keeps its settings folder,\nacross restarts and new IP addresses: use it to recognise a server you\nhave paired with.\n\n## Connecting\n\nOpen a TLS connection (TLS 1.2 or later) to the address and port from\nBonjour. The certificate is self-signed, so normal verification fails:\ncheck it yourself instead.\n\n- **First time:** accept the certificate, compute the SHA-256 of its DER\n  bytes, and check that the first 16 hex digits equal the advertised\n  `id`. Keep the whole fingerprint with the token you get from pairing.\n- **Every time after:** accept only a certificate with that fingerprint.\n\n## Pairing and hello\n\nThe first line you send decides who you are. You have 20 seconds to send\nit; a first line that isn't `pair` or `hello` closes the connection.\n\n**Pairing** (once). Try without a code first:\n\n```json\n{\"cmd\":\"pair\",\"code\":\"\",\"name\":\"Anna's tablet\"}\n```\n\nA new server that nobody has paired with takes the first client without a\ncode, during its first 30 minutes after starting: that client is its\nowner (a Raspberry Pi has no screen to show a code on). Starting the\nserver again opens those 30 minutes again, as long as nobody has paired.\n\nOtherwise the answer is `code_needed`, and the user types the server's\n4-digit code into your app. It is shown in any paired client\n([`pairing_code`](#cmd-pairing_code); Samklang shows it under\nSettings › Add a device), in the Mac's Samklang Server window, and in the\nlog (`remote control: pairing code 4719`):\n\n```json\n{\"cmd\":\"pair\",\"code\":\"4719\",\"name\":\"Anna's tablet\"}\n```\n\n```json\n{\"type\":\"paired\",\"token\":\"Vb3…\",\"server\":{\"id\":\"3f2ac81e90b4d7c2\",\"name\":\"Vardagsrum\",\"version\":\"0.9.3\",\"os\":\"linux\",\"proto\":\"1\"}}\n```\n\nStore `token`, which is secret: whoever has it controls the server. The\ncode changes after every successful pairing.\n\n**Hello** (every connection after):\n\n```json\n{\"cmd\":\"hello\",\"token\":\"Vb3…\"}\n```\n\n```json\n{\"type\":\"hello\",\"server\":{\"id\":\"3f2ac81e90b4d7c2\",\"name\":\"Vardagsrum\",\"version\":\"0.9.3\",\"os\":\"linux\",\"proto\":\"1\"}}\n```\n\nAn app on the **same computer** as the server (connecting to 127.0.0.1)\nmay send `{\"cmd\":\"hello\"}` without a token, unless the server runs with\n`-remote-local-pairing`.\n\n**Errors** end the connection:\n\n```json\n{\"type\":\"error\",\"error\":\"code_needed\",\"retry_after\":0}\n{\"type\":\"error\",\"error\":\"bad_code\",\"retry_after\":0}\n{\"type\":\"error\",\"error\":\"locked\",\"retry_after\":60}\n{\"type\":\"error\",\"error\":\"unknown_token\"}\n```\n\n- `code_needed`: the server is set up already; ask the user for its code.\n  Trying without a code never counts as a wrong guess.\n- `bad_code`: wrong code. Each wrong code takes a second to answer.\n- `locked`: five wrong codes lock pairing, for a minute at first and\n  doubling each time up to an hour, and the code changes. Try again after\n  `retry_after` seconds.\n- `unknown_token`: the token was unpaired on the server (or its settings\n  were reset). Pair again.\n\n## Commands and messages\n\nAfter the handshake, send commands one per line. Every command has a\n`cmd`, and every message from the server a `type`.\n\nMost commands get no reply: you see the result in the next `state`. A\ncommand the server can't carry out (or doesn't know) is written to its\nlog and otherwise ignored. A few commands are answered, to you only,\nwith the `ref` you sent (any string), so you can match the answer to the\nquestion: `pairing_code`, `clock`, `satellite_firmware` and the\ncalibration commands.\n\nCommands for a multiroom speaker act on the one playing (else\nEverywhere); name another with `\"group\"`. A device is always named by\nits `label`.\n\n## Behaviour to rely on\n\n- **Ignore what you don't know.** New fields, new values and new message\n  types may be added in protocol version 1; they never change the\n  meaning of the old ones. A breaking change gets a new `v`.\n- **The state is complete.** Each `state` replaces the last, and is only\n  sent when it changed.\n- **Read fast.** Each client has a queue of 64 messages. A client that\n  stops reading is disconnected; reconnect and you get the current state again.\n- **Restarts.** `settings` and `rename` restart the server, and so may an\n  update. Reconnect with a short back-off (the apps wait about a second,\n  then longer).\n- **Lines can be long.** A cover is a few hundred kB of base64 on one\n  line; a calibration recording up to a few MB the other way. Don't use a\n  small fixed line buffer.\n\n## A complete example\n\nA Python client that pairs, then prints what is playing and pauses it.\nNo packages needed.\n\n```python\nimport hashlib, json, socket, ssl, sys\n\nHOST, PORT = sys.argv[1], 7090            # from Bonjour (_samklang._tcp)\n\nctx = ssl.create_default_context()\nctx.check_hostname, ctx.verify_mode = False, ssl.CERT_NONE   # we pin instead\nconn = ctx.wrap_socket(socket.create_connection((HOST, PORT)))\nfingerprint = hashlib.sha256(conn.getpeercert(binary_form=True)).hexdigest()\nf = conn.makefile(\"rwb\")\n\ndef send(msg):\n    f.write(json.dumps(msg).encode() + b\"\\n\")\n    f.flush()\n\ndef receive():\n    return json.loads(f.readline())\n\ntry:\n    saved = json.load(open(\"samklang-token.json\"))\nexcept FileNotFoundError:\n    saved = None\n\nif saved:\n    assert saved[\"fingerprint\"] == fingerprint, \"not the server we paired with\"\n    send({\"cmd\": \"hello\", \"token\": saved[\"token\"]})\n    reply = receive()\nelse:\n    # A new server takes its first client without a code.\n    send({\"cmd\": \"pair\", \"code\": \"\", \"name\": \"My script\"})\n    reply = receive()\n    if reply.get(\"error\") == \"code_needed\":\n        conn = ctx.wrap_socket(socket.create_connection((HOST, PORT)))  # an error ends the connection\n        f = conn.makefile(\"rwb\")\n        send({\"cmd\": \"pair\", \"code\": input(\"Pairing code: \"), \"name\": \"My script\"})\n        reply = receive()\n\nif reply[\"type\"] == \"error\":\n    sys.exit(\"server says: \" + reply[\"error\"])\nif reply[\"type\"] == \"paired\":\n    json.dump({\"fingerprint\": fingerprint, \"token\": reply[\"token\"]}, open(\"samklang-token.json\", \"w\"))\nprint(\"connected to\", reply[\"server\"][\"name\"])\n\nwhile True:\n    msg = receive()\n    if msg[\"type\"] != \"state\":\n        continue\n    track = msg.get(\"track\")\n    print(\"playing:\", f'{track[\"artist\"]} – {track[\"title\"]}' if track else \"nothing\")\n    if msg[\"playing\"] and not msg[\"paused\"] and msg[\"can_control\"]:\n        send({\"cmd\": \"transport\", \"action\": \"playpause\"})\n    break\n```\n\nQuestions, or something the API should do for your client? Write to\nsupport@wieoapps.com.",
    "contact": {
      "name": "wieo AB",
      "email": "support@wieoapps.com",
      "url": "https://samklang.wieoapps.com/support.html"
    },
    "license": {
      "name": "This API description may be used freely to write clients for Samklang Server."
    },
    "tags": [
      {
        "$ref": "#/components/tags/Handshake"
      },
      {
        "$ref": "#/components/tags/State"
      },
      {
        "$ref": "#/components/tags/Playing"
      },
      {
        "$ref": "#/components/tags/Sync"
      },
      {
        "$ref": "#/components/tags/Devices"
      },
      {
        "$ref": "#/components/tags/Groups"
      },
      {
        "$ref": "#/components/tags/Settings"
      },
      {
        "$ref": "#/components/tags/Satellites"
      },
      {
        "$ref": "#/components/tags/Calibration"
      },
      {
        "$ref": "#/components/tags/Updates"
      },
      {
        "$ref": "#/components/tags/Localonly"
      }
    ],
    "externalDocs": {
      "description": "The Developers page, made from this file",
      "url": "https://samklang.wieoapps.com/api.html"
    }
  },
  "defaultContentType": "application/json",
  "servers": {
    "local": {
      "host": "{host}:7090",
      "protocol": "tcp",
      "title": "Samklang Server on your network",
      "description": "TLS (1.2 or later) over TCP, port 7090 (`-remote-port`), one JSON object per line\nboth ways. Find the server with Bonjour (mDNS): browse for `_samklang._tcp`; its TXT\nrecords are `v` (protocol version), `id` (the first 16 hex digits of the certificate's\nSHA-256 fingerprint), `ver` (the server's version) and `os`. The certificate is\nself-signed: pin its fingerprint.",
      "variables": {
        "host": {
          "default": "localhost",
          "description": "The server's address, from Bonjour."
        }
      }
    }
  },
  "channels": {
    "connection": {
      "title": "The connection",
      "summary": "One long-lived TLS connection: JSON lines both ways.",
      "description": "A connection starts with the handshake (`pair` or `hello`) and then lives as long as\nyou like: the server pushes messages, you send commands. It has no address.",
      "servers": [
        {
          "$ref": "#/servers/local"
        }
      ],
      "messages": {
        "cmd_pair": {
          "$ref": "#/components/messages/cmd_pair"
        },
        "cmd_hello": {
          "$ref": "#/components/messages/cmd_hello"
        },
        "cmd_pairing_code": {
          "$ref": "#/components/messages/cmd_pairing_code"
        },
        "msg_paired": {
          "$ref": "#/components/messages/msg_paired"
        },
        "msg_hello": {
          "$ref": "#/components/messages/msg_hello"
        },
        "msg_error": {
          "$ref": "#/components/messages/msg_error"
        },
        "msg_pairing_code": {
          "$ref": "#/components/messages/msg_pairing_code"
        },
        "msg_state": {
          "$ref": "#/components/messages/msg_state"
        },
        "msg_artwork": {
          "$ref": "#/components/messages/msg_artwork"
        },
        "msg_group_artwork": {
          "$ref": "#/components/messages/msg_group_artwork"
        },
        "cmd_transport": {
          "$ref": "#/components/messages/cmd_transport"
        },
        "cmd_volume": {
          "$ref": "#/components/messages/cmd_volume"
        },
        "cmd_level": {
          "$ref": "#/components/messages/cmd_level"
        },
        "cmd_resync": {
          "$ref": "#/components/messages/cmd_resync"
        },
        "cmd_auto_sync": {
          "$ref": "#/components/messages/cmd_auto_sync"
        },
        "cmd_set": {
          "$ref": "#/components/messages/cmd_set"
        },
        "cmd_clock": {
          "$ref": "#/components/messages/cmd_clock"
        },
        "msg_clock": {
          "$ref": "#/components/messages/msg_clock"
        },
        "cmd_include": {
          "$ref": "#/components/messages/cmd_include"
        },
        "cmd_show": {
          "$ref": "#/components/messages/cmd_show"
        },
        "cmd_rename": {
          "$ref": "#/components/messages/cmd_rename"
        },
        "cmd_channel": {
          "$ref": "#/components/messages/cmd_channel"
        },
        "cmd_seen_device": {
          "$ref": "#/components/messages/cmd_seen_device"
        },
        "cmd_forget_device": {
          "$ref": "#/components/messages/cmd_forget_device"
        },
        "cmd_save_group": {
          "$ref": "#/components/messages/cmd_save_group"
        },
        "cmd_delete_group": {
          "$ref": "#/components/messages/cmd_delete_group"
        },
        "cmd_group_member": {
          "$ref": "#/components/messages/cmd_group_member"
        },
        "cmd_google_groups": {
          "$ref": "#/components/messages/cmd_google_groups"
        },
        "cmd_settings": {
          "$ref": "#/components/messages/cmd_settings"
        },
        "cmd_satellite_scan": {
          "$ref": "#/components/messages/cmd_satellite_scan"
        },
        "cmd_satellite_use": {
          "$ref": "#/components/messages/cmd_satellite_use"
        },
        "cmd_satellite_quality": {
          "$ref": "#/components/messages/cmd_satellite_quality"
        },
        "cmd_satellite_board": {
          "$ref": "#/components/messages/cmd_satellite_board"
        },
        "cmd_satellite_update": {
          "$ref": "#/components/messages/cmd_satellite_update"
        },
        "cmd_satellite_firmware": {
          "$ref": "#/components/messages/cmd_satellite_firmware"
        },
        "msg_satellite_firmware": {
          "$ref": "#/components/messages/msg_satellite_firmware"
        },
        "cmd_calibrate": {
          "$ref": "#/components/messages/cmd_calibrate"
        },
        "cmd_chime": {
          "$ref": "#/components/messages/cmd_chime"
        },
        "cmd_heard": {
          "$ref": "#/components/messages/cmd_heard"
        },
        "cmd_calibration_skip": {
          "$ref": "#/components/messages/cmd_calibration_skip"
        },
        "cmd_calibration_reset": {
          "$ref": "#/components/messages/cmd_calibration_reset"
        },
        "cmd_calibration_result": {
          "$ref": "#/components/messages/cmd_calibration_result"
        },
        "cmd_calibration_apply": {
          "$ref": "#/components/messages/cmd_calibration_apply"
        },
        "cmd_calibration_note": {
          "$ref": "#/components/messages/cmd_calibration_note"
        },
        "msg_calibration": {
          "$ref": "#/components/messages/msg_calibration"
        },
        "msg_chime": {
          "$ref": "#/components/messages/msg_chime"
        },
        "msg_heard": {
          "$ref": "#/components/messages/msg_heard"
        },
        "msg_calibration_result": {
          "$ref": "#/components/messages/msg_calibration_result"
        },
        "msg_not_calibrating": {
          "$ref": "#/components/messages/msg_not_calibrating"
        },
        "cmd_forget_remote": {
          "$ref": "#/components/messages/cmd_forget_remote"
        },
        "msg_pairing": {
          "$ref": "#/components/messages/msg_pairing"
        },
        "msg_remotes": {
          "$ref": "#/components/messages/msg_remotes"
        }
      }
    }
  },
  "operations": {
    "receive_pair": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Pair",
      "summary": "The first line of a new client: pair, once, and get a token.",
      "tags": [
        {
          "$ref": "#/components/tags/Handshake"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_pair"
        }
      ],
      "reply": {
        "channel": {
          "$ref": "#/channels/connection"
        },
        "messages": [
          {
            "$ref": "#/channels/connection/messages/msg_paired"
          },
          {
            "$ref": "#/channels/connection/messages/msg_error"
          }
        ]
      }
    },
    "receive_hello": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Hello",
      "summary": "The first line of a paired client, every time after pairing.",
      "tags": [
        {
          "$ref": "#/components/tags/Handshake"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_hello"
        }
      ],
      "reply": {
        "channel": {
          "$ref": "#/channels/connection"
        },
        "messages": [
          {
            "$ref": "#/channels/connection/messages/msg_hello"
          },
          {
            "$ref": "#/channels/connection/messages/msg_error"
          }
        ]
      }
    },
    "receive_pairing_code": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Ask for the pairing code",
      "summary": "The code another device pairs with now.",
      "tags": [
        {
          "$ref": "#/components/tags/Handshake"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_pairing_code"
        }
      ],
      "reply": {
        "channel": {
          "$ref": "#/channels/connection"
        },
        "messages": [
          {
            "$ref": "#/channels/connection/messages/msg_pairing_code"
          }
        ]
      }
    },
    "receive_transport": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Play, pause, skip",
      "summary": "Play/pause, next or previous on the sender.",
      "tags": [
        {
          "$ref": "#/components/tags/Playing"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_transport"
        }
      ]
    },
    "receive_volume": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Volume",
      "summary": "The sender's volume.",
      "tags": [
        {
          "$ref": "#/components/tags/Playing"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_volume"
        }
      ]
    },
    "receive_level": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "A device's volume",
      "summary": "One device's volume relative to the sender's.",
      "tags": [
        {
          "$ref": "#/components/tags/Playing"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_level"
        }
      ]
    },
    "receive_resync": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Resync",
      "summary": "Bring one playing device back into sync now.",
      "tags": [
        {
          "$ref": "#/components/tags/Playing"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_resync"
        }
      ]
    },
    "receive_auto_sync": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Auto sync",
      "summary": "Automatic sync on or off.",
      "tags": [
        {
          "$ref": "#/components/tags/Sync"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_auto_sync"
        }
      ]
    },
    "receive_set": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Fine-tune or delay",
      "summary": "A device's fine-tune (under auto sync) or its delay.",
      "tags": [
        {
          "$ref": "#/components/tags/Sync"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_set"
        }
      ]
    },
    "receive_clock": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Clock",
      "summary": "Ask for the server's clock.",
      "tags": [
        {
          "$ref": "#/components/tags/Sync"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_clock"
        }
      ],
      "reply": {
        "channel": {
          "$ref": "#/channels/connection"
        },
        "messages": [
          {
            "$ref": "#/channels/connection/messages/msg_clock"
          }
        ]
      }
    },
    "receive_include": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Leave out of Everywhere",
      "summary": "Leave a device out of Everywhere, or take it back.",
      "tags": [
        {
          "$ref": "#/components/tags/Devices"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_include"
        }
      ]
    },
    "receive_show": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Show its AirPlay speaker",
      "summary": "Hide (or show) a device's own AirPlay speaker.",
      "tags": [
        {
          "$ref": "#/components/tags/Devices"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_show"
        }
      ]
    },
    "receive_rename": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Rename",
      "summary": "Rename a device in Samklang. The server restarts.",
      "tags": [
        {
          "$ref": "#/components/tags/Devices"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_rename"
        }
      ]
    },
    "receive_channel": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Stereo pair",
      "summary": "Play both channels on a device, or only the left or the right one.",
      "tags": [
        {
          "$ref": "#/components/tags/Devices"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_channel"
        }
      ]
    },
    "receive_seen_device": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Seen",
      "summary": "The user has seen a new device: it is no longer marked `new`.",
      "tags": [
        {
          "$ref": "#/components/tags/Devices"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_seen_device"
        }
      ]
    },
    "receive_forget_device": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Forget",
      "summary": "Drop an offline device from the list.",
      "tags": [
        {
          "$ref": "#/components/tags/Devices"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_forget_device"
        }
      ]
    },
    "receive_save_group": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Save a group",
      "summary": "Make a group, or change one.",
      "tags": [
        {
          "$ref": "#/components/tags/Groups"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_save_group"
        }
      ]
    },
    "receive_delete_group": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Delete a group",
      "summary": "Delete a group.",
      "tags": [
        {
          "$ref": "#/components/tags/Groups"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_delete_group"
        }
      ]
    },
    "receive_group_member": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Group member",
      "summary": "Put a device in a group, or take it out.",
      "tags": [
        {
          "$ref": "#/components/tags/Groups"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_group_member"
        }
      ]
    },
    "receive_google_groups": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Google Home groups",
      "summary": "Play a group's Chromecasts through Google Home groups.",
      "tags": [
        {
          "$ref": "#/components/tags/Groups"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_google_groups"
        }
      ]
    },
    "receive_settings": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Settings",
      "summary": "Change what the server looks for. The server restarts.",
      "tags": [
        {
          "$ref": "#/components/tags/Settings"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_settings"
        }
      ]
    },
    "receive_satellite_scan": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Look for speakers",
      "summary": "Look for Bluetooth speakers in pairing mode.",
      "tags": [
        {
          "$ref": "#/components/tags/Satellites"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_satellite_scan"
        }
      ]
    },
    "receive_satellite_use": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Use a speaker",
      "summary": "Play on that speaker from now on.",
      "tags": [
        {
          "$ref": "#/components/tags/Satellites"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_satellite_use"
        }
      ]
    },
    "receive_satellite_quality": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Sound quality",
      "summary": "Its speaker's sound. It connects to the speaker again.",
      "tags": [
        {
          "$ref": "#/components/tags/Satellites"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_satellite_quality"
        }
      ]
    },
    "receive_satellite_board": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Sound board",
      "summary": "Analog sound on, with that board, or off.",
      "tags": [
        {
          "$ref": "#/components/tags/Satellites"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_satellite_board"
        }
      ]
    },
    "receive_satellite_update": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Approve an update",
      "summary": "The user approves the satellite's waiting firmware update.",
      "tags": [
        {
          "$ref": "#/components/tags/Satellites"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_satellite_update"
        }
      ]
    },
    "receive_satellite_firmware": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Firmware for a new satellite",
      "summary": "The firmware to write to a Pico plugged into your computer.",
      "tags": [
        {
          "$ref": "#/components/tags/Satellites"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_satellite_firmware"
        }
      ],
      "reply": {
        "channel": {
          "$ref": "#/channels/connection"
        },
        "messages": [
          {
            "$ref": "#/channels/connection/messages/msg_satellite_firmware"
          }
        ]
      }
    },
    "receive_calibrate": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Calibrate",
      "summary": "Start or end a calibration of a group.",
      "tags": [
        {
          "$ref": "#/components/tags/Calibration"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_calibrate"
        }
      ],
      "reply": {
        "channel": {
          "$ref": "#/channels/connection"
        },
        "messages": [
          {
            "$ref": "#/channels/connection/messages/msg_calibration"
          }
        ]
      }
    },
    "receive_chime": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Chime",
      "summary": "Play the chime on one speaker.",
      "tags": [
        {
          "$ref": "#/components/tags/Calibration"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_chime"
        }
      ],
      "reply": {
        "channel": {
          "$ref": "#/channels/connection"
        },
        "messages": [
          {
            "$ref": "#/channels/connection/messages/msg_chime"
          }
        ]
      }
    },
    "receive_heard": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Heard",
      "summary": "Send the recording of a chime.",
      "tags": [
        {
          "$ref": "#/components/tags/Calibration"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_heard"
        }
      ],
      "reply": {
        "channel": {
          "$ref": "#/channels/connection"
        },
        "messages": [
          {
            "$ref": "#/channels/connection/messages/msg_heard"
          },
          {
            "$ref": "#/channels/connection/messages/msg_not_calibrating"
          }
        ]
      }
    },
    "receive_calibration_skip": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Skip a speaker",
      "summary": "Leave a speaker out of the result.",
      "tags": [
        {
          "$ref": "#/components/tags/Calibration"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_calibration_skip"
        }
      ],
      "reply": {
        "channel": {
          "$ref": "#/channels/connection"
        },
        "messages": [
          {
            "$ref": "#/channels/connection/messages/msg_not_calibrating"
          }
        ]
      }
    },
    "receive_calibration_reset": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Start over",
      "summary": "Forget what was measured so far.",
      "tags": [
        {
          "$ref": "#/components/tags/Calibration"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_calibration_reset"
        }
      ],
      "reply": {
        "channel": {
          "$ref": "#/channels/connection"
        },
        "messages": [
          {
            "$ref": "#/channels/connection/messages/msg_not_calibrating"
          }
        ]
      }
    },
    "receive_calibration_result": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Result",
      "summary": "See the result so far.",
      "tags": [
        {
          "$ref": "#/components/tags/Calibration"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_calibration_result"
        }
      ],
      "reply": {
        "channel": {
          "$ref": "#/channels/connection"
        },
        "messages": [
          {
            "$ref": "#/channels/connection/messages/msg_calibration_result"
          },
          {
            "$ref": "#/channels/connection/messages/msg_not_calibrating"
          }
        ]
      }
    },
    "receive_calibration_apply": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Apply",
      "summary": "Set the fine-tunes the calibration found.",
      "tags": [
        {
          "$ref": "#/components/tags/Calibration"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_calibration_apply"
        }
      ],
      "reply": {
        "channel": {
          "$ref": "#/channels/connection"
        },
        "messages": [
          {
            "$ref": "#/channels/connection/messages/msg_calibration_result"
          },
          {
            "$ref": "#/channels/connection/messages/msg_not_calibrating"
          }
        ]
      }
    },
    "receive_calibration_note": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Note",
      "summary": "What the app made of a chime, for the server's log.",
      "tags": [
        {
          "$ref": "#/components/tags/Calibration"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_calibration_note"
        }
      ]
    },
    "receive_forget_remote": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Unpair an app",
      "summary": "Unpair an app; its connections close.",
      "tags": [
        {
          "$ref": "#/components/tags/Localonly"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/cmd_forget_remote"
        }
      ]
    },
    "send_state": {
      "action": "send",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "State",
      "summary": "The whole picture: sent when you connect, and whenever it changes.",
      "tags": [
        {
          "$ref": "#/components/tags/State"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/msg_state"
        }
      ]
    },
    "send_artwork": {
      "action": "send",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Cover",
      "summary": "The cover of the track playing.",
      "tags": [
        {
          "$ref": "#/components/tags/State"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/msg_artwork"
        }
      ]
    },
    "send_group_artwork": {
      "action": "send",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "A group's cover",
      "summary": "The cover of one playing group.",
      "tags": [
        {
          "$ref": "#/components/tags/State"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/msg_group_artwork"
        }
      ]
    },
    "send_pairing": {
      "action": "send",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Pairing code",
      "summary": "The code apps pair with now.",
      "tags": [
        {
          "$ref": "#/components/tags/Localonly"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/msg_pairing"
        }
      ]
    },
    "send_remotes": {
      "action": "send",
      "channel": {
        "$ref": "#/channels/connection"
      },
      "title": "Paired apps",
      "summary": "The apps paired with the server.",
      "tags": [
        {
          "$ref": "#/components/tags/Localonly"
        }
      ],
      "messages": [
        {
          "$ref": "#/channels/connection/messages/msg_remotes"
        }
      ]
    }
  },
  "components": {
    "messages": {
      "cmd_pair": {
        "name": "pair",
        "title": "Pair",
        "summary": "The first line of a new client: pair, once, and get a token.",
        "description": "Only as the first line of a connection. Answered with `paired`, or\nwith `error` (`code_needed`, `bad_code` or `locked`), which ends the\nconnection. See [Pairing and hello](#pairing-and-hello).",
        "tags": [
          {
            "$ref": "#/components/tags/Handshake"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `pair`.",
              "const": "pair"
            },
            "code": {
              "type": "string",
              "description": "The server's 4-digit pairing code, or `\"\"` to try without one (a new server takes its first client without a code)."
            },
            "name": {
              "type": "string",
              "description": "Your client's name, as the server's list of paired apps shows it (up to 100 bytes; `\"Remote\"` when empty)."
            }
          },
          "required": [
            "cmd",
            "code"
          ]
        },
        "examples": [
          {
            "name": "Without a code",
            "payload": {
              "cmd": "pair",
              "code": "",
              "name": "Anna's tablet"
            }
          },
          {
            "name": "With the code",
            "payload": {
              "cmd": "pair",
              "code": "4719",
              "name": "Anna's tablet"
            }
          }
        ]
      },
      "cmd_hello": {
        "name": "hello",
        "title": "Hello",
        "summary": "The first line of a paired client, every time after pairing.",
        "description": "Only as the first line of a connection. Answered with `hello`, or with\n`error` `unknown_token`, which ends the connection.",
        "tags": [
          {
            "$ref": "#/components/tags/Handshake"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `hello`.",
              "const": "hello"
            },
            "token": {
              "type": "string",
              "description": "The token from `paired`. Leave it out only from the server's own computer (127.0.0.1), unless the server runs with `-remote-local-pairing`."
            }
          },
          "required": [
            "cmd"
          ]
        },
        "examples": [
          {
            "name": "With a token",
            "payload": {
              "cmd": "hello",
              "token": "Vb3…"
            }
          },
          {
            "name": "From the same computer",
            "payload": {
              "cmd": "hello"
            }
          }
        ]
      },
      "cmd_pairing_code": {
        "name": "pairing_code",
        "title": "Ask for the pairing code",
        "summary": "The code another device pairs with now.",
        "description": "For an \"add a device\" screen (Samklang shows it under Settings › Add a\ndevice). Any paired client may ask.",
        "tags": [
          {
            "$ref": "#/components/tags/Handshake"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `pairing_code`.",
              "const": "pairing_code"
            },
            "ref": {
              "type": "string",
              "description": "Any string you like: the answer carries it back, so you can match the two."
            }
          },
          "required": [
            "cmd"
          ]
        },
        "examples": [
          {
            "name": "Ask",
            "payload": {
              "cmd": "pairing_code",
              "ref": "p1"
            }
          }
        ]
      },
      "msg_paired": {
        "name": "paired",
        "title": "Paired",
        "summary": "Pairing worked: keep the token.",
        "description": "Store `token` together with the certificate's whole fingerprint. The\npairing code changes after every successful pairing.",
        "tags": [
          {
            "$ref": "#/components/tags/Handshake"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "description": "Always `paired`.",
              "const": "paired"
            },
            "token": {
              "type": "string",
              "description": "Your token: send it in `hello` from now on. It is secret: whoever has it controls the server."
            },
            "server": {
              "type": "object",
              "description": "The server.",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "The first 16 hex digits of its certificate's SHA-256 fingerprint (Bonjour's `id`)."
                },
                "name": {
                  "type": "string",
                  "description": "Its name."
                },
                "version": {
                  "type": "string",
                  "description": "Samklang Server's version."
                },
                "os": {
                  "type": "string",
                  "description": "Its operating system: `darwin`, `linux` or `windows`."
                },
                "proto": {
                  "type": "string",
                  "description": "The protocol version: `1`."
                }
              },
              "required": [
                "id",
                "name",
                "version",
                "os",
                "proto"
              ]
            }
          },
          "required": [
            "type",
            "token",
            "server"
          ]
        },
        "examples": [
          {
            "name": "Paired",
            "payload": {
              "type": "paired",
              "token": "Vb3…",
              "server": {
                "id": "3f2ac81e90b4d7c2",
                "name": "Vardagsrum",
                "version": "0.9.3",
                "os": "linux",
                "proto": "1"
              }
            }
          }
        ]
      },
      "msg_hello": {
        "name": "hello",
        "title": "Hello",
        "summary": "The handshake worked: the state follows.",
        "tags": [
          {
            "$ref": "#/components/tags/Handshake"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "description": "Always `hello`.",
              "const": "hello"
            },
            "server": {
              "type": "object",
              "description": "The server.",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "The first 16 hex digits of its certificate's SHA-256 fingerprint (Bonjour's `id`)."
                },
                "name": {
                  "type": "string",
                  "description": "Its name."
                },
                "version": {
                  "type": "string",
                  "description": "Samklang Server's version."
                },
                "os": {
                  "type": "string",
                  "description": "Its operating system: `darwin`, `linux` or `windows`."
                },
                "proto": {
                  "type": "string",
                  "description": "The protocol version: `1`."
                }
              },
              "required": [
                "id",
                "name",
                "version",
                "os",
                "proto"
              ]
            }
          },
          "required": [
            "type",
            "server"
          ]
        },
        "examples": [
          {
            "name": "Hello",
            "payload": {
              "type": "hello",
              "server": {
                "id": "3f2ac81e90b4d7c2",
                "name": "Vardagsrum",
                "version": "0.9.3",
                "os": "linux",
                "proto": "1"
              }
            }
          }
        ]
      },
      "msg_error": {
        "name": "error",
        "title": "Handshake error",
        "summary": "The handshake failed; the server closes the connection.",
        "description": "- `code_needed`: trying without a code never counts as a wrong guess.\n- `bad_code`: each wrong code takes a second to answer.\n- `locked`: five wrong codes lock pairing, for a minute at first and\n  doubling each time up to an hour, and the code changes.\n- `unknown_token`: pair again.",
        "tags": [
          {
            "$ref": "#/components/tags/Handshake"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "description": "Always `error`.",
              "const": "error"
            },
            "error": {
              "type": "string",
              "description": "`code_needed`: the server is set up already; ask the user for its code. `bad_code`: wrong code. `locked`: too many wrong codes; try again after `retry_after` seconds. `unknown_token`: the token was unpaired (or the server's settings were reset); pair again.",
              "enum": [
                "code_needed",
                "bad_code",
                "locked",
                "unknown_token"
              ]
            },
            "retry_after": {
              "type": "integer",
              "description": "Seconds until pairing opens again. Sent with every answer to `pair` (`0` unless `locked`); not with `unknown_token`."
            }
          },
          "required": [
            "type",
            "error"
          ]
        },
        "examples": [
          {
            "name": "Code needed",
            "payload": {
              "type": "error",
              "error": "code_needed",
              "retry_after": 0
            }
          },
          {
            "name": "Bad code",
            "payload": {
              "type": "error",
              "error": "bad_code",
              "retry_after": 0
            }
          },
          {
            "name": "Locked",
            "payload": {
              "type": "error",
              "error": "locked",
              "retry_after": 60
            }
          },
          {
            "name": "Unknown token",
            "payload": {
              "type": "error",
              "error": "unknown_token"
            }
          }
        ]
      },
      "msg_pairing_code": {
        "name": "pairing_code",
        "title": "Pairing code",
        "summary": "The answer to `pairing_code`.",
        "tags": [
          {
            "$ref": "#/components/tags/Handshake"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "description": "Always `pairing_code`.",
              "const": "pairing_code"
            },
            "ref": {
              "type": "string",
              "description": "The `ref` the command carried (`\"\"` when it had none)."
            },
            "code": {
              "type": "string",
              "description": "The 4-digit code another device pairs with now."
            }
          },
          "required": [
            "type",
            "ref",
            "code"
          ]
        },
        "examples": [
          {
            "name": "The code",
            "payload": {
              "type": "pairing_code",
              "ref": "p1",
              "code": "4719"
            }
          }
        ]
      },
      "msg_state": {
        "name": "state",
        "title": "State",
        "summary": "The whole picture: sent when you connect, and whenever it changes.",
        "description": "Replace what you had, don't merge. It is sent right after the handshake\nand then each time something changes (never when nothing did).\n\nSeveral groups can play at once: two people playing to two groups, each\nits own song. Each playing group has its own `now` in `groups`. A device\nin two groups plays the group that started last; the other group plays\non without it, and gets it back when that group stops.",
        "tags": [
          {
            "$ref": "#/components/tags/State"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "description": "Always `state`.",
              "const": "state"
            },
            "speaker": {
              "type": "string",
              "description": "The multiroom speaker (group) the top-level fields describe: the one playing (the first, when several are), else Everywhere. `\"\"` when there is none."
            },
            "everywhere": {
              "type": "boolean",
              "description": "Whether there is a multiroom speaker. When false, only `settings`, `groups`, `devices`, `satellites`, `sound_boards` and `updates` say anything."
            },
            "playing": {
              "type": "boolean",
              "description": "Whether something plays on it (a paused stream still counts as playing)."
            },
            "paused": {
              "type": "boolean",
              "description": "Whether it is paused."
            },
            "can_control": {
              "type": "boolean",
              "description": "Whether the sender (the iPhone or Mac playing to Samklang) takes `transport` and `volume` commands."
            },
            "volume": {
              "type": "number",
              "description": "The sender's volume, 0 to 1. Missing until the sender has set one.",
              "minimum": 0,
              "maximum": 1
            },
            "settings": {
              "type": "object",
              "description": "What the server looks for and offers.",
              "properties": {
                "find_airplay": {
                  "type": "boolean",
                  "description": "Use paired AirPlay receivers, such as an Apple TV. Default on."
                },
                "find_chromecast": {
                  "type": "boolean",
                  "description": "Use Chromecasts and Cast groups. Default on."
                },
                "find_upnp": {
                  "type": "boolean",
                  "description": "Use UPnP, DLNA and Sonos renderers. Default off."
                },
                "multiroom": {
                  "type": "boolean",
                  "description": "Offer the multiroom speakers (they need AirPlay and Chromecast both on). Default on."
                },
                "find_cast_groups": {
                  "type": "boolean",
                  "description": "Use the speaker groups made in Google Home too; off, each Chromecast plays on its own. Default off."
                },
                "find_bluetooth": {
                  "type": "boolean",
                  "description": "Play to Bluetooth speakers paired with the server's computer. Default off: headphones paired with it would turn up too."
                },
                "check_updates": {
                  "type": "boolean",
                  "description": "Look for new versions of Samklang Server, Samklang and the satellites once a day. Default on. Off, nothing is fetched from Samklang's website."
                }
              },
              "required": [
                "find_airplay",
                "find_chromecast",
                "find_upnp",
                "multiroom",
                "find_cast_groups",
                "find_bluetooth",
                "check_updates"
              ]
            },
            "groups": {
              "type": "array",
              "description": "Every multiroom speaker: Everywhere and the user's groups.",
              "items": {
                "type": "object",
                "description": "A multiroom speaker.",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Its name: the key every group command uses."
                  },
                  "auto_add": {
                    "type": "boolean",
                    "description": "Devices seen for the first time join it."
                  },
                  "auto": {
                    "type": "boolean",
                    "description": "True for Everywhere: every device not left out. A user's group has `members` instead."
                  },
                  "members": {
                    "type": "array",
                    "description": "A user's group: its devices, by label.",
                    "items": {
                      "type": "string",
                      "description": "A device's label."
                    }
                  },
                  "playing": {
                    "type": "boolean",
                    "description": "Whether it plays (a paused stream still counts)."
                  },
                  "google_groups": {
                    "type": "boolean",
                    "description": "Its Chromecasts play through Google Home groups (see `google_groups`)."
                  },
                  "google_fits": {
                    "type": "array",
                    "description": "The Google Home groups that would (or do) play in place of its Chromecasts. Missing: the option isn't offered.",
                    "items": {
                      "type": "object",
                      "description": "A Google Home speaker group.",
                      "properties": {
                        "name": {
                          "type": "string",
                          "description": "The Google Home group's name."
                        },
                        "speakers": {
                          "type": "array",
                          "description": "The speakers it plays in place of.",
                          "items": {
                            "type": "string",
                            "description": "A speaker's label."
                          }
                        }
                      },
                      "required": [
                        "name",
                        "speakers"
                      ]
                    }
                  },
                  "now": {
                    "type": "object",
                    "description": "What this group plays, while it plays (or its sender paused and left).",
                    "properties": {
                      "paused": {
                        "type": "boolean",
                        "description": "Whether it is paused."
                      },
                      "can_control": {
                        "type": "boolean",
                        "description": "Whether its sender takes `transport` and `volume` commands."
                      },
                      "volume": {
                        "type": "number",
                        "description": "The sender's volume, 0 to 1. Missing until the sender has set one.",
                        "minimum": 0,
                        "maximum": 1
                      },
                      "track": {
                        "type": "object",
                        "description": "What is playing. Missing when nothing is, or when the sender says nothing about it.",
                        "properties": {
                          "title": {
                            "type": "string",
                            "description": "The track's title."
                          },
                          "artist": {
                            "type": "string",
                            "description": "Its artist."
                          },
                          "album": {
                            "type": "string",
                            "description": "Its album."
                          },
                          "duration_ms": {
                            "type": "integer",
                            "description": "Its length in ms, when known."
                          },
                          "started_at_ms": {
                            "type": "integer",
                            "description": "While playing: when the track began, in Unix ms. Count on from it yourself (against the server's clock: see `clock`)."
                          },
                          "position_ms": {
                            "type": "integer",
                            "description": "While paused: where it stopped, in ms."
                          }
                        },
                        "required": [
                          "title",
                          "artist",
                          "album"
                        ]
                      },
                      "artwork_id": {
                        "type": "integer",
                        "description": "Its cover, sent as `group_artwork` with this `id`."
                      },
                      "members": {
                        "type": "array",
                        "description": "The devices it plays on now, by label.",
                        "items": {
                          "type": "string",
                          "description": "A device's label."
                        }
                      }
                    },
                    "required": [
                      "paused",
                      "can_control",
                      "artwork_id",
                      "members"
                    ]
                  }
                },
                "required": [
                  "name",
                  "auto",
                  "playing"
                ]
              }
            },
            "auto_sync": {
              "type": "boolean",
              "description": "Automatic sync is on."
            },
            "target_ms": {
              "type": "integer",
              "description": "The delay auto sync brings every device onto, in ms."
            },
            "track": {
              "type": "object",
              "description": "What is playing. Missing when nothing is, or when the sender says nothing about it.",
              "properties": {
                "title": {
                  "type": "string",
                  "description": "The track's title."
                },
                "artist": {
                  "type": "string",
                  "description": "Its artist."
                },
                "album": {
                  "type": "string",
                  "description": "Its album."
                },
                "duration_ms": {
                  "type": "integer",
                  "description": "Its length in ms, when known."
                },
                "started_at_ms": {
                  "type": "integer",
                  "description": "While playing: when the track began, in Unix ms. Count on from it yourself (against the server's clock: see `clock`)."
                },
                "position_ms": {
                  "type": "integer",
                  "description": "While paused: where it stopped, in ms."
                }
              },
              "required": [
                "title",
                "artist",
                "album"
              ]
            },
            "artwork_id": {
              "type": "integer",
              "description": "The cover that goes with this state: the `id` of the last `artwork`."
            },
            "members": {
              "type": "array",
              "description": "The devices playing now.",
              "items": {
                "type": "object",
                "description": "A device playing now.",
                "properties": {
                  "type": {
                    "type": "string",
                    "description": "How the server reaches it: `airplay`, `cast`, `upnp`, `bluetooth` (paired with the server's computer) or `sat` (a satellite's speaker, or its headphone jack).",
                    "enum": [
                      "airplay",
                      "cast",
                      "upnp",
                      "bluetooth",
                      "sat"
                    ]
                  },
                  "label": {
                    "type": "string",
                    "description": "Its name in Samklang: the key every command uses."
                  },
                  "kind": {
                    "type": "string",
                    "description": "What it is, to show (`\"Chromecast Audio\"`, `\"AirPlay\"`)."
                  },
                  "detail": {
                    "type": "string",
                    "description": "More about it, to show (`\"Google Home group\"`)."
                  },
                  "ms": {
                    "type": "integer",
                    "description": "Under auto sync its fine-tune, else its delay: what `set` changes."
                  },
                  "delay_ms": {
                    "type": "integer",
                    "description": "Its delay."
                  },
                  "lag_ms": {
                    "type": "integer",
                    "description": "Its measured lag, when known."
                  },
                  "connected": {
                    "type": "boolean",
                    "description": "Whether it is receiving."
                  },
                  "level": {
                    "type": "number",
                    "description": "Its volume relative to the sender's, 0 to 1.",
                    "minimum": 0,
                    "maximum": 1
                  },
                  "timing": {
                    "type": "string",
                    "description": "`measured` (its lag is measured and corrected) or `fixed` (known exactly).",
                    "enum": [
                      "measured",
                      "fixed"
                    ]
                  },
                  "syncing": {
                    "type": "boolean",
                    "description": "Auto sync is still bringing it onto the target."
                  },
                  "speakers": {
                    "type": "array",
                    "description": "A Cast group's speakers, each with its own volume.",
                    "items": {
                      "type": "object",
                      "description": "One speaker of a playing Cast group.",
                      "properties": {
                        "label": {
                          "type": "string",
                          "description": "The speaker's label."
                        },
                        "level": {
                          "type": "number",
                          "description": "Its volume relative to the group's, 0 to 1.",
                          "minimum": 0,
                          "maximum": 1
                        }
                      },
                      "required": [
                        "label",
                        "level"
                      ]
                    }
                  }
                },
                "required": [
                  "type",
                  "label",
                  "kind",
                  "ms",
                  "delay_ms",
                  "connected",
                  "level",
                  "timing"
                ]
              }
            },
            "devices": {
              "type": "array",
              "description": "Every device the server knows, online or not.",
              "items": {
                "type": "object",
                "description": "A device the server knows, online or not.",
                "properties": {
                  "type": {
                    "type": "string",
                    "description": "How the server reaches it: `airplay`, `cast`, `upnp`, `bluetooth` (paired with the server's computer) or `sat` (a satellite's speaker, or its headphone jack).",
                    "enum": [
                      "airplay",
                      "cast",
                      "upnp",
                      "bluetooth",
                      "sat"
                    ]
                  },
                  "label": {
                    "type": "string",
                    "description": "Its name in Samklang: the key every command uses."
                  },
                  "kind": {
                    "type": "string",
                    "description": "What it is, to show."
                  },
                  "detail": {
                    "type": "string",
                    "description": "More about it, to show."
                  },
                  "included": {
                    "type": "boolean",
                    "description": "Everywhere plays on it (see `include`)."
                  },
                  "group": {
                    "type": "boolean",
                    "description": "It is a Cast group."
                  },
                  "speakers": {
                    "type": "array",
                    "description": "A Cast group's speakers.",
                    "items": {
                      "type": "string",
                      "description": "A speaker's label."
                    }
                  },
                  "in_group": {
                    "type": "string",
                    "description": "A speaker: the Cast group that plays it."
                  },
                  "online": {
                    "type": "boolean",
                    "description": "Seen now."
                  },
                  "last_seen": {
                    "type": "string",
                    "description": "When an offline device was last seen (RFC 3339).",
                    "format": "date-time"
                  },
                  "shown": {
                    "type": "boolean",
                    "description": "Its own AirPlay speaker is offered (always true for AirPlay receivers). See `show`."
                  },
                  "groups": {
                    "type": "array",
                    "description": "The user's groups it is in.",
                    "items": {
                      "type": "string",
                      "description": "A group's name."
                    }
                  },
                  "new": {
                    "type": "boolean",
                    "description": "Seen for the first time, until a client sends `seen_device`."
                  },
                  "channel": {
                    "type": "string",
                    "description": "What it plays: `both` channels (the default), or `left` or `right`, as one of a stereo pair. See `channel`.",
                    "enum": [
                      "both",
                      "left",
                      "right"
                    ]
                  }
                },
                "required": [
                  "type",
                  "label",
                  "kind",
                  "included",
                  "online",
                  "shown",
                  "channel"
                ]
              }
            },
            "satellites": {
              "type": "array",
              "description": "The Samklang satellites connected, with a speaker or not (one with a speaker is among the `devices` too). Missing when there are none.",
              "items": {
                "type": "object",
                "description": "A Samklang satellite: a Raspberry Pi Pico W or Pico 2 W that plays on a Bluetooth speaker.",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "The satellite's id: the key the satellite commands use."
                  },
                  "name": {
                    "type": "string",
                    "description": "Its name."
                  },
                  "state": {
                    "type": "string",
                    "description": "`none` (no speaker yet), `connecting`, `ready` or `playing`.",
                    "enum": [
                      "none",
                      "connecting",
                      "ready",
                      "playing"
                    ]
                  },
                  "speaker": {
                    "type": "string",
                    "description": "The speaker it plays on."
                  },
                  "speaker_addr": {
                    "type": "string",
                    "description": "That speaker's Bluetooth address."
                  },
                  "found": {
                    "type": "array",
                    "description": "Bluetooth speakers it found after `satellite_scan`.",
                    "items": {
                      "type": "object",
                      "description": "A speaker it found.",
                      "properties": {
                        "addr": {
                          "type": "string",
                          "description": "Its Bluetooth address: what `satellite_use` takes."
                        },
                        "name": {
                          "type": "string",
                          "description": "Its name."
                        },
                        "rssi": {
                          "type": "integer",
                          "description": "Its signal strength, in dBm."
                        }
                      },
                      "required": [
                        "addr",
                        "name",
                        "rssi"
                      ]
                    }
                  },
                  "board": {
                    "type": "string",
                    "description": "The sound board the user said is attached: an `id` from the state's `sound_boards`. Missing for none."
                  },
                  "pins": {
                    "type": "object",
                    "description": "A board wired by hand (`i2s`): its pins.",
                    "properties": {
                      "data": {
                        "type": "integer",
                        "description": "The data pin (GP number)."
                      },
                      "clock": {
                        "type": "integer",
                        "description": "The bit clock's pin; the word clock is on the pin after."
                      }
                    },
                    "required": [
                      "data",
                      "clock"
                    ]
                  },
                  "quality": {
                    "type": "string",
                    "description": "Its speaker's sound as set: `auto`, `standard` or `high`.",
                    "enum": [
                      "auto",
                      "standard",
                      "high"
                    ]
                  },
                  "xq": {
                    "type": "boolean",
                    "description": "Whether its speaker plays SBC XQ now."
                  },
                  "jack": {
                    "type": "string",
                    "description": "With a sound board: its headphone jack's label, a device of its own."
                  },
                  "chip": {
                    "type": "string",
                    "description": "`rp2040` (Pico W) or `rp2350` (Pico 2 W).",
                    "enum": [
                      "rp2040",
                      "rp2350"
                    ]
                  },
                  "update": {
                    "type": "string",
                    "description": "Its firmware update: `available` (the user is asked; `satellite_update` approves), `updating`, or `waiting` (a developer's server, which updates as soon as the satellite doesn't play). Missing: none.",
                    "enum": [
                      "available",
                      "updating",
                      "waiting"
                    ]
                  },
                  "version": {
                    "type": "string",
                    "description": "Its firmware's version. Missing on older firmware, or a developer's build."
                  },
                  "update_to": {
                    "type": "string",
                    "description": "The version an update brings."
                  }
                },
                "required": [
                  "id",
                  "name",
                  "state"
                ]
              }
            },
            "sound_boards": {
              "type": "array",
              "description": "The sound boards a satellite can have, to offer. Missing on a server without satellite support.",
              "items": {
                "type": "object",
                "description": "A sound board a satellite can have.",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Its id: what `satellite_board` takes."
                  },
                  "name": {
                    "type": "string",
                    "description": "Its name, to show."
                  },
                  "detail": {
                    "type": "string",
                    "description": "More about it, to show."
                  },
                  "custom": {
                    "type": "boolean",
                    "description": "Wired by hand: ask the user for its pins."
                  }
                },
                "required": [
                  "id",
                  "name",
                  "detail"
                ]
              }
            },
            "updates": {
              "type": "object",
              "description": "What is new for Samklang Server and Samklang. Missing until the server has looked, and while `check_updates` is off.",
              "properties": {
                "server": {
                  "type": "object",
                  "description": "A newer Samklang Server, when there is one. It is updated on the computer it runs on.",
                  "properties": {
                    "version": {
                      "type": "string",
                      "description": "Its version."
                    },
                    "url": {
                      "type": "string",
                      "description": "Where to get it."
                    },
                    "notes": {
                      "type": "string",
                      "description": "What is new, to show."
                    }
                  },
                  "required": [
                    "version"
                  ]
                },
                "app": {
                  "type": "object",
                  "description": "Samklang's latest version, for Samklang itself; your own client can ignore it.",
                  "properties": {
                    "version": {
                      "type": "string",
                      "description": "Its version."
                    },
                    "url": {
                      "type": "string",
                      "description": "Where to get it."
                    },
                    "notes": {
                      "type": "string",
                      "description": "What is new, to show."
                    }
                  },
                  "required": [
                    "version"
                  ]
                },
                "checked": {
                  "type": "string",
                  "description": "When the server last read the update file (RFC 3339).",
                  "format": "date-time"
                }
              },
              "required": [
                "checked"
              ]
            }
          },
          "required": [
            "type",
            "speaker",
            "everywhere",
            "playing",
            "paused",
            "can_control",
            "settings",
            "groups",
            "auto_sync",
            "target_ms",
            "artwork_id",
            "members",
            "devices"
          ]
        },
        "examples": [
          {
            "name": "Playing",
            "payload": {
              "type": "state",
              "speaker": "Vardagsrum",
              "everywhere": true,
              "playing": true,
              "paused": false,
              "can_control": true,
              "volume": 0.6,
              "settings": {
                "find_airplay": true,
                "find_chromecast": true,
                "find_upnp": false,
                "multiroom": true,
                "find_cast_groups": false,
                "find_bluetooth": false,
                "check_updates": true
              },
              "groups": [
                {
                  "name": "Everywhere",
                  "auto": true,
                  "playing": false
                },
                {
                  "name": "Vardagsrum",
                  "auto": false,
                  "members": [
                    "Kök",
                    "Sovrum"
                  ],
                  "playing": true,
                  "now": {
                    "paused": false,
                    "can_control": true,
                    "volume": 0.6,
                    "track": {
                      "title": "…",
                      "artist": "…",
                      "album": "…",
                      "duration_ms": 215000,
                      "started_at_ms": 1793366400000
                    },
                    "artwork_id": 3,
                    "members": [
                      "Kök",
                      "Sovrum"
                    ]
                  }
                }
              ],
              "auto_sync": true,
              "target_ms": 2000,
              "track": {
                "title": "…",
                "artist": "…",
                "album": "…",
                "duration_ms": 215000,
                "started_at_ms": 1793366400000
              },
              "artwork_id": 3,
              "members": [
                {
                  "type": "cast",
                  "label": "Kök",
                  "kind": "Chromecast Audio",
                  "ms": -40,
                  "delay_ms": 1960,
                  "lag_ms": 1210,
                  "connected": true,
                  "level": 0.65,
                  "timing": "measured"
                },
                {
                  "type": "sat",
                  "label": "Sovrum",
                  "kind": "Samklang satellite",
                  "ms": 0,
                  "delay_ms": 2000,
                  "connected": true,
                  "level": 1,
                  "timing": "fixed",
                  "syncing": true
                }
              ],
              "devices": [
                {
                  "type": "cast",
                  "label": "Kök",
                  "kind": "Chromecast Audio",
                  "included": true,
                  "online": true,
                  "shown": true,
                  "groups": [
                    "Vardagsrum"
                  ],
                  "channel": "left"
                },
                {
                  "type": "sat",
                  "label": "Sovrum",
                  "kind": "Samklang satellite",
                  "included": true,
                  "online": true,
                  "shown": true,
                  "groups": [
                    "Vardagsrum"
                  ],
                  "channel": "both"
                },
                {
                  "type": "upnp",
                  "label": "Hall",
                  "kind": "UPnP",
                  "included": true,
                  "online": false,
                  "last_seen": "2026-10-30T13:02:11Z",
                  "shown": true,
                  "channel": "both"
                }
              ],
              "satellites": [
                {
                  "id": "28CDC1008DC6",
                  "name": "Hall",
                  "state": "ready",
                  "speaker": "ENEBY Portable",
                  "speaker_addr": "6C:5C:3D:34:BC:5C",
                  "found": [
                    {
                      "addr": "6C:5C:3D:34:BC:5C",
                      "name": "ENEBY Portable",
                      "rssi": -58
                    }
                  ],
                  "board": "pimoroni-audio-pack",
                  "quality": "auto",
                  "xq": true,
                  "jack": "Hall (jack)",
                  "chip": "rp2350",
                  "version": "1.0.0",
                  "update": "available",
                  "update_to": "1.1.0"
                }
              ],
              "sound_boards": [
                {
                  "id": "pimoroni-audio-pack",
                  "name": "Pimoroni Pico Audio Pack",
                  "detail": "PCM5100A, line out and headphones (PIM544)"
                },
                {
                  "id": "waveshare-pico-audio",
                  "name": "Waveshare Pico-Audio",
                  "detail": "…"
                },
                {
                  "id": "i2s",
                  "name": "Another I2S DAC or amplifier",
                  "detail": "…",
                  "custom": true
                }
              ],
              "updates": {
                "server": {
                  "version": "1.1.0",
                  "url": "https://samklang.wieoapps.com/setup.html",
                  "notes": "…"
                },
                "app": {
                  "version": "1.1.0",
                  "url": "https://apps.apple.com/…"
                },
                "checked": "2026-11-02T09:00:00Z"
              }
            }
          },
          {
            "name": "No multiroom speaker",
            "payload": {
              "type": "state",
              "speaker": "",
              "everywhere": false,
              "playing": false,
              "paused": false,
              "can_control": false,
              "settings": {
                "find_airplay": true,
                "find_chromecast": false,
                "find_upnp": false,
                "multiroom": true,
                "find_cast_groups": false,
                "find_bluetooth": false,
                "check_updates": true
              },
              "groups": [],
              "auto_sync": false,
              "target_ms": 0,
              "artwork_id": 0,
              "members": [],
              "devices": [
                {
                  "type": "airplay",
                  "label": "Vardagsrum",
                  "kind": "AirPlay",
                  "included": true,
                  "online": true,
                  "shown": true,
                  "channel": "both"
                }
              ]
            }
          }
        ]
      },
      "msg_artwork": {
        "name": "artwork",
        "title": "Cover",
        "summary": "The cover of the track playing.",
        "description": "Sent once whenever it changes, before the state that carries the same\n`artwork_id`, and when you connect.",
        "tags": [
          {
            "$ref": "#/components/tags/State"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "description": "Always `artwork`.",
              "const": "artwork"
            },
            "id": {
              "type": "integer",
              "description": "Its id: the state's `artwork_id` names it."
            },
            "jpeg": {
              "type": "string",
              "description": "The JPEG, base64. `\"\"` when there is none.",
              "contentEncoding": "base64"
            }
          },
          "required": [
            "type",
            "id",
            "jpeg"
          ]
        },
        "examples": [
          {
            "name": "Cover",
            "payload": {
              "type": "artwork",
              "id": 3,
              "jpeg": "<base64>"
            }
          }
        ]
      },
      "msg_group_artwork": {
        "name": "group_artwork",
        "title": "A group's cover",
        "summary": "The cover of one playing group.",
        "description": "Sent when it changes, before the state whose `groups[].now.artwork_id`\nnames it, and when you connect.",
        "tags": [
          {
            "$ref": "#/components/tags/State"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "description": "Always `group_artwork`.",
              "const": "group_artwork"
            },
            "group": {
              "type": "string",
              "description": "The group's name."
            },
            "id": {
              "type": "integer",
              "description": "Its id: the group's `now.artwork_id` names it."
            },
            "jpeg": {
              "type": "string",
              "description": "The JPEG, base64. `\"\"` when there is none.",
              "contentEncoding": "base64"
            }
          },
          "required": [
            "type",
            "group",
            "id",
            "jpeg"
          ]
        },
        "examples": [
          {
            "name": "Cover",
            "payload": {
              "type": "group_artwork",
              "group": "Kök",
              "id": 7,
              "jpeg": "<base64>"
            }
          }
        ]
      },
      "cmd_transport": {
        "name": "transport",
        "title": "Play, pause, skip",
        "summary": "Play/pause, next or previous on the sender.",
        "description": "Only does something when the state's `can_control` is true.",
        "tags": [
          {
            "$ref": "#/components/tags/Playing"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `transport`.",
              "const": "transport"
            },
            "action": {
              "type": "string",
              "description": "`playpause`, `next` or `previous`.",
              "enum": [
                "playpause",
                "next",
                "previous"
              ]
            },
            "group": {
              "type": "string",
              "description": "The multiroom speaker (group) it is for. Default: the one playing, else Everywhere."
            }
          },
          "required": [
            "cmd",
            "action"
          ]
        },
        "examples": [
          {
            "name": "Play/pause",
            "payload": {
              "cmd": "transport",
              "action": "playpause"
            }
          }
        ]
      },
      "cmd_volume": {
        "name": "volume",
        "title": "Volume",
        "summary": "The sender's volume.",
        "tags": [
          {
            "$ref": "#/components/tags/Playing"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `volume`.",
              "const": "volume"
            },
            "value": {
              "type": "number",
              "description": "0 to 1.",
              "minimum": 0,
              "maximum": 1
            },
            "group": {
              "type": "string",
              "description": "The multiroom speaker (group) it is for. Default: the one playing, else Everywhere."
            }
          },
          "required": [
            "cmd",
            "value"
          ]
        },
        "examples": [
          {
            "name": "Volume",
            "payload": {
              "cmd": "volume",
              "value": 0.6
            }
          }
        ]
      },
      "cmd_level": {
        "name": "level",
        "title": "A device's volume",
        "summary": "One device's volume relative to the sender's.",
        "tags": [
          {
            "$ref": "#/components/tags/Playing"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `level`.",
              "const": "level"
            },
            "label": {
              "type": "string",
              "description": "The device, by its `label`; also a speaker inside a playing Cast group."
            },
            "value": {
              "type": "number",
              "description": "0 to 1 (outside is clamped).",
              "minimum": 0,
              "maximum": 1
            },
            "group": {
              "type": "string",
              "description": "The multiroom speaker (group) it is for. Default: the one playing, else Everywhere."
            }
          },
          "required": [
            "cmd",
            "label",
            "value"
          ]
        },
        "examples": [
          {
            "name": "Level",
            "payload": {
              "cmd": "level",
              "label": "Kök",
              "value": 0.65
            }
          }
        ]
      },
      "cmd_resync": {
        "name": "resync",
        "title": "Resync",
        "summary": "Bring one playing device back into sync now.",
        "tags": [
          {
            "$ref": "#/components/tags/Playing"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `resync`.",
              "const": "resync"
            },
            "label": {
              "type": "string",
              "description": "The device, by its `label`."
            },
            "group": {
              "type": "string",
              "description": "The multiroom speaker (group) it is for. Default: the one playing, else Everywhere."
            }
          },
          "required": [
            "cmd",
            "label"
          ]
        },
        "examples": [
          {
            "name": "Resync",
            "payload": {
              "cmd": "resync",
              "label": "Kök"
            }
          }
        ]
      },
      "cmd_auto_sync": {
        "name": "auto_sync",
        "title": "Auto sync",
        "summary": "Automatic sync on or off.",
        "description": "For every group. Ignored while there is no multiroom speaker.",
        "tags": [
          {
            "$ref": "#/components/tags/Sync"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `auto_sync`.",
              "const": "auto_sync"
            },
            "on": {
              "type": "boolean",
              "description": "On or off."
            }
          },
          "required": [
            "cmd",
            "on"
          ]
        },
        "examples": [
          {
            "name": "On",
            "payload": {
              "cmd": "auto_sync",
              "on": true
            }
          }
        ]
      },
      "cmd_set": {
        "name": "set",
        "title": "Fine-tune or delay",
        "summary": "A device's fine-tune (under auto sync) or its delay.",
        "tags": [
          {
            "$ref": "#/components/tags/Sync"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `set`.",
              "const": "set"
            },
            "label": {
              "type": "string",
              "description": "The device, by its `label`."
            },
            "ms": {
              "type": "integer",
              "description": "Under auto sync the fine-tune, −2000 to 2000; without it the delay, 0 to 5000. Out of range is ignored."
            },
            "group": {
              "type": "string",
              "description": "The multiroom speaker (group) it is for. Default: the one playing, else Everywhere."
            }
          },
          "required": [
            "cmd",
            "label",
            "ms"
          ]
        },
        "examples": [
          {
            "name": "Fine-tune",
            "payload": {
              "cmd": "set",
              "label": "Kök",
              "ms": -40
            }
          }
        ]
      },
      "cmd_clock": {
        "name": "clock",
        "title": "Clock",
        "summary": "Ask for the server's clock.",
        "description": "For anything timed against the server (track progress, calibration).\nHalve the round trip to estimate the offset. While calibrating, it also\ntells the server you are still there.",
        "tags": [
          {
            "$ref": "#/components/tags/Sync"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `clock`.",
              "const": "clock"
            },
            "t": {
              "type": "number",
              "description": "Your own clock, in Unix ms."
            },
            "ref": {
              "type": "string",
              "description": "Any string you like: the answer carries it back, so you can match the two."
            }
          },
          "required": [
            "cmd",
            "t"
          ]
        },
        "examples": [
          {
            "name": "Clock",
            "payload": {
              "cmd": "clock",
              "t": 1793366400123.5,
              "ref": "c1"
            }
          }
        ]
      },
      "msg_clock": {
        "name": "clock",
        "title": "Clock",
        "summary": "The answer to `clock`.",
        "tags": [
          {
            "$ref": "#/components/tags/Sync"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "description": "Always `clock`.",
              "const": "clock"
            },
            "ref": {
              "type": "string",
              "description": "The `ref` the command carried (`\"\"` when it had none)."
            },
            "t": {
              "type": "number",
              "description": "Your `t`, sent back."
            },
            "server_ms": {
              "type": "number",
              "description": "The server's clock when it answered, in Unix ms."
            }
          },
          "required": [
            "type",
            "ref",
            "t",
            "server_ms"
          ]
        },
        "examples": [
          {
            "name": "Clock",
            "payload": {
              "type": "clock",
              "ref": "c1",
              "t": 1793366400123.5,
              "server_ms": 1793366400119.2
            }
          }
        ]
      },
      "cmd_include": {
        "name": "include",
        "title": "Leave out of Everywhere",
        "summary": "Leave a device out of Everywhere, or take it back.",
        "tags": [
          {
            "$ref": "#/components/tags/Devices"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `include`.",
              "const": "include"
            },
            "label": {
              "type": "string",
              "description": "The device, by its `label`."
            },
            "on": {
              "type": "boolean",
              "description": "`false` leaves it out, `true` takes it back."
            }
          },
          "required": [
            "cmd",
            "label",
            "on"
          ]
        },
        "examples": [
          {
            "name": "Leave out",
            "payload": {
              "cmd": "include",
              "label": "Kök",
              "on": false
            }
          }
        ]
      },
      "cmd_show": {
        "name": "show",
        "title": "Show its AirPlay speaker",
        "summary": "Hide (or show) a device's own AirPlay speaker.",
        "tags": [
          {
            "$ref": "#/components/tags/Devices"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `show`.",
              "const": "show"
            },
            "label": {
              "type": "string",
              "description": "The device, by its `label`."
            },
            "on": {
              "type": "boolean",
              "description": "Show it."
            }
          },
          "required": [
            "cmd",
            "label",
            "on"
          ]
        },
        "examples": [
          {
            "name": "Hide",
            "payload": {
              "cmd": "show",
              "label": "Kök",
              "on": false
            }
          }
        ]
      },
      "cmd_rename": {
        "name": "rename",
        "title": "Rename",
        "summary": "Rename a device in Samklang. The server restarts.",
        "description": "Its AirPlay speaker gets the new name too. **The server restarts**:\nreconnect and say hello again.",
        "tags": [
          {
            "$ref": "#/components/tags/Devices"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `rename`.",
              "const": "rename"
            },
            "label": {
              "type": "string",
              "description": "The device, by its `label`."
            },
            "name": {
              "type": "string",
              "description": "Its new name. Not one another device or group has."
            }
          },
          "required": [
            "cmd",
            "label",
            "name"
          ]
        },
        "examples": [
          {
            "name": "Rename",
            "payload": {
              "cmd": "rename",
              "label": "Kök",
              "name": "Köket"
            }
          }
        ]
      },
      "cmd_channel": {
        "name": "channel",
        "title": "Stereo pair",
        "summary": "Play both channels on a device, or only the left or the right one.",
        "description": "Two speakers in one room make a stereo pair: one plays `left`, the\nother `right`. A device set to one channel plays it on both of its own\noutputs, so a single speaker still sounds full. It applies at once,\nwherever the device plays: in any group and on its own AirPlay speaker.\nOther devices are not affected. The server keeps it, through restarts\nand renames; the state's `devices[].channel` shows it.",
        "tags": [
          {
            "$ref": "#/components/tags/Devices"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `channel`.",
              "const": "channel"
            },
            "label": {
              "type": "string",
              "description": "The device, by its `label`."
            },
            "channel": {
              "type": "string",
              "description": "`both` (the default), `left` or `right`. Anything else is ignored.",
              "enum": [
                "both",
                "left",
                "right"
              ]
            }
          },
          "required": [
            "cmd",
            "label",
            "channel"
          ]
        },
        "examples": [
          {
            "name": "Left",
            "payload": {
              "cmd": "channel",
              "label": "Kök",
              "channel": "left"
            }
          }
        ]
      },
      "cmd_seen_device": {
        "name": "seen_device",
        "title": "Seen",
        "summary": "The user has seen a new device: it is no longer marked `new`.",
        "tags": [
          {
            "$ref": "#/components/tags/Devices"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `seen_device`.",
              "const": "seen_device"
            },
            "label": {
              "type": "string",
              "description": "The device, by its `label`."
            }
          },
          "required": [
            "cmd",
            "label"
          ]
        },
        "examples": [
          {
            "name": "Seen",
            "payload": {
              "cmd": "seen_device",
              "label": "Kök"
            }
          }
        ]
      },
      "cmd_forget_device": {
        "name": "forget_device",
        "title": "Forget",
        "summary": "Drop an offline device from the list.",
        "description": "It comes back if it is seen again.",
        "tags": [
          {
            "$ref": "#/components/tags/Devices"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `forget_device`.",
              "const": "forget_device"
            },
            "label": {
              "type": "string",
              "description": "The device, by its `label`."
            }
          },
          "required": [
            "cmd",
            "label"
          ]
        },
        "examples": [
          {
            "name": "Forget",
            "payload": {
              "cmd": "forget_device",
              "label": "Kök"
            }
          }
        ]
      },
      "cmd_save_group": {
        "name": "save_group",
        "title": "Save a group",
        "summary": "Make a group, or change one.",
        "tags": [
          {
            "$ref": "#/components/tags/Groups"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `save_group`.",
              "const": "save_group"
            },
            "old": {
              "type": "string",
              "description": "`\"\"` (or left out) to make a group; a group's name to change that group."
            },
            "name": {
              "type": "string",
              "description": "The group's name. A new name gives it a new AirPlay speaker."
            },
            "members": {
              "type": "array",
              "description": "Its devices, by label.",
              "items": {
                "type": "string",
                "description": "A device's label."
              }
            },
            "auto_add": {
              "type": "boolean",
              "description": "New devices join it."
            }
          },
          "required": [
            "cmd",
            "name",
            "members"
          ]
        },
        "examples": [
          {
            "name": "New group",
            "payload": {
              "cmd": "save_group",
              "old": "",
              "name": "Downstairs",
              "members": [
                "Kök",
                "Sovrum"
              ],
              "auto_add": false
            }
          }
        ]
      },
      "cmd_delete_group": {
        "name": "delete_group",
        "title": "Delete a group",
        "summary": "Delete a group.",
        "tags": [
          {
            "$ref": "#/components/tags/Groups"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `delete_group`.",
              "const": "delete_group"
            },
            "group": {
              "type": "string",
              "description": "The group's name."
            }
          },
          "required": [
            "cmd",
            "group"
          ]
        },
        "examples": [
          {
            "name": "Delete",
            "payload": {
              "cmd": "delete_group",
              "group": "Downstairs"
            }
          }
        ]
      },
      "cmd_group_member": {
        "name": "group_member",
        "title": "Group member",
        "summary": "Put a device in a group, or take it out.",
        "tags": [
          {
            "$ref": "#/components/tags/Groups"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `group_member`.",
              "const": "group_member"
            },
            "group": {
              "type": "string",
              "description": "The group's name."
            },
            "label": {
              "type": "string",
              "description": "The device, by its `label`."
            },
            "on": {
              "type": "boolean",
              "description": "In or out."
            }
          },
          "required": [
            "cmd",
            "group",
            "label",
            "on"
          ]
        },
        "examples": [
          {
            "name": "Add",
            "payload": {
              "cmd": "group_member",
              "group": "Downstairs",
              "label": "Kök",
              "on": true
            }
          }
        ]
      },
      "cmd_google_groups": {
        "name": "google_groups",
        "title": "Google Home groups",
        "summary": "Play a group's Chromecasts through Google Home groups.",
        "description": "The Google Home groups used are the ones in the group's `google_fits`.",
        "tags": [
          {
            "$ref": "#/components/tags/Groups"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `google_groups`.",
              "const": "google_groups"
            },
            "group": {
              "type": "string",
              "description": "The group's name."
            },
            "on": {
              "type": "boolean",
              "description": "On or off."
            }
          },
          "required": [
            "cmd",
            "group",
            "on"
          ]
        },
        "examples": [
          {
            "name": "On",
            "payload": {
              "cmd": "google_groups",
              "group": "Downstairs",
              "on": true
            }
          }
        ]
      },
      "cmd_settings": {
        "name": "settings",
        "title": "Settings",
        "summary": "Change what the server looks for. The server restarts.",
        "description": "Send all of them (start from the state's `settings`). The server saves\nthem and **restarts**: your connection drops; reconnect and say hello\nagain. Changing only `check_updates` doesn't restart it, and the same\nsettings again do nothing.",
        "tags": [
          {
            "$ref": "#/components/tags/Settings"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `settings`.",
              "const": "settings"
            },
            "settings": {
              "type": "object",
              "description": "All the settings. One you leave out keeps its default (not its current value), so start from the state's `settings`.",
              "properties": {
                "find_airplay": {
                  "type": "boolean",
                  "description": "Use paired AirPlay receivers, such as an Apple TV. Default on."
                },
                "find_chromecast": {
                  "type": "boolean",
                  "description": "Use Chromecasts and Cast groups. Default on."
                },
                "find_upnp": {
                  "type": "boolean",
                  "description": "Use UPnP, DLNA and Sonos renderers. Default off."
                },
                "multiroom": {
                  "type": "boolean",
                  "description": "Offer the multiroom speakers (they need AirPlay and Chromecast both on). Default on."
                },
                "find_cast_groups": {
                  "type": "boolean",
                  "description": "Use the speaker groups made in Google Home too; off, each Chromecast plays on its own. Default off."
                },
                "find_bluetooth": {
                  "type": "boolean",
                  "description": "Play to Bluetooth speakers paired with the server's computer. Default off: headphones paired with it would turn up too."
                },
                "check_updates": {
                  "type": "boolean",
                  "description": "Look for new versions of Samklang Server, Samklang and the satellites once a day. Default on. Off, nothing is fetched from Samklang's website."
                }
              }
            }
          },
          "required": [
            "cmd",
            "settings"
          ]
        },
        "examples": [
          {
            "name": "Settings",
            "payload": {
              "cmd": "settings",
              "settings": {
                "find_airplay": true,
                "find_chromecast": true,
                "find_upnp": false,
                "find_bluetooth": false,
                "multiroom": true,
                "find_cast_groups": false,
                "check_updates": true
              }
            }
          }
        ]
      },
      "cmd_satellite_scan": {
        "name": "satellite_scan",
        "title": "Look for speakers",
        "summary": "Look for Bluetooth speakers in pairing mode.",
        "description": "They appear in its `found`.",
        "tags": [
          {
            "$ref": "#/components/tags/Satellites"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `satellite_scan`.",
              "const": "satellite_scan"
            },
            "id": {
              "type": "string",
              "description": "The satellite's `id`."
            }
          },
          "required": [
            "cmd",
            "id"
          ]
        },
        "examples": [
          {
            "name": "Scan",
            "payload": {
              "cmd": "satellite_scan",
              "id": "28CDC1008DC6"
            }
          }
        ]
      },
      "cmd_satellite_use": {
        "name": "satellite_use",
        "title": "Use a speaker",
        "summary": "Play on that speaker from now on.",
        "tags": [
          {
            "$ref": "#/components/tags/Satellites"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `satellite_use`.",
              "const": "satellite_use"
            },
            "id": {
              "type": "string",
              "description": "The satellite's `id`."
            },
            "addr": {
              "type": "string",
              "description": "The speaker's Bluetooth address, from `found`."
            }
          },
          "required": [
            "cmd",
            "id",
            "addr"
          ]
        },
        "examples": [
          {
            "name": "Use",
            "payload": {
              "cmd": "satellite_use",
              "id": "28CDC1008DC6",
              "addr": "6C:5C:3D:34:BC:5C"
            }
          }
        ]
      },
      "cmd_satellite_quality": {
        "name": "satellite_quality",
        "title": "Sound quality",
        "summary": "Its speaker's sound. It connects to the speaker again.",
        "tags": [
          {
            "$ref": "#/components/tags/Satellites"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `satellite_quality`.",
              "const": "satellite_quality"
            },
            "id": {
              "type": "string",
              "description": "The satellite's `id`."
            },
            "quality": {
              "type": "string",
              "description": "`high` (SBC XQ when the speaker takes it), `standard` or `auto`.",
              "enum": [
                "auto",
                "standard",
                "high"
              ]
            }
          },
          "required": [
            "cmd",
            "id",
            "quality"
          ]
        },
        "examples": [
          {
            "name": "High",
            "payload": {
              "cmd": "satellite_quality",
              "id": "28CDC1008DC6",
              "quality": "high"
            }
          }
        ]
      },
      "cmd_satellite_board": {
        "name": "satellite_board",
        "title": "Sound board",
        "summary": "Analog sound on, with that board, or off.",
        "description": "The satellite remembers it and restarts. Pins wired by hand must be\nGP0–22 or GP26–28 (the radio has GP23–25), with the word clock on the\npin after the bit clock and the data pin on neither clock: the example\nis data on GP18, bit clock GP16, word clock GP17.",
        "tags": [
          {
            "$ref": "#/components/tags/Satellites"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `satellite_board`.",
              "const": "satellite_board"
            },
            "id": {
              "type": "string",
              "description": "The satellite's `id`."
            },
            "board": {
              "type": "string",
              "description": "An `id` from the state's `sound_boards`; `\"\"` (or `none`) for off."
            },
            "data": {
              "type": "integer",
              "description": "For a board wired by hand (`custom`): the data pin, a GP number.",
              "minimum": 0,
              "maximum": 28
            },
            "clock": {
              "type": "integer",
              "description": "For a board wired by hand: the bit clock's pin. The word clock goes on the pin after.",
              "minimum": 0,
              "maximum": 28
            }
          },
          "required": [
            "cmd",
            "id",
            "board"
          ]
        },
        "examples": [
          {
            "name": "A known board",
            "payload": {
              "cmd": "satellite_board",
              "id": "28CDC1008DC6",
              "board": "pimoroni-audio-pack"
            }
          },
          {
            "name": "Wired by hand",
            "payload": {
              "cmd": "satellite_board",
              "id": "28CDC1008DC6",
              "board": "i2s",
              "data": 18,
              "clock": 16
            }
          }
        ]
      },
      "cmd_satellite_update": {
        "name": "satellite_update",
        "title": "Approve an update",
        "summary": "The user approves the satellite's waiting firmware update.",
        "description": "It starts at once, and music on that satellite stops; the satellite\nrestarts into the new firmware.",
        "tags": [
          {
            "$ref": "#/components/tags/Satellites"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `satellite_update`.",
              "const": "satellite_update"
            },
            "id": {
              "type": "string",
              "description": "The satellite's `id`."
            }
          },
          "required": [
            "cmd",
            "id"
          ]
        },
        "examples": [
          {
            "name": "Update",
            "payload": {
              "cmd": "satellite_update",
              "id": "28CDC1008DC6"
            }
          }
        ]
      },
      "cmd_satellite_firmware": {
        "name": "satellite_firmware",
        "title": "Firmware for a new satellite",
        "summary": "The firmware to write to a Pico plugged into your computer.",
        "description": "What makes a Pico a new satellite: the firmware this server carries,\nso it needs no update when it first connects, with no Wi-Fi settings\nand its old pairings erased. Copy the UF2 to the Pico in its boot mode;\nit then opens its setup network.",
        "tags": [
          {
            "$ref": "#/components/tags/Satellites"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `satellite_firmware`.",
              "const": "satellite_firmware"
            },
            "chip": {
              "type": "string",
              "description": "`rp2040` (Pico W) or `rp2350` (Pico 2 W). Anything else counts as `rp2040`.",
              "enum": [
                "rp2040",
                "rp2350"
              ]
            },
            "ref": {
              "type": "string",
              "description": "Any string you like: the answer carries it back, so you can match the two."
            }
          },
          "required": [
            "cmd",
            "chip"
          ]
        },
        "examples": [
          {
            "name": "Pico 2 W",
            "payload": {
              "cmd": "satellite_firmware",
              "chip": "rp2350",
              "ref": "5"
            }
          }
        ]
      },
      "msg_satellite_firmware": {
        "name": "satellite_firmware",
        "title": "Firmware",
        "summary": "The answer to `satellite_firmware`.",
        "description": "Either `image` and `uf2`, or `error`.",
        "tags": [
          {
            "$ref": "#/components/tags/Satellites"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "description": "Always `satellite_firmware`.",
              "const": "satellite_firmware"
            },
            "ref": {
              "type": "string",
              "description": "The `ref` the command carried (`\"\"` when it had none)."
            },
            "chip": {
              "type": "string",
              "description": "The chip it is for.",
              "enum": [
                "rp2040",
                "rp2350"
              ]
            },
            "image": {
              "type": "string",
              "description": "The firmware image's id (its CRC-32, in hex)."
            },
            "uf2": {
              "type": "string",
              "description": "The UF2 file, base64.",
              "contentEncoding": "base64"
            },
            "error": {
              "type": "string",
              "description": "Why there is none (this server carries no firmware for that chip, say)."
            }
          },
          "required": [
            "type",
            "ref",
            "chip"
          ]
        },
        "examples": [
          {
            "name": "Firmware",
            "payload": {
              "type": "satellite_firmware",
              "ref": "5",
              "chip": "rp2350",
              "image": "9a3c51f0",
              "uf2": "<base64>"
            }
          }
        ]
      },
      "cmd_calibrate": {
        "name": "calibrate",
        "title": "Calibrate",
        "summary": "Start or end a calibration of a group.",
        "description": "Needs auto sync on.",
        "tags": [
          {
            "$ref": "#/components/tags/Calibration"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `calibrate`.",
              "const": "calibrate"
            },
            "group": {
              "type": "string",
              "description": "The group to calibrate."
            },
            "on": {
              "type": "boolean",
              "description": "Start (`true`) or end (`false`)."
            },
            "restore": {
              "type": "boolean",
              "description": "Ending: put back the fine-tunes from before."
            },
            "ref": {
              "type": "string",
              "description": "Any string you like: the answer carries it back, so you can match the two."
            }
          },
          "required": [
            "cmd",
            "group",
            "on"
          ]
        },
        "examples": [
          {
            "name": "Start",
            "payload": {
              "cmd": "calibrate",
              "group": "Vardagsrum",
              "on": true,
              "ref": "1"
            }
          },
          {
            "name": "End",
            "payload": {
              "cmd": "calibrate",
              "group": "Vardagsrum",
              "on": false
            }
          }
        ]
      },
      "cmd_chime": {
        "name": "chime",
        "title": "Chime",
        "summary": "Play the chime on one speaker.",
        "description": "The answer says when to record.",
        "tags": [
          {
            "$ref": "#/components/tags/Calibration"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `chime`.",
              "const": "chime"
            },
            "group": {
              "type": "string",
              "description": "The group being calibrated."
            },
            "label": {
              "type": "string",
              "description": "The speaker, by its `label`; `*` plays it on every device at once (not measured)."
            },
            "ref": {
              "type": "string",
              "description": "Any string you like: the answer carries it back, so you can match the two."
            }
          },
          "required": [
            "cmd",
            "group",
            "label"
          ]
        },
        "examples": [
          {
            "name": "Chime",
            "payload": {
              "cmd": "chime",
              "group": "Vardagsrum",
              "label": "Kök",
              "ref": "2"
            }
          }
        ]
      },
      "cmd_heard": {
        "name": "heard",
        "title": "Heard",
        "summary": "Send the recording of a chime.",
        "description": "Record from `record_from_ms` to `record_to_ms` of the `chime` answer (server time: use `clock`).",
        "tags": [
          {
            "$ref": "#/components/tags/Calibration"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `heard`.",
              "const": "heard"
            },
            "group": {
              "type": "string",
              "description": "The group being calibrated."
            },
            "label": {
              "type": "string",
              "description": "The speaker the chime was for."
            },
            "start_ms": {
              "type": "number",
              "description": "The server time of the first sample, in Unix ms."
            },
            "rate": {
              "type": "number",
              "description": "The sample rate in Hz (8000 and up)."
            },
            "pcm": {
              "type": "string",
              "description": "The recording: 16-bit little-endian mono PCM, base64, up to 15 s at 48 kHz.",
              "contentEncoding": "base64"
            },
            "missing": {
              "type": "boolean",
              "description": "There is no recording: send this instead of `pcm`."
            },
            "note": {
              "type": "string",
              "description": "With `missing`: why, for the log. With `calibration_note`: what the app made of a chime."
            },
            "ref": {
              "type": "string",
              "description": "Any string you like: the answer carries it back, so you can match the two."
            }
          },
          "required": [
            "cmd",
            "group",
            "label"
          ]
        },
        "examples": [
          {
            "name": "A recording",
            "payload": {
              "cmd": "heard",
              "group": "Vardagsrum",
              "label": "Kök",
              "start_ms": 1793366400750.0,
              "rate": 48000,
              "pcm": "<base64>",
              "ref": "3"
            }
          },
          {
            "name": "No recording",
            "payload": {
              "cmd": "heard",
              "group": "Vardagsrum",
              "label": "Kök",
              "missing": true,
              "note": "no microphone",
              "ref": "3"
            }
          }
        ]
      },
      "cmd_calibration_skip": {
        "name": "calibration_skip",
        "title": "Skip a speaker",
        "summary": "Leave a speaker out of the result.",
        "description": "No answer, unless the group isn't being calibrated.",
        "tags": [
          {
            "$ref": "#/components/tags/Calibration"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `calibration_skip`.",
              "const": "calibration_skip"
            },
            "group": {
              "type": "string",
              "description": "The group being calibrated."
            },
            "label": {
              "type": "string",
              "description": "The speaker."
            }
          },
          "required": [
            "cmd",
            "group",
            "label"
          ]
        },
        "examples": [
          {
            "name": "Skip",
            "payload": {
              "cmd": "calibration_skip",
              "group": "Vardagsrum",
              "label": "Kök"
            }
          }
        ]
      },
      "cmd_calibration_reset": {
        "name": "calibration_reset",
        "title": "Start over",
        "summary": "Forget what was measured so far.",
        "description": "No answer, unless the group isn't being calibrated.",
        "tags": [
          {
            "$ref": "#/components/tags/Calibration"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `calibration_reset`.",
              "const": "calibration_reset"
            },
            "group": {
              "type": "string",
              "description": "The group being calibrated."
            }
          },
          "required": [
            "cmd",
            "group"
          ]
        },
        "examples": [
          {
            "name": "Reset",
            "payload": {
              "cmd": "calibration_reset",
              "group": "Vardagsrum"
            }
          }
        ]
      },
      "cmd_calibration_result": {
        "name": "calibration_result",
        "title": "Result",
        "summary": "See the result so far.",
        "tags": [
          {
            "$ref": "#/components/tags/Calibration"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `calibration_result`.",
              "const": "calibration_result"
            },
            "group": {
              "type": "string",
              "description": "The group being calibrated."
            },
            "ref": {
              "type": "string",
              "description": "Any string you like: the answer carries it back, so you can match the two."
            }
          },
          "required": [
            "cmd",
            "group"
          ]
        },
        "examples": [
          {
            "name": "Result",
            "payload": {
              "cmd": "calibration_result",
              "group": "Vardagsrum",
              "ref": "4"
            }
          }
        ]
      },
      "cmd_calibration_apply": {
        "name": "calibration_apply",
        "title": "Apply",
        "summary": "Set the fine-tunes the calibration found.",
        "description": "Answered like `calibration_result`.",
        "tags": [
          {
            "$ref": "#/components/tags/Calibration"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `calibration_apply`.",
              "const": "calibration_apply"
            },
            "group": {
              "type": "string",
              "description": "The group being calibrated."
            },
            "ref": {
              "type": "string",
              "description": "Any string you like: the answer carries it back, so you can match the two."
            }
          },
          "required": [
            "cmd",
            "group"
          ]
        },
        "examples": [
          {
            "name": "Apply",
            "payload": {
              "cmd": "calibration_apply",
              "group": "Vardagsrum",
              "ref": "5"
            }
          }
        ]
      },
      "cmd_calibration_note": {
        "name": "calibration_note",
        "title": "Note",
        "summary": "What the app made of a chime, for the server's log.",
        "description": "No answer.",
        "tags": [
          {
            "$ref": "#/components/tags/Calibration"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `calibration_note`.",
              "const": "calibration_note"
            },
            "label": {
              "type": "string",
              "description": "The speaker."
            },
            "note": {
              "type": "string",
              "description": "What the app heard (the log keeps 300 bytes)."
            }
          },
          "required": [
            "cmd",
            "label",
            "note"
          ]
        },
        "examples": [
          {
            "name": "Note",
            "payload": {
              "cmd": "calibration_note",
              "label": "Kök",
              "note": "…"
            }
          }
        ]
      },
      "msg_calibration": {
        "name": "calibration",
        "title": "Calibration",
        "summary": "The answer to `calibrate`.",
        "tags": [
          {
            "$ref": "#/components/tags/Calibration"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "description": "Always `calibration`.",
              "const": "calibration"
            },
            "ref": {
              "type": "string",
              "description": "The `ref` the command carried (`\"\"` when it had none)."
            },
            "on": {
              "type": "boolean",
              "description": "Whether a calibration runs now."
            },
            "speakers": {
              "type": [
                "array",
                "null"
              ],
              "items": {
                "type": "string",
                "description": "A speaker's label."
              },
              "description": "Started: the speakers to calibrate, by label."
            },
            "target_ms": {
              "type": "integer",
              "description": "Started: the group's target delay, in ms."
            },
            "error": {
              "type": "string",
              "description": "Why it didn't start: `auto sync is off`, `no such group`, …"
            }
          },
          "required": [
            "type",
            "ref",
            "on"
          ]
        },
        "examples": [
          {
            "name": "Started",
            "payload": {
              "type": "calibration",
              "ref": "1",
              "on": true,
              "speakers": [
                "Kök",
                "Sovrum"
              ],
              "target_ms": 2000
            }
          },
          {
            "name": "Refused",
            "payload": {
              "type": "calibration",
              "ref": "1",
              "on": false,
              "error": "auto sync is off"
            }
          }
        ]
      },
      "msg_chime": {
        "name": "chime",
        "title": "Chime",
        "summary": "The answer to `chime`: when to record.",
        "description": "Either the times, or `error`.",
        "tags": [
          {
            "$ref": "#/components/tags/Calibration"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "description": "Always `chime`.",
              "const": "chime"
            },
            "ref": {
              "type": "string",
              "description": "The `ref` the command carried (`\"\"` when it had none)."
            },
            "label": {
              "type": "string",
              "description": "The speaker."
            },
            "at_ms": {
              "type": "number",
              "description": "When the chime starts at the live point (server time, Unix ms)."
            },
            "target_ms": {
              "type": "integer",
              "description": "The group's target delay, in ms."
            },
            "offset_ms": {
              "type": "integer",
              "description": "The speaker's fine-tune, in ms."
            },
            "record_from_ms": {
              "type": "number",
              "description": "Record from this server time (Unix ms)…"
            },
            "record_to_ms": {
              "type": "number",
              "description": "…to this one."
            },
            "error": {
              "type": "string",
              "description": "Why there is no chime (`no such group`, `everywhere: not calibrating`, …)."
            }
          },
          "required": [
            "type",
            "ref",
            "label"
          ]
        },
        "examples": [
          {
            "name": "Chime",
            "payload": {
              "type": "chime",
              "ref": "2",
              "label": "Kök",
              "at_ms": 1793366401250.0,
              "target_ms": 2000,
              "offset_ms": 0,
              "record_from_ms": 1793366400750.0,
              "record_to_ms": 1793366407750.0
            }
          }
        ]
      },
      "msg_heard": {
        "name": "heard",
        "title": "Heard",
        "summary": "The answer to `heard`.",
        "tags": [
          {
            "$ref": "#/components/tags/Calibration"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "description": "Always `heard`.",
              "const": "heard"
            },
            "ref": {
              "type": "string",
              "description": "The `ref` the command carried (`\"\"` when it had none)."
            },
            "label": {
              "type": "string",
              "description": "The speaker."
            },
            "heard": {
              "type": "boolean",
              "description": "Whether the chime was found in the recording."
            },
            "message": {
              "type": "string",
              "description": "Something to show the user about it."
            }
          },
          "required": [
            "type",
            "ref",
            "label",
            "heard"
          ]
        },
        "examples": [
          {
            "name": "Heard",
            "payload": {
              "type": "heard",
              "ref": "3",
              "label": "Kök",
              "heard": true,
              "message": "…"
            }
          }
        ]
      },
      "msg_calibration_result": {
        "name": "calibration_result",
        "title": "Calibration result",
        "summary": "The answer to `calibration_result` and `calibration_apply`.",
        "tags": [
          {
            "$ref": "#/components/tags/Calibration"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "description": "Always `calibration_result`.",
              "const": "calibration_result"
            },
            "ref": {
              "type": "string",
              "description": "The `ref` the command carried (`\"\"` when it had none)."
            },
            "speakers": {
              "type": [
                "array",
                "null"
              ],
              "description": "Each speaker asked for, in order.",
              "items": {
                "type": "object",
                "description": "A speaker.",
                "properties": {
                  "label": {
                    "type": "string",
                    "description": "The speaker."
                  },
                  "heard": {
                    "type": "boolean",
                    "description": "Whether its chime was heard."
                  },
                  "detail": {
                    "type": "string",
                    "description": "`skipped`, `heard`, `heard the latest` or `heard N ms early`, to show."
                  },
                  "fix_ms": {
                    "type": "integer",
                    "description": "The fine-tune change it needs, in ms."
                  }
                },
                "required": [
                  "label",
                  "heard",
                  "detail"
                ]
              }
            },
            "measured": {
              "type": "integer",
              "description": "How many speakers were heard."
            },
            "can_apply": {
              "type": "boolean",
              "description": "Whether there is something to apply."
            },
            "applied": {
              "type": "boolean",
              "description": "Whether it was applied."
            }
          },
          "required": [
            "type",
            "ref",
            "speakers",
            "measured",
            "can_apply",
            "applied"
          ]
        },
        "examples": [
          {
            "name": "Result",
            "payload": {
              "type": "calibration_result",
              "ref": "4",
              "speakers": [
                {
                  "label": "Kök",
                  "heard": true,
                  "detail": "…",
                  "fix_ms": -35
                }
              ],
              "measured": 2,
              "can_apply": true,
              "applied": false
            }
          }
        ]
      },
      "msg_not_calibrating": {
        "name": "not_calibrating",
        "title": "Not calibrating",
        "summary": "The answer to a calibration command for a group that isn't being calibrated.",
        "description": "Its `type` is the command's name.",
        "tags": [
          {
            "$ref": "#/components/tags/Calibration"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "description": "The command's name.",
              "enum": [
                "heard",
                "calibration_skip",
                "calibration_reset",
                "calibration_result",
                "calibration_apply"
              ]
            },
            "label": {
              "type": "string",
              "description": "The command's `label` (`\"\"` when it had none)."
            },
            "error": {
              "type": "string",
              "description": "Always `not calibrating`.",
              "const": "not calibrating"
            },
            "ref": {
              "type": "string",
              "description": "The `ref` the command carried (`\"\"` when it had none)."
            }
          },
          "required": [
            "type",
            "label",
            "error",
            "ref"
          ]
        },
        "examples": [
          {
            "name": "Not calibrating",
            "payload": {
              "type": "calibration_skip",
              "label": "Kök",
              "error": "not calibrating",
              "ref": ""
            }
          }
        ]
      },
      "cmd_forget_remote": {
        "name": "forget_remote",
        "title": "Unpair an app",
        "summary": "Unpair an app; its connections close.",
        "description": "Refused from other computers.",
        "tags": [
          {
            "$ref": "#/components/tags/Localonly"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "cmd": {
              "type": "string",
              "description": "Always `forget_remote`.",
              "const": "forget_remote"
            },
            "id": {
              "type": "string",
              "description": "The app's `id`, from `remotes`."
            }
          },
          "required": [
            "cmd",
            "id"
          ]
        },
        "examples": [
          {
            "name": "Unpair",
            "payload": {
              "cmd": "forget_remote",
              "id": "9c1e…"
            }
          }
        ]
      },
      "msg_pairing": {
        "name": "pairing",
        "title": "Pairing code",
        "summary": "The code apps pair with now.",
        "description": "Sent when you connect and whenever the code changes.",
        "tags": [
          {
            "$ref": "#/components/tags/Localonly"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "description": "Always `pairing`.",
              "const": "pairing"
            },
            "code": {
              "type": "string",
              "description": "The 4-digit code."
            }
          },
          "required": [
            "type",
            "code"
          ]
        },
        "examples": [
          {
            "name": "Code",
            "payload": {
              "type": "pairing",
              "code": "4719"
            }
          }
        ]
      },
      "msg_remotes": {
        "name": "remotes",
        "title": "Paired apps",
        "summary": "The apps paired with the server.",
        "description": "Sent when you connect and whenever it changes.",
        "tags": [
          {
            "$ref": "#/components/tags/Localonly"
          }
        ],
        "payload": {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "description": "Always `remotes`.",
              "const": "remotes"
            },
            "remotes": {
              "type": "array",
              "description": "Every paired app.",
              "items": {
                "type": "object",
                "description": "A paired app.",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Its id: what `forget_remote` takes."
                  },
                  "name": {
                    "type": "string",
                    "description": "The name it paired with."
                  },
                  "paired_at": {
                    "type": "string",
                    "description": "When it paired (RFC 3339).",
                    "format": "date-time"
                  },
                  "last_seen": {
                    "type": "string",
                    "description": "When it last connected (RFC 3339; updated at most once an hour).",
                    "format": "date-time"
                  }
                },
                "required": [
                  "id",
                  "name",
                  "paired_at"
                ]
              }
            }
          },
          "required": [
            "type",
            "remotes"
          ]
        },
        "examples": [
          {
            "name": "Apps",
            "payload": {
              "type": "remotes",
              "remotes": [
                {
                  "id": "9c1e…",
                  "name": "Anna's tablet",
                  "paired_at": "2026-10-28T09:12:00Z",
                  "last_seen": "2026-10-30T13:02:11Z"
                }
              ]
            }
          }
        ]
      }
    },
    "tags": {
      "Handshake": {
        "name": "Handshake",
        "description": "The first line you send decides who you are: `pair` once, `hello`\nevery time after. See [Pairing and hello](#pairing-and-hello)."
      },
      "State": {
        "name": "State",
        "description": "Right after the handshake you get the current covers and state, then a\nnew message each time something changes."
      },
      "Playing": {
        "name": "Playing",
        "description": "Commands for a multiroom speaker act on the one playing (else\nEverywhere); name another with `group`. A device is always named by its\n`label`."
      },
      "Sync": {
        "name": "Sync",
        "description": "With auto sync on, the server measures each device's lag and brings\nevery device onto the same delay, `target_ms`; you can fine-tune each\none. With it off, you set each device's delay yourself."
      },
      "Devices": {
        "name": "Devices",
        "description": "A device is always named by its `label`."
      },
      "Groups": {
        "name": "Groups",
        "description": "Everywhere plays on every device not left out. The user's groups play\non their own devices, each as its own AirPlay speaker."
      },
      "Settings": {
        "name": "Settings",
        "description": "What the server looks for. Changing them restarts the server."
      },
      "Satellites": {
        "name": "Satellites",
        "description": "A satellite is a Raspberry Pi Pico W (or Pico 2 W) that plays Samklang on\na Bluetooth speaker. Connected ones are in the state's `satellites`.\nA satellite with a speaker is among the `devices` too.\n\nIts **software**: `version` is the firmware's version, and `update` says\nwhether new firmware waits; `update_to` is the version it brings.\nNothing is installed until a client sends `satellite_update`.\n\nIts speaker's **sound**: `quality` is the setting, and `xq` whether the\nspeaker plays SBC XQ now. High asks the speaker for SBC XQ (dual channel\nat bitpool 47, about 551 kbit/s against 328): better sound, from any\nspeaker that takes it, at about 1.5 times the Wi-Fi and Bluetooth; one\nthat doesn't plays standard SBC. Automatic is high on a Pico 2 W and\nstandard on a Pico W, whose Wi-Fi has less to spare.\n\nA satellite can have **analog sound**: a sound board (an I2S DAC or\namplifier) with a headphone jack. Its jack is then a device of its own,\nnamed in `jack`: in the same group as the speaker, or in another\nplaying another song. No board can be detected, so the user turns\nanalog sound on and says which. Two different songs at once need twice\nthe Wi-Fi, which a Pico W may not have.\n\nThe boards to offer come in the state's `sound_boards`. Show them as\nthey come: the server knows the boards (their pins) and sends the\nsatellite the pins, so a board added to the server works without new\napps or firmware. One with `custom` needs the pins from the user: the\ndata pin and the bit clock, the word clock on the pin after.\n\nGetting a new satellite onto the Wi-Fi happens before it reaches the\nserver, over the satellite's own setup network; that part isn't in this\nAPI."
      },
      "Calibration": {
        "name": "Calibration",
        "description": "Calibration measures with a microphone how late each speaker really is,\nand fixes it. Your app only plays the part a microphone can: it records\nthe moment the server says, and sends the recording back. The server\nfinds the chime in it and works everything out.\n\n1. Start: `calibrate` with `\"on\":true`. The answer lists the speakers.\n2. For each speaker, ask for a `chime`.\n3. Record from `record_from_ms` to `record_to_ms` (server time: use\n   `clock`), and send it with `heard`.\n4. Leave a speaker out with `calibration_skip`; start over with\n   `calibration_reset`.\n5. See the result with `calibration_result`.\n6. Apply it with `calibration_apply`.\n7. End: `calibrate` with `\"on\":false`, and `\"restore\":true` to put back\n   the fine-tunes from before.\n\nSend `clock` now and then while calibrating: it tells the server you are\nstill there. A calibration left alone for two minutes ends."
      },
      "Updates": {
        "name": "Updates",
        "description": "Once a day (unless `check_updates` is off) the server reads Samklang's\nsigned update file and puts what is new in the state's `updates`:\n\n```json\n\"updates\": {\"server\":{\"version\":\"1.1.0\",\"url\":\"https://samklang.wieoapps.com/setup.html\",\"notes\":\"…\"},\n            \"app\":{\"version\":\"1.1.0\",\"url\":\"https://apps.apple.com/…\"},\n            \"checked\":\"2026-11-02T09:00:00Z\"}\n```\n\n`server` is there only when a newer Samklang Server exists: show it with\nits `notes`; the server is updated on the computer it runs on. `app` is\nSamklang's latest version, for Samklang itself; your own client can ignore\nit. Satellites' firmware updates are in each satellite's `update` (see\n[Satellites](#satellites))."
      },
      "Localonly": {
        "name": "Local only",
        "description": "Clients on the server's own computer (connecting to 127.0.0.1) also get\nthe pairing code and the paired apps, so they can show them (as the\nMac's Samklang Server window does), and may unpair an app."
      }
    }
  }
}
