Skip to content

Bump aiosendspin to 9.1.1 and wire up pairing/encryption - #277

Open
OnFreund wants to merge 3 commits into
Sendspin:mainfrom
OnFreund:modernize-aiosendspin-pairing
Open

Bump aiosendspin to 9.1.1 and wire up pairing/encryption#277
OnFreund wants to merge 3 commits into
Sendspin:mainfrom
OnFreund:modernize-aiosendspin-pairing

Conversation

@OnFreund

@OnFreund OnFreund commented Sep 3, 2026

Copy link
Copy Markdown

sendspin-python-cli was pinned to aiosendspin~=6.0.1, three major versions and 77 commits behind, and never adopted the encryption/pairing subsystem that landed in that gap. This bumps the dependency to 9.1.1 and:

  • Persists a per-client Identity (X25519 keypair) and FileClientPairingStore under --settings-dir (now available for the TUI too, not just daemon).
  • Wires PairingSupport into both SendspinClient constructions (TUI, daemon), rendering a dynamic pairing PIN in a new "Pairing Required" TUI panel or logging it in daemon mode.
  • Fixes everything else the version bump broke: the client-state enum removal, server/hello restructuring, ServerInfo/GoodbyeReason changes, and the server-side identity/pairing_store constructor change (affects sendspin serve too).
  • Fixes a real bug surfaced by live testing against a Music Assistant server: SendspinClient.attach_websocket() now blocks for the connection's entire lifetime once admitted (previously it returned right after handshake), so the daemon's server-initiated listener never reached its own post-handshake audio/MPRIS wiring. Audio chunks flowed at the protocol level but never reached local playback. Fixed by running attach_websocket() as a background task and polling client.connected, matching the pattern aiosendspin's own tests use for this scenario.

sendspin-python-cli was pinned to aiosendspin~=6.0.1, three major versions
and 77 commits behind, and never adopted the encryption/pairing subsystem
that landed in that gap. This bumps the dependency to 9.1.1 and:

- Persists a per-client Identity (X25519 keypair) and FileClientPairingStore
  under --settings-dir (now available for the TUI too, not just daemon).
- Wires PairingSupport into both SendspinClient constructions (TUI, daemon),
  rendering a dynamic pairing PIN in a new "Pairing Required" TUI panel or
  logging it in daemon mode.
- Fixes everything else the version bump broke: the client-state enum
  removal, server/hello restructuring, ServerInfo/GoodbyeReason changes,
  and the server-side identity/pairing_store constructor change (affects
  `sendspin serve` too).
- Fixes a real bug surfaced by live testing against a Music Assistant
  server: SendspinClient.attach_websocket() now blocks for the connection's
  entire lifetime once admitted (previously it returned right after
  handshake), so the daemon's server-initiated listener never reached its
  own post-handshake audio/MPRIS wiring. Audio chunks flowed at the
  protocol level but never reached local playback. Fixed by running
  attach_websocket() as a background task and polling client.connected,
  matching the pattern aiosendspin's own tests use for this scenario.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@OnFreund
OnFreund requested a review from balloob September 3, 2026 13:53
Spell out that the daemon-mode PIN is log-only (journalctl under systemd)
with no unattended-pairing surface, since that's easy to miss compared to
the TUI's dedicated panel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@chriscn

chriscn commented Sep 7, 2026

Copy link
Copy Markdown

I think the upgrade path for a previously installed and paired server is broken:

I ran the following:

uv tool uninstall sendspin
uv tool install "git+https://github.com/OnFreund/sendspin-python-cli.git@modernize-aiosendspin-pairing"

I get the following logs:

