Skip to content

Confirm Sonos announcements from the speaker's clip status (0.3.0) - #7

Merged
will-roscoe merged 6 commits into
mainfrom
fix/sonos-clip-verification
Sep 17, 2026
Merged

will-roscoe merged 6 commits into
mainfrom
fix/sonos-clip-verification

Conversation

@will-roscoe

Copy link
Copy Markdown
Owner

Problem

Broadcasts to a Sonos speaker were reported as unverified, with the summary reading FAILED — played on 0/1 speakers, even when the message was clearly heard.

Home Assistant's Sonos integration plays announcements (so tts.speak) through the speaker's local audio-clip API, and the media player entity does not change state while a clip plays. The intercom confirmed playback purely from entity state, so it could never see a Sonos announcement. That applied to the native Sonos entity and to Music Assistant players backed by the same speaker.

Change

A pluggable playback check (custom_components/intercom/verifiers/):

  • PlaybackVerifier: the base class. It checks nothing and assumes the clip played, and is used when verify: false. Subclasses register themselves when defined, and must define name and matches(hass, entity_id). priority decides between several matches.
  • StateVerifier: the existing entity-state check, moved unchanged. It is the fallback for every player.
  • Checks now return a verdict (started / failed / timeout / assumed), so a player that itself refuses a clip is reported as failed, not unverified.
  • Each player outcome gains verified_by, naming the check that judged it.

SonosClipVerifier (sonos_clip):

  • Finds the speaker's address through the entity and device registries. This covers:
    • native Sonos entities;
    • Music Assistant players whose unique_id is the Sonos player id;
    • Music Assistant players that wrap a native entity.
  • Opens its own connection to the speaker's local websocket API, subscribes to audioClip:1 status, ignores clips the speaker already had, and follows the first new clip. ACTIVE/DONE means played; ERROR means failed, with a message pointing at the usual cause (the speaker cannot reach the audio link Home Assistant gave it).
  • While the connection is up it is authoritative, because a proxy entity can look busy while the speaker refuses the clip. If the connection can't be opened or drops, or if no clip appears at all (audio sent some other way, e.g. AirPlay), the entity-state check decides.
  • No new requirements: aiohttp and sonos_websocket (which ships with HA's Sonos integration) are imported only when a Sonos speaker is actually checked.

Docs: new docs/troubleshooting.md, and README notes on verified_by, the two checks, how to add one, and the limitations. The troubleshooting guide covers the case that exposed this: a Home Assistant host on two subnets hands out an audio link on the one the speaker cannot reach. The speaker accepts the command and plays nothing, and Music Assistant shows ERROR_LOST_CONNECTION.

Version bumped to 0.3.0.

Testing

  • 79 tests pass (37 new), ruff check and ruff format --check clean.

    • New stubs for the entity and device registries match core's lookup signatures.
    • The fake speaker replays the handshake a real Sonos Roam sends.
    • With the Sonos check disabled, the new regression test fails with unverified, as the bug did.
  • Live, against a Sonos Roam on Home Assistant, running this module's clip channel without restarting HA:

    Clip Statuses seen
    tts.speak via the native entity ACTIVEDONE
    tts.speak via the Music Assistant entity ACTIVEDONE
    Announce with an unreachable URL ERROR after ~3 s
  • Address lookup against the real registries: the native entity and both Music Assistant entities resolve to the speaker, and none of the other 7 media players match.

Assisted by claude-opus-5 anthropic@willroscoe.uk

Will Roscoe added 6 commits September 17, 2026 00:06
Playback confirmation was a single hard-coded entity-state watch inside
broadcast.py. Move it behind a small verifier API so players that expose
better evidence can get their own check:

- PlaybackVerifier: base class that checks nothing and assumes the clip
  played (used when verify is off). Subclasses register themselves and
  must define name and matches(hass, entity_id); priority breaks ties.
- StateVerifier: the existing state watch, unchanged in behaviour, and
  the fallback for every player.
- Verdicts are started / failed / timeout / assumed, so a verifier can
  now report that the player itself refused the clip (status failed).
- Each player outcome gains verified_by, naming the check that judged it.

Helped-By: claude-opus-5 <anthropic@willroscoe.uk>
Home Assistant's Sonos integration plays announcements (so tts.speak)
through the speaker's audio-clip API, and the media player entity never
changes state while a clip plays. Every Sonos broadcast was therefore
reported as unverified, and the whole broadcast as FAILED, even when it
was clearly heard.

Add SonosClipVerifier. It finds the speaker behind the entity through the
entity and device registries (native Sonos entities, Music Assistant
players that carry the Sonos player id, and Music Assistant players that
wrap a native entity), opens its own connection to the speaker's local
websocket API, subscribes to audio-clip status and follows the first clip
that appears after it was armed:

- ACTIVE / DONE: played
- ERROR: failed, with a message pointing at the usual cause (the speaker
  cannot reach the address Home Assistant gave it for the audio)

While that connection is up it is authoritative, since a proxy entity can
look busy while the speaker refuses the clip. If it cannot be opened, or
drops, the ordinary entity-state check decides instead.

Helped-By: claude-opus-5 <anthropic@willroscoe.uk>
Add docs/troubleshooting.md covering a speaker that accepts a broadcast
but cannot fetch the audio (a multi-homed Home Assistant host handing out
an address the speaker cannot reach, how to confirm it and how to fix
it), broadcasts reported failed but heard, and where to find the logs.

README: document verified_by and the sonos_clip / state checks, how to
add a check for another kind of player, and that failed now also covers
a speaker refusing the clip.

Helped-By: claude-opus-5 <anthropic@willroscoe.uk>
Helped-By: claude-opus-5 <anthropic@willroscoe.uk>
Audio can reach a Sonos without going through its audio-clip API, e.g.
Music Assistant streaming over AirPlay. The clip channel then stays
silent, and holding back the entity-state evidence would report such
broadcasts as unverified. If no clip has appeared by the start timeout,
use the state evidence after all. Once the speaker has shown our clip,
its status stays authoritative.

Also document that targeting one Sonos through two entities in a single
broadcast is ambiguous.

Helped-By: claude-opus-5 <anthropic@willroscoe.uk>
Where to find an add-on's log is a Home Assistant question, not one about
this integration. Keep only the part that is: a speaker's own errors are
reported to whatever asked it to play, so they surface in that
component's log.

Helped-By: claude-opus-5 <anthropic@willroscoe.uk>
@will-roscoe
will-roscoe merged commit b3f74e1 into main Sep 17, 2026
4 checks 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