Espex.Sendspin behaviour (espex v0.12.0)

Copy Markdown View Source

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

CallbackPurpose
client_id/0The client's id, nil before it has started
pairing_token/0The pairing token, or why there is none
feature_flags/0Optional. 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 client_id/0 or 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

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

ReturnStatusMeaning
{:ok, token}OKThe token, sent to the client
:not_readyNOT_READYThe client has not started yet; ask again later
:disabledDISABLEDSendspin is off and has not started since boot
:failedFAILEDThe 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 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
)

Summary

Types

What pairing_token/0 returns; see the moduledoc table.

Callbacks

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

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

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

Functions

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

Types

pairing_token_result()

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

What pairing_token/0 returns; see the moduledoc table.

Callbacks

client_id()

(since 0.12.0)
@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)
@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)
@callback pairing_token() :: pairing_token_result()

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

Functions

pairing_token_feature()

(since 0.12.0)
@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.