Sep 07 17:28:11 living-room systemd[1]: Started sendspin.service - Sendspin Multi-Room Audio Client.
Sep 07 17:28:14 living-room sendspin[43322]: INFO:sendspin.audio_devices:Using audio device 1: RPi DigiAMP+: Raspberry Pi DigiAMP+ HiFi pcm512x-hifi-0 (hw:2,0)
Sep 07 17:28:14 living-room sendspin[43322]: INFO:sendspin.cli:Using ALSA mixer volume control: card 2, element 'Digital'
Sep 07 17:28:14 living-room sendspin[43322]: INFO:sendspin.audio:C volume extension unavailable; falling back to numpy (slower)
Sep 07 17:28:15 living-room sendspin[43322]: INFO:sendspin.daemon.daemon:Starting Sendspin daemon: gWJGPmeWtQRJjW15I7vJ2nqOzK31MmK7J9uk94kZsw4
Sep 07 17:28:15 living-room sendspin[43322]: INFO:sendspin.daemon.daemon:Listening for server connections on port 8928 (mDNS: _sendspin._tcp.local.)
Sep 07 17:28:15 living-room sendspin[43322]: INFO:aiosendspin.client.listener:ClientListener started on port 8928, path /sendspin
Sep 07 17:28:16 living-room sendspin[43322]: INFO:aiosendspin.client.listener:mDNS advertising client 'gWJGPmeWtQRJjW15I7vJ2nqOzK31MmK7J9uk94kZsw4' on port 8928 with path /sendspin
Sep 07 17:28:29 living-room sendspin[43322]: INFO:sendspin.daemon.daemon:Server connected
Sep 07 17:28:29 living-room sendspin[43322]: INFO:sendspin.audio_devices:Detected 32 supported audio formats (FLAC + PCM)
Sep 07 17:28:29 living-room sendspin[43322]: INFO:aiohttp.access:192.168.0.6 [07/Sep/2026:17:28:29 +0100] "GET /sendspin HTTP/1.1" 101 0 "-" "Music Assistant/2.10.2 aiohttp/3.14.3 Python/3.14"

But nothing plays out of the speakers.

Addresses feedback on PR Sendspin#277: upgrading from a pre-pairing install
replaces the old free-text client_id with one derived from a generated
identity's public key, so the client now looks like a new device to any
server that keyed player state off the old client_id. That change itself
can't be avoided (client_id is cryptographically bound to the identity's
keypair, not an arbitrary label), but:

- Log a one-time warning when a fresh identity is generated for a settings
  dir that already had an old-style client_id, explaining what happened
  and that the player may need to be re-added on the server.
- Populate DeviceInfo.mac_address (a stable hardware identifier the
  protocol already supports but this client never sent) so servers that
  support device-based reconciliation have something durable to key off,
  independent of client_id.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@OnFreund

OnFreund commented Sep 7, 2026

Copy link
Copy Markdown
Author

Thanks for testing this against a real Music Assistant setup — really helpful.

I think what you're hitting is a real (and somewhat unavoidable) consequence of adding
encryption/pairing, not a bug in the streaming path itself: client_id used to be a
stable, often user-chosen string (sendspin-cli-<hostname>, or --id). With pairing, it's
now derived from a generated cryptographic identity's public key instead, since that's
what the Noise handshake actually authenticates. That means upgrading a previously-running
install makes the client look like a brand-new device to Music Assistant — your old
player record (and anything pointed at it — zones, groups, HA entities) doesn't carry
over, since MA keys its player registry by client_id. There's no way to make the new
identity's public key equal the old free-text string; they're fundamentally different
kinds of value.

I just pushed two things that address what's actually in our control here:

  • A one-time warning logged when a fresh identity replaces an old-style client_id,
    explaining what happened.
  • DeviceInfo.mac_address is now populated (the protocol already had the field, we just
    weren't sending it) — a stable hardware identifier independent of client_id, in case
    MA's newer versions do any device-based reconciliation with it.

That said, I want to make sure we're not also looking at a second, separate bug: your log
stops right after the WS upgrade (GET /sendspin ... 101) — no "Pairing required" PIN, no
"Stream started". Could you confirm a couple of things?

  1. Did a new player show up in Music Assistant's device list, distinct from your
    previously-configured one?
  2. If you select that new player and try to play something, does audio come through, or
    does it stay silent even then?
  3. Does the log ever progress past that WS-upgrade line if you wait a bit longer (a pairing
    PIN, a "Stream started" line, or a timeout warning), or does it just stay silent forever?

If audio genuinely doesn't work even on the new player once selected, that's a distinct bug
from the identity-continuity issue and I'd like to chase it down.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants