[Release] Sonos Advanced Controller

I don't really have an opinion on this, as I tend to use volume presets for all my various groups and don't adjust them often. And typically when I do, I adjust the Group Volume via a virtual dimmer, so I can tell my voice assistant to "increase volume by 5%" or whatever,

Overall, I'm finding the last couple of updates have really made for a smooth and reliable experience, and surely full release is right around the corner. Thank you for all your hard work to bring this app to life.

When things settle down in a few weeks, months, or whatever, I'm wondering if you might take another look at the "speak" commands. I'm still thrown a bit that these announcements only play on the dominant speaker in a pair. My wife noticed it yesterday when I was testing the speak commands on your app, and said that, for her, it sometimes made the announcements harder to hear.

The node-sonos-http-api I still use for some automations somehow manages to send the announcement to both speakers in a pair. Perhaps there's a solution buried in there somewhere?

Perfectly in sync? And the same 'doesn't interrupt what you're playing, even if you're playing an AirPlay stream'?

This is what Sonos gives for the left channel:
image

and for the right:
image

So, technically, it should be possible to discovery the right channel, add their IP to the data I keep on the left, then have it do loadAudioClip API on both, but I don't see a benefit to that if it's not in sync.

Adding it isn't even all that hard, since the info that the speakers come back with specifically lists not just their RINCON, but the RINCON of their 'primaryDeviceId' which is the left channel in the pair:
image

Edit: looks very much like they use the same interrupting (and possibly failing to restore entirely) method that the built-in Sonos app on Hubitat uses:
image

Eventually, this sort of snapshot and "hope for the best and attempt to restore" is what the "Sonos Advanced Snapshot" device driver is for... which is currently just in my backlog.

That said, it's still just "hope for the best". If you're playing something via AirPlay, there's no way to restore that. Sure, I can tell the speakers "hey, go play this avTransportUri and use this metadata for it!" but that'll never work with AirPlay. When you do this sort of interrupting TTS it 'steals' the speaker away from whatever was AirPlaying to it, and no amount of trying to restore will make your iPhone, iPad, Mac, whatever was AirPlaying previous start back up. Just because a Sonos speaker wants to play the stream again doesn't mean it can actually control your iPhone and make it start playing again.

My family primarily uses the speakers in the house via AirPlay, so that's one of the primary reasons I started on this path of making an app that uses the non-interrupting loadAudioClip API, as it doesn't even stop the AirPlay stream at all, just lowers the volume, so there's nothing to restore. It just keeps on playing.

I'm also neutral on the volume change subject, but only because I don't really use it currently in my automations.

One thing I do miss from stock integration is the ability to specify a +/- "boost" for TTS volume on a specific player. I found it nice in rooms with more ambient noise or kids using headphones to make sure announcements are heard. Not sure if it's possible using the group feature though?

1 Like

Yeah, it's easy enough. The TTS stuff requires a volume be sent, so if you're using one of the various ways to send a TTS that doesn't have a volume option, it just takes 'current volume' and sends that. It is easy enough to add a "boost" setting then just send 'current + boost' to the API.

I'll toss that in on the next release.

Yes, perfectly in sync. But it does interrupt whatever you're playing. If I have to choose between announcements playing on two speakers or having non-interrupting TTS, I'd definitely choose the latter.

I typically used this feature when speaking to several speakers using a volume, but added 15 to the living room (Home Theater) so the announcement was more likely to be heard if someone is watching/listening to something on the audio system. Would this still function the same way?

So I was just playing around here on my stereo pair...

If I tossed in a quick second API call to the right channel like so (hard-coded for testing only):

It plays on both. It's not perfectly in sync tho. Not "echo-y" by any means, but not 100% perfect either. Maybe a couple milliseconds of de-sync, like 5ms or something. It's so close that I can only tell it's not 100% maybe half the time I've tried it. The other half the time it's so close I can't even tell.

Putting this in as an option would require updating the discovery code so it finds the right-channel speakers, then adding their IP to the left channel data fields. And I'd for sure put this in with an optional toggle, defaulting to off. But I should be able to get TTS on both speakers, not synced, but probably good enough for most use cases.

Comparatively, if you've ever had Rule Machine do a TTS with multiple speakers selected, it's NOTHING like that. Doing that in Rule Machine has very noticeable differences because Rule Machine isn't sending the commands to the speakers quickly enough. There's easily 10-50ms between each command sent out to each speaker when selecting multiple speakers in RM.

To get an idea of how 'not 100% synced but pretty close' a stereo pair could be, just use the 'speak' command on a Group Device. At least for me, I literally can't tell that isn't 100% in sync. There's more delays just from the speed of sound not being all that fast than there is from it looping through the code.

The one big caveat is that all of this relies on the speakers themselves responding in approximately the same amount of time. Since it's not an actual time-synced data stream, it could be 99% perfect 99% of the time, then randomly have a speaker just take an extra second or two to start up and be wildly out of sync. That would probably entirely depend on your network setup, whether you have newer Sonos speakers with fast CPUs inside mixed with older ones with slower CPU, etc.

