# `Espex.Sendspin`
[🔗](https://github.com/bbangert/espex/blob/main/lib/espex/sendspin.ex#L1)

Behaviour for Sendspin adapters (API 1.18).

Sendspin is a protocol for synchronized audio playback. A device that
runs a Sendspin client pairs with a Sendspin server using a pairing
token. Home Assistant reads that token from the device over the native
API and hands it to the server, so the user never types it.

Implement this module when the host runs a Sendspin client. espex
reports the client in `DeviceCapabilitiesResponse.sendspin` and
answers `SendspinPairingTokenRequest` from your callbacks. Without a
`:sendspin` adapter, espex reports no Sendspin capability and does not
answer the token request, as ESPHome firmware built without Sendspin.

## Callbacks

| Callback | Purpose |
|----------|---------|
| `c:client_id/0` | The client's id, `nil` before it has started |
| `c:pairing_token/0` | The pairing token, or why there is none |
| `c:feature_flags/0` | Optional. Supported features, default `pairing_token_feature/0` |

## Values change at runtime

The client id and token do not exist until the Sendspin client has
started once. espex calls the callbacks on every request instead of
capturing them when a connection is accepted, so return the current
values each time. Keep the callbacks fast: they run in the connection
process.

Clients read capabilities on every connect. If `c:client_id/0` or
`c:feature_flags/0` raises, or returns something other than `nil` or a
UTF-8 string of at most 43 bytes and a non-negative integer, espex logs
it and reports an empty client id rather than dropping the connection.

## Pairing token

`c:pairing_token/0` returns one of these, sent as
`SendspinPairingTokenResponse.status`:

| Return | Status | Meaning |
|--------|--------|---------|
| `{:ok, token}` | `OK` | The token, sent to the client |
| `:not_ready` | `NOT_READY` | The client has not started yet; ask again later |
| `:disabled` | `DISABLED` | Sendspin is off and has not started since boot |
| `:failed` | `FAILED` | The client failed to start and will not until restart |

The token must be a non-empty UTF-8 string of at most 127 bytes. If the callback raises or
returns anything else, espex logs that it did, without the value, and
answers `FAILED`.

## Encryption is required

The token is a long-lived secret. espex sends it only on a connection
encrypted with the Noise protocol. On a plaintext connection, espex
answers `ENCRYPTION_REQUIRED` without calling `c:pairing_token/0`. A
key provisioned at runtime applies to later connections only, so a
client must reconnect after provisioning before it can read the token.

espex never logs the token. Your adapter must not log it either.

## Example

    defmodule MyApp.SendspinAdapter do
      @behaviour Espex.Sendspin

      @impl true
      def client_id, do: MyApp.Sendspin.client_id()

      @impl true
      def pairing_token do
        case MyApp.Sendspin.status() do
          :running -> {:ok, MyApp.Sendspin.pairing_token()}
          :starting -> :not_ready
          :off -> :disabled
          :crashed -> :failed
        end
      end
    end

## Wiring

    Espex.start_link(
      device_config: [name: "speaker", psk: psk],
      sendspin: MyApp.SendspinAdapter
    )

# `pairing_token_result`
*since 0.12.0* 

```elixir
@type pairing_token_result() :: {:ok, String.t()} | :not_ready | :disabled | :failed
```

What `c:pairing_token/0` returns; see the moduledoc table.

# `client_id`
*since 0.12.0* 

```elixir
@callback client_id() :: String.t() | nil
```

The Sendspin client's id: the base64url encoding of its static public
key, 43 characters. Return `nil` or `""` before the client has started.

# `feature_flags`
*since 0.12.0* *optional* 

```elixir
@callback feature_flags() :: non_neg_integer()
```

Bitmask of the Sendspin features the client supports. Optional; the
default is `pairing_token_feature/0`, the only feature defined.

# `pairing_token`
*since 0.12.0* 

```elixir
@callback pairing_token() :: pairing_token_result()
```

The pairing token, or why there is none. Called only on an encrypted
connection. Never log the token.

# `pairing_token_feature`
*since 0.12.0* 

```elixir
@spec pairing_token_feature() :: non_neg_integer()
```

The `PAIRING_TOKEN` feature bit (`1 <<< 0`): the device answers
`SendspinPairingTokenRequest`. Clients send the request only when this
bit is set.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
