Conversation
Volume control stopped working today with no visible symptom: the faders moved, /status reported sensible levels, nothing was marked unavailable, and the speakers ignored everything. The journal showed the requests arriving and the ratio being computed, but no writes: === Volume adjust (ratio) delta=-0.050 === [nest_audio] set_volume failed: [google_home] set_volume failed: [chromecast] set_volume failed: _init_casts() opens one connection per speaker at startup and nothing ever reopens them. All three had died, so every write threw — and two things kept it hidden. cast.status keeps serving the last cached levels after the socket dies, so /status looked healthy; and since volume writes became asynchronous the HTTP response returns ok:true as soon as a target is queued, so the failure only ever reached the journal. Restarting the service was the only cure. Three changes: - Each write now gets two attempts. If the first throws, the connection is dropped and reopened, so turning the dial is enough to recover. The disconnect runs on a throwaway thread because tearing down a dead socket can block, and the writer must not stall. Reconnecting is per-speaker, so one dead speaker cannot hold up the others. - _speaker_unavailable checks socket_client.is_connected, so a dropped link is reported immediately rather than waiting for a write to fail. The fader dims and shows "--" instead of looking live. - The last write error per speaker is tracked and surfaced through /status, which is now the only channel that can carry it. Also logs the exception type: a dead pychromecast socket raises with an empty str(), which is why the failures above printed no reason. Verified with fakes standing in for the connections: a dead socket is reported before any write is attempted, the retry lands the volume on the reopened connection and clears the error, a speaker that is genuinely gone reports "offline" without retrying forever, and a failure on one speaker leaves the others alone. On the device, connections open at startup and adjusts land as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
Volume control stopped working with no visible symptom — faders moved,
/statusreported sensible levels, nothing was marked unavailable, and the speakers ignored everything. The journal showed requests arriving and the ratio being computed, but no writes:_init_casts()opens one pychromecast connection per speaker at startup only, and nothing ever reopens them. All three had died, so every write threw.Two things kept it hidden:
cast.statuskeeps serving the last cached levels after the socket dies, so/statuslooked healthy andunavailablereported nothing.ok:truethe moment a target is queued. The failure only ever reached the journal. That regression came in withb427c33, which traded visible per-speaker failure for a 14× latency win without giving the failure another route out.Restarting the service was the only cure, and the only symptom was a fader that moves while nothing happens.
The fix
_speaker_unavailable()now checkssocket_client.is_connected, so a dropped link is reported immediately instead of waiting for a write to fail. The fader dims and shows--rather than looking live./status, which is now the only channel that can carry it.Also logs the exception type — a dead pychromecast socket raises with an empty
str(), which is why the failures above printed no reason at all.Testing
iptablesisn't installed on the Pi and the kernel lacksINET_DIAG_DESTROY, so neither blocking traffic norss -Kcould kill a live socket. Verified the logic with fakes standing in for connections instead:On the device: connections open at startup and adjusts land as before —
Not covered: the real-world path where a socket dies mid-session is verified by construction rather than by observation, for the tooling reasons above. Recovery is also write-triggered — a speaker that drops while idle shows as dimmed but only reconnects on the next dial turn. A periodic health check would close that gap if it proves annoying.
🤖 Generated with Claude Code