1 Like

I guess I'm not discerning enough to be able if my speakers are ever a few milliseconds out of second, because I've used the node.js for years and never noticed it. But no doubt this would have to be optional to accommodate those who are either more discerning or have system that are slower to respond.

That's because that node.js library you linked IS in sync because it's using the interrupting method to play TTS and that's why it's on both speakers. It's not playing the same TTS on both speakers, it's playing a single TTS to the pair 'device'.

A point I overlooked. So, just as a test, I threw together an asynchronous POST request to the Sonos API in webCoRE to my left and right office speakers with a longer audio clip that mixes various beeps, music, and speech.

I still couldn't discern a delay, but that probably says more about my hearing than anything else.

I did like a dozen tests and I could barely hear the faintest delay on maybe half of them... maybe. It's really really close to synced.

I'll work on getting it in Sonos Advanced, and toss a toggle on it for people that have better hearing than you or I (or other issues, network issues, whatever, that might cause it to be more than 'just a few milliseconds'). But yeah, totally can just make it speak on both, and it's in sync enough that I can't even tell otherwise.

It'll take a bit to code up the new parts for discovery that'll query for right-channel speakers and add their info to the left channel, so it'll be this weekend at the earliest.

1 Like

I seem to have an issue with the player status after updating HE to 2.3.8.112. The Sonos app is on the latest version.

Status always says playing, even when stopped/paused, it doesn't seem to be updating. I noticed as I use 'toggle play/pause' on my IKEA remote so that doesn't work

image

I've tried another player and that seems fine though? I've tried Initialize but that didn't work, no errors showing in the logs. I have 5 sid's showing. Any idea how to fix it?

Update: Seems to be okay now :person_shrugging:

Yeah... There's some issue with Failed to acquire semaphore for method... that I'm trying to get figured out.

I'm not sure what the cause is. There's all of like 5 posts on the entire forum here with that, and none of them are helpful. I've narrowed it down to getting stuck 'playing' whenever there's a thread trying to obtain a semaphore, and a few minutes it seems to clear up.

It shouldn't even be trying to obtain a semaphore since both the driver and app are explicitly single threaded, so that makes it even more weird.

I've got another big update in the pipeline that's bringing in as much as I can via web socket. I did a bunch of trial and error to figure out what I could do with web socket on these speakers, and it turns out that all of th subscribe methods documented on the Sonos Cloud API also work via local web socket. That opens up stuff like subscribing to the loadAudioClip namespace and getting notified when a non-interrupting TTS/MP3 starts playing and when it stops playing, so I'll be able to add a queue to these, so you can have automations just fire TTS away and each one will wait on the previous to finish before moving onto the next.

There's a few other nice things that opens up, too. I'm also working on moving as much code as I can back down into the driver level and out of the app. I kept running into some race conditions with the code being in the app with it set as multithreaded, so I set them as single threaded, which fixed a few race conditions but I think it created the semaphore issue. By moving more code to the driver, I should be able to avoid those race conditions and let everything run multithreaded again.

It's a fairly big change, and I'm making sure it's all working properly before putting it on HPM.

From what I understand of Sonos' roadmap, I think they're moving away from all of the UPnP stuff that I've been using and toward a web socket API. Right now they have NO local API, officially. They've stated repeatedly that they are working on one, and I'm going to assume it'll be web socket based, since it needs to be bi-directional, and that's a lot easier than a bunch of REST callbacks. So if/when they do shut down the UPnP stuff and flush out the web socket API to provide all the stuff it currently lacks and make it a full replacement for UPnP, I'll be in a good spot code-wise to move the remaining UPnP stuff to web socket with minimal fuss, too.

So give me a couple days here and I'll get an update that should be quite a bit faster all around, have a few big new features (like queued non-interrupting TTS that works on both speakers in a stereo pair), and hopefully be a bit more future proof for whenever Sonos releases a supported local API.

7 Likes

Update on current progress...

I'm almost ready for a v0.4.0 release.

This has a TON of changes:

It uses websocket connections wherever possible, this is a bit faster than HTTP requests, and allows for subscribing to a few more things than I could previously subscribe to, such as loadAudioClip and favorites.

Being able to subscribe to loadAudioClip (the API that allows for the non-interrupting TTS/MP3 playing) means I know exactly when the previous message has finished. And since I know that, I added a QUEUE to these. You can send non-interrupting TTS or other MP3 URIs to your Sonos left, right, and center and it'll just queue them all up and play them in order. No overlapping. No long gaps between messages. No gaps between messages at all, actually! No need to futz around with crap like "wait until stopped -> send next". None of that. Just send away and Sonos Advanced will take care of the rest.

I've also added a "high priority" version of the non-interrupting commands, so if you have some long TTS playing or something, and say you have an MP3 that plays when your doorbell rings, you can set that off with the high priority one and it'll play immediately rather than being queued up. And it'll play right over top of what's playing, in a non-interrupting way, AND if there's a low priority TTS or other URI being played, it plays non-interrupting right over top of it, too. So let's say you have some minute long "morning annoucement TTS" that's playing and someone rings the doorbell... the morning announcement will have "ducked" (lowered) the volume of your music so it can play non-interrupting, then the doorbell with duck both the music and the other TTS so it can play, then when it's done, it goes back to the previous TSS and when that's done back to your music.

