Skip to content

Replace sonor with sinuous-client - #167

Merged
abusch merged 8 commits into
mainfrom
sinuous-client
Oct 8, 2026
Merged

abusch merged 8 commits into
mainfrom
sinuous-client

Conversation

@abusch

@abusch abusch commented Oct 8, 2026

Copy link
Copy Markdown
Owner

Switch from UPnP via sonor to the speakers' local websocket API through the sinuous-client crate, and build on it.

Changes

  • Replace sonor with sinuous-client: commands go to the right player through sinuous_client::Household, and the group list now picks up topology changes made from other controllers. The websocket API can't read the queue, so the Queue view now shows the current and next tracks. The Favorites view lists every favorite (not just playlists) and plays them with loadFavorite. This commit also fixes Shift-Tab (BackTab), selecting the previous group from the first one, and empty Spotify Connect tracks showing as "Unknown".
  • Move UI state out of the Sonos service: the view mode, selected group and favorites cursor now live in the UI, and the service publishes a HouseholdState snapshot on a watch channel. Each command runs in its own task, one at a time, so a slow speaker no longer holds up the UI. There are new screens for connecting, for a failed connection, and for errors.
  • Connect to the first speaker that answers discovery using discover_stream, which cuts the time to the first screen from about 2.8s to under 1s.
  • Use events instead of polling: the service subscribes to playback, metadata, volume and favorites events instead of polling every second. The UI works out the playback position between events.
  • Upgrade sinuous-client to 0.2.0, then rely on it to restore subscriptions: the household now restores subscriptions after a dropped connection and moves them to a group's new coordinator. The 60s resync timer and the handling of coordinator changes are therefore gone. Failed subscriptions are retried every 30s, and the service still resubscribes to everything when it misses events.
  • Clean-ups: use sinuous-client from crates.io and remove unused dependencies.

Testing

  • cargo clippy --all-targets: no issues
  • cargo test: passes

🤖 Generated with Claude Code

abusch and others added 8 commits October 8, 2026 22:19
Talk to the speakers through their local websocket API, using the
sinuous-client crate, instead of UPnP through sonor. Commands are
routed to the right player by sinuous_client::Household, and the
group list now follows topology changes made from other controllers.

The websocket API has no way of reading the queue, so the Queue view
now shows the current and next tracks. The Favorites view lists all
favorites (not just playlists) and plays them with loadFavorite.

Also fix Shift-Tab (crossterm reports it as BackTab), selecting the
previous group from the first one, and empty tracks reported by
suspended Spotify Connect sessions showing as "Unknown".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The view mode, selected group and favorites cursor now live in the UI,
so navigating no longer waits for the speakers. The service publishes a
HouseholdState snapshot on a watch channel, and the UI tells it which
group to keep up to date.

Commands now name the group (and favorite) they apply to, and run one
at a time in their own task, so that a slow speaker doesn't hold up
state updates or key handling.

The UI also shows a screen while connecting, why connecting failed,
and errors from commands and refreshes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Discovery used to wait for its whole 2s timeout before connecting to
anything, although players answer within a few hundred milliseconds.
Use sinuous_client::discover_stream (new in sinuous-client 0.1.1) to
connect to the first player as soon as it answers, which brings the
time to the first screen from about 2.8s down to under 1s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The service used to poll the selected group every second. It now
subscribes to the playback, metadata and volume events of every group,
and to favorites events, and keeps the state up to date from them:

- Subscriptions run in the background, as they may need to connect to
  other speakers, and are made again for groups that appear in the
  topology or change coordinator.
- Favorites are fetched when their version changes, including from the
  initial event sent on subscription.
- Speakers re-send their whole state when subscribed to again, so the
  service does that every minute and when it misses events, to catch up
  with anything lost, e.g. on a dropped connection.
- Commands no longer trigger a refresh: the speakers send events about
  what changed.

As speakers only report the playback position when something changes,
the UI works it out from the last one reported, and redraws on a timer
while it advances or an error is shown.

The UI no longer needs to tell the service which group it shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Household::connection is now async, as it reconnects to the player if
the connection was lost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since 0.2.0, the household restores subscriptions when a connection
drops, and moves group subscriptions to the group's new coordinator. So
there is no need to resubscribe every minute, or on coordinator changes.

Subscriptions that failed (which the household doesn't keep) are still
retried periodically, and everything is still resubscribed to when
events are missed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@abusch
abusch merged commit 3ac59ac into main Oct 8, 2026
1 check passed
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.

1 participant