Device Discovery now finds "secondary" speakers and adds their IP to the primary's "Data" section. On the primary (left channel, usually) there's a new preference for "Create child device right channel? (stereo pair only)". If this is enabled, it creates a child device for the right speaker, which doesn't do much, but one thing it does do is maintain its own websocket connection to the right channel speaker, and then any of the non-interrupting messages play on BOTH speakers. It's not properly time-sync'd, but it's within a few milliseconds and for the most part I can't even tell that it's just playing on both independently.

There's no (current) way to properly play anything with loadAudioClip on both speakers, it's just how Sonos made it. I can play it independently on both, with it close enough to synced. If you find the right channel 'echo-y' and out of sync, just turn the toggle back off and it'll just play on the left speaker.

Sonos has a built-in chime it can play with loadAudioClip, and since I can queue things up back-to-back with zero gap between them, that means I can queue up that chime to play before any TTS messages. So if you want a chime before your speakers just start shouting at you, there's an option for that now, too. Or leave it off and it'll just play the TTS with no chime.

I've reorganized and optimized quite a bit of the code, and many more parts are @CompileStatic annotated. In my testing so far I'm finding the 0.4.0 branch to be LESS CPU usage than the built-in Sonos app. Right now I'm seeing just under 2% total CPU usage by both the app and all of the driver code, and that's with pretty heavy development being done AND logging a TON of crap at trace level. If I turn trace logging off, it's closer to 1% total usage. When I ran the built-in Sonos app, it always totaled around 2-2.5% for me, so Sonos Advanced being less than that is pretty amazing.

As part of the Favorites matching, I've added an optional "Favorites" child device. You can toggle it on any/all of your player devices. I recommend only using one, since it'll just be duplicated data anyway. All the Favorites child does is keep a copy of the latest favorites in its state, so you don't have to click on "getFavorites" each time you want to see what number a favorite is.

With this addition, I'm also able to now subscribe to the favorites namespace using web sockets. So if you add/remove/change favorites, Sonos Advanced immediately updates the cached list of favorites it uses for matching, and the favorites state on your favorites child device.

The "is a favorite playing" code has been rewritten significantly too. It now runs FAR less often. So it's now triggered when a new track starts playing, and 95% of the time there's just a single call to "is a favorite playing". But like 5% of the time Sonos sends duplicate "new track started playing" messages with only "current position" changed, so it's not quite as low as possible, but it's close. And it's been optimized a lot too, to the point where it should have almost no impact on CPU usage overall, so feel free to turn it back on if you had it off, you'll probably not even notice a difference in resource usage.

Next step is pulling in some more websocket goodness to the group/ungroup/join/unjoin stuff, and adding a few more features to the Group Devices.

And I'll be adding support for "Sonos Playlists" in the same manner that Favorites are supported now. Well, at least for loading them. Haven't looked into whether there's any way to know whether there's a Sonos Playlist 'currently playing' like I can for favorites.

So lots of good stuff coming, probably this weekend, maybe sooner.

10 Likes

@daniel.winks

Just to let you know, I am very happy :grinning: to say that I have removed HE's built-in Sonos Integration app and completely migrated all my HE RM's to use your awesome Sonos Advanced Controller (SAC). I must say, looking over your massive code streams, you have remarkable groovy programing talents that have helped me improve several of my custom apps.

Your SAC group controller is holding my 13 Sonos speakers in check. Before SAC, a Sonos speaker would un-join itself randomly and I had used SoCo-CLI Python code running on a raspberryPI webserver to re-group and control default group volume levels based on household states.

Now I am able to use SAC to TTS seamlessly to my household speakers and not wreck the playing queue.

The best part is my "Name that Tune" rule which TTS the Album & Song Title after playing 30 secs into the currently playing song. Before SAC, I had to use my Amazon Echo's for TTS and the built-in driver was not always accurate with the song track data. My wife and I are improving our competitive trivia nature at who knows that song name/album/year and other band meta data.

I just sent you some Sonos Advanced Controller bucks :moneybag: to support your development to your @winksd Paypal account... Again thank you for your wonderful programming talents and improving the integration and value of my HE system...

10 Likes

So i just picked up a couple of the Sonos Connects (S1) and seen this app, I was able to install this in HPM, I added my two connects. And everything seems to work except the Speak, (TTS) it throws a "Could not determine service type for message:" any ideas? The integrated Sonos app TTS works.

Thanks

No idea... but I'm releasing a version with all of those commands entirely re-written to use webscokets instead of HTTP requests, so chances are whatever issue you're running into with them with be resolved with the new version. See my post a couple posts up for more info.

1 Like

Sounds good, I did see the new version coming, thanks I apricate it.

Do you think this new version will resolve the Connection refused warning and/or the No route to host (Host unreachable) warning I keep gertting with the built-in integration and the current Advanced Controller version?