[Nearing Release] Sonos Advanced Controller

EDIT 8Jan2024: Near complete re-write "finished". Removed ALL code related to anything "Cloud". New version runs 100% locally. Does NOT require any sort of 'Sonos Developer' account to function. It's 100% local, using a combination of UPnP commands and an unpublished local API. At some point, Sonos may lock down access to either or both of these, but what little information I could find on the matter seems to indicate they have zero intention of removing local control, so at worst there might be some fixes needed if/when that day occurs. Additionally, as I don't have any S1-only devices available for testing, I can't say for sure if there's any issues with this code regarding S1-only devices.

Now, on to the app itself! How is this different than the built-in app for Sonos? Well, a few ways:

First, it has NON-INTERRUPTING TTS. The built-in app has buttons for "Play Text And Restore", however this does NOT restore. It doesn't restore if you use the Sonos app to start whatever was playing originally from your favorite streaming service. It doesn't restore if what you were playing originally was an AirPlay stream. Maybe it restores if you're playing a playlist of locally stored MP3, I don't know, I haven't tried it, and I don't have a folder of MP3s to test with. Presumably most people use streaming music services, so I'd assume that for most people it doesn't restore.

'Sonos Advanced' does. Well, it doesn't actually even stop in the first place. It just lowers the volume of what is playing, does the TTS announcement, then restores the volume. The TTS can (optionally) be at a different volume than whatever is play, too. Unlike my previous version, this also works 100% locally. Any command with "And Resume" or "And Restore" uses the non-interrupting method.

"Play Track" and "Set Track" do not. They use "SetAVTransportURI" to start playing. I've tested this on local files, such as "http://192.168.1.4/local/doorbell.mp3", and to works as expected. Uris that start with "x-rincon" or "x-sonosapi" or "x-sonos" still need addressed. These require "CurrentURIMetaData" being sent during the UPnP command, which causes most of them to fail. I'm looking into ways to handle this, such as submitting a basic "metadata template" with them so the command will succeed. I'm also working on creating "Snapshot" child devices for the Player device, which would 'snapshot' the 'CurrentURI' and 'CurrentURIMetaData' as it is currently playing on the player, which should allow for restoring many/most streams. This is WIP with no estimated date of completion, assuming I can get it working at all.

All of the TTS related commands all use the non-interrupting method, even if they don't say "And Resume". So "Play Text" and "Play Text And Restore" both do the exact same thing. I don't see any real reason to have a TTS interrupt the stream since it's running locally anyway. If someone has a request to make "Play Text" interrupt, I can change this.

Secondly, my app allows for the creation of "Grouping Devices", which are just as they sound. You can select a "coordinator" and one or more "followers" and it'll make a virtual group device. This group device has a couple of buttons on it. One will group up all the players you selected, and this will create NEW group from scratch, ending anything that might have been playing on ANY of the players. Another button is "join players to coordinator" which does just that, joins them and they will play whatever the coordinator is playing, in a Sonos group. Then there's "remove players from coordinator" which will remove ALL players, but keep the coordinator playing, and an "ungroup players" which removes everything, coordinator included, from all groups. This, like the "group players" button, ends anything that's playing, on coordinator and all.

The "Sonos Advanced Group" devices now also have "switch" capability on them, so you can more easily call them in a Rule. And to make this even better, the switch reflect current state of the group. If the speakers are grouped exactly as set up in the Group Device, the switch will be "on". This means the selected coordinator needs to currently be the coordinator, all of the follower devices need to be in the group, and no other devices can be in the group. It doesn't matter how the group is created, whether through the Sonos Advanced app on HE, through any of the Sonos official apps on phones, computers, or any other method. If the group is set properly, the switch is "on", otherwise it's "off". This should hopefully open up some fun new possibilities for automation.

Each of the Virtual Sonos Advanced Player devices also have a button on them for "Ungroup Player", which will remove that player from whatever group it is in. If there's currently something playing, and the player you remove from the group is the "coordinator" then one of the other players in the group will end up as the coordinator.

Thirdly, the Sonos Advanced Controller allows for the enabling/disabling of "Crossfade" and setting the repeat mode to "All", "One", or "Off". Additionally, I've added optional child devices for each player device, giving on/off switch control for crossfade, shuffle, "repeat one", and "repeat all". I'll be adding a few more of these, too, for mute and a few other things. They're optional and off by default. Both the "current state" for these and control are available via the main "Sonos Advanced Player" device, with no need to create the child devices. They're 100% optional.

Lastly, but not least, the Sonos Advanced Controller has a button for "Load Favorite", which allows you to automate the loading of a Sonos Favorite. This can be anything that's a Sonos Favorite, whether that be a single song, a playlist, album, etc. And there's a "Get Favorites" button, which will fetch all your favorites, with album art, and display the name, album art, and favorite number, so you can easily figure out what favorite number you want. Now, I have a few dozen favorites, and this works well for me. If you have hundreds of Sonos Favorites, I don't know how it'll work... please let me know if anyone tries it with hundreds of favorites. I expect it should work fine, but I haven't tried it with a very large favorites list.

Sonos Advanced is on HPM for easy installation and updating. Enjoy and please feel free to provide feedback here or put in pull requests or issues on GitHub.

27 Likes

Reserving in case needed.

@daniel.winks This is exciting and I'm glad you have developed this much needed robust Sonos integration...

Created the Sonos developer account, keys and authenticated as you stated:

Success!

Your Sonos Account is now connected to Hubitat
Close this window to continue setup.

However when I try to create virtual player devices, I get this log error.

app:15022023-12-22 04:39:04.472 PMerrorjava.lang.NullPointerException: Cannot invoke method collectEntries() on null object on line 273 (method playerPage)

You can PM me if we need to debug...

I see what's probably happening. It's trying to collectEntries() on 'state.players', which on a totally new install might not exist by the time you get there, as everything is async. There's no sort of "await" available for Hubitat apps code, so it's always fun trying to avoid these sorts of race conditions.

I'll update the app so it calls refreshPlayersAndGroups() right after OAuth success, which should help avoid that race condition. What I really need is an "await refreshPlayersAndGroups()" on line 273.

If you click on the app status page, do you have a HashMap named "players" populated?

No players Hashmap in the App installedapp/status page.

I added your refreshPlayersAndGroups() on line 51 of the app to create the HashMap, and bingo, all is working!

With some limited testing, with commands play, mute looks good. Will extensively test tomorrow! I had much of this functionality using a RPi web server running SoCo python code. It will be great to replace that means of a Sonos snapshot workaround when I played mp3 sound clips.

Great Work!

Get Favorites:

Few little issues:

[dev:2240](http://10.0.0.80/logs#)2023-12-22 05:08:52.021 PM[warn](http://10.0.0.80/logs#)Sonos Group: HomeSonosGroup: No initialize() method defined or initialize() resulted in error: groovy.lang.MissingMethodException: No signature of method: user_driver_dwinks_Sonos_Cloud_Group_1984.initialize() is applicable for argument types: () values: [] Possible solutions: installed()
dev:22402023-12-22 05:36:17.266 PMwarnSonos Group: HomeSonosGroup: No configure() method defined or configure() resulted in error: groovy.lang.MissingMethodException: No signature of method: user_driver_dwinks_Sonos_Cloud_Group_1984.configure() is applicable for argument types: () values: []
dev:22402023-12-22 05:36:17.257 PMdebugSonos Group: HomeSonosGroup: Updated...
1 Like

Great! I'm pushing up a few changes here shortly.

The initialize() spits out that error if any methods it calls have an error, which they do on fresh/new installs, as they try calling .value on a null entity. I'm putting some try/catch around them so that should fix that.

1 Like

Great, let me know, happy to test...

Ungroup works but group command causes an error: Bad Request

[app:1502](http://10.0.0.80/logs#)2023-12-22 05:40:43.330 PM[error](http://10.0.0.80/logs#)Sonos Cloud Controller: post request error: Bad Request
[app:1502](http://10.0.0.80/logs#)2023-12-22 05:40:43.327 PM[error](http://10.0.0.80/logs#)Sonos Cloud Controller: post request returned HTTP status 400
[app:1502](http://10.0.0.80/logs#)2023-12-22 05:40:42.763 PM[debug](http://10.0.0.80/logs#)Sonos Cloud Controller: post https://api.ws.sonos.com/control/api/v1/hou...

Odd about the Bad Request on ungroup(). That's the only issue you've posted here I can't replicate easily. The others I was able to easily replicated by using my old C7 with a completely fresh install. Seems there was a few code issues that only popped up on a fresh install where some of the state variables weren't already populated from running on my main C8 as I coded this up.

Anyway, I changed the main page to call refreshPlayersAndGroups(), so with any luck by the time you click on the playerPage it'll have already returned. I also changed playerPage so it just uses a ConcurrentHashMap that can be updated freely by refreshPlayersAndGroups() when the callback for it runs. So now worst case is you get "0 players found" and can just wait a second or two and refresh the page for them to show up.

Fixed the last bugs there with Sonos Group devices not having configure()... I pulled out one of my other libraries from them to make this app a bit fewer parts, but didn't notice that I needed to add those to the drivers.

Let me know if you run into anything else. Happy to get this 100% for everyone here in the community. It's sorely needed for Sonos speakers on Hubitat IMO. I've got a few more ideas for future enhancements too, and happily welcome any suggestions too. But it's dinner time here in Ohio so I'm out for the night here. I'll check back in tomorrow to see if there's any other quick fixes to put in!

There's a new version up on GitHub with the latest fixes from all the above (other than the Ungroup() issue).

2 Likes

The ungroup() works as expected. It's the group() that has the issue and does not group the speakers back..

@daniel.winks you've done a great job on this app. It will definitely make many HE Sonos users happy, especially when they want to play a sound file (e.g. mp3, wav, etc) and restore the existing Sonos state/playlist.

The python based Sonos control library, aka ' soco.snapshot module' is what I have been using to perform the sound file play and restore function. However, I have found that if I have a cloud queue playing (which is 99% of the time with Spotify) my Sonos group will not restart after playing a sound file (arg!). The soco developers have acknowledged this as an issue).

It will be interesting if your HE native Sonos cloud controller api app can restore & play the existing cloud queue oriented playlist after a sound file is played.

Yeah, that part definitely works, I’ve been using it for about a year. Just until today I hadn’t polished up things enough to consider making it public. This app didn’t even stop your queue at all, so there’s nothing to restore. It just keeps on playing at a much reduced volume while the TTS or MP3 plays.

I’ve got morning announcements that play every morning without interrupting the whole house morning birdsong that plays during the wake up routine. Doorbell MP3 plays without interrupting. “Door ajar” alerts (kids leave doors open all the time). Etc. Never ever interrupts.

3 Likes

This is freakin AWESOME! Thank you. All of the integrations I've tried before have stopped my audio, (including the Hubitat-delivered Sonos integration), and Hubitat's AirPlay integration actually disconnects the speaker from my Airplay stream, forcing me to manually connect it. With this I'm able to stream from my iMac or iOS devices to multiple Airplay speakers (combo of Sonos and HomePods) and the announcement plays perfectly without interrupting, stopping, or changing the volume of my music. I can also use functions like "next track" and it skips to the next just like I would on the speaker or device. I haven't tried some of the other features, but this is really all I need for now. I'll report back any findings.

Currently moving all of my voice notifications to this integration.

1 Like

One thing I see is that I'm not getting any "Current States" on the devices. Is this expected, or did I miss something? I do see an "Event Callback URL" field in the client credentials of my Sonos Control App. Does something need to go here to send states back to my devices? I didn't see any instructs about that in the app setup... only the "Redirect URI" field.

EDIT: Also, unmute seems to take 2 command attempts to work.

I just tested your Sonos Cloud Controller play text and play uri: commands and they work perfectly! Kudo's! Now to migrate much of my legacy python code and Alexa TTS devices to sonos using your cloud api app! Not sure why I did not think of this before, I suppose I thought it would not play a MP3/WAV without destroying the Sonos queue.

I noticed that you are also planning of adding the HE music/audio/speak capabilities and associated events to the Sonos Cloud Player device which will be nice to see some of the audio and physical attribute values of the Sonos system.

I can recommend a few others for future improvements down the line (or if you want I can fork and push to you): Some require directly http access the Sonos device via local home network as I have shown below.

  1. 'refresh' to assure that the device's playing attribute values are current.
  2. 'battery' for charge "Level" state of those battery devices (eg. Move, Roam) that provide this attribute.
  3. 'PowerSource' ENUM ["battery", "dc", "mains", "unknown"] for devices in #2

capability 'AudioNotification'
capability 'MusicPlayer'
capability 'SpeechSynthesis'
capability 'Refresh'
capability 'Battery'
capability 'PowerSource'

Yes, it doesn't get any states. For that you'll want to run the built-in Sonos app side-by-side with it. I'm working on figuring all the ins and outs of UPnP, SSDP, and a few other things to implement all the stuff that the built-in one does, and have my app use local SOAP commands if possible, and only use the cloud stuff for the things that Sonos only allows via cloud (like the non-interrupting-TTS).

The "Event Callback URL" on Sonos is bugged. If you put the URL there for this app, which by Hubitat requirements is 'https://cloud.hubitat.com/api/{HubUUID}/apps/30/local/callback?access_token={this app access token}' then Sonos barfs and claims that it's not a "Secure URL". I'm guessing their webpage has a poorly written RegEx that doesn't properly handle a URL with a query parameter (ie, the '?access_token=...' bit). I've tried a few different ways to get them to accept the Hubitat URL, URL encoding it, etc, and it won't go. I submitted feedback to Sonos, but still haven't heard back. Without that, you can't call any of the "subscribe" methods on their WebAPI. That said, I'm working on using the local SOAP/XML stuff (like the built in Hubitat app uses) anyway. But that'll be a bit.

Really odd on the Mute/UnMute issue. I've not run into it not working or needing 2 commands to work. I'll see if I can replicate that and figure out why it's iffy for you.

1 Like

Yeah, that would be welcome. I'm working on an entire "local control" library to add to this.

My eventual goal is a complete Sonos app for Hubitat with zero need for any external anything and no need to run the built-in one side-by-side. The built-in stuff works "OK" enough, but it's VERY resource intensive, especially if you have many different active Sonos players. It's often over 50% of my total resource usage on Hubitat while things are playing. It does a lot of state updates that I honestly have zero use for. I really don't need it constantly updating "trackData" and "uri", so I plan on having that be an optional thing on my app/driver, to cut down on CPU usage.

2 Likes

Ok, I'll play around with it too to see if I can make any progress, buy my skill is limited.

I just did some additional testing, and this seems to be related to playing via an Airplay stream from iMac/iOS. If I'm playing music directly on the Sonos (via Sonos voice command), Mute and Unmute both work.

Both of these things are not a big issue for me, as I primarily use the integration for voice notifications, which work great.

That's already there. Basically everything loadAudioClip on the Sonos API. If you use "playText", "playTextAndRestore", or "speak", it all just textToSpeech(text, voice) to generate an MP3 on the hubitat and then sends that URL to Sonos. If you supply a URI to "playTrack", "playTrackAndRestore", etc, then it skips the generation of the TTS MP3 and uses your URI directly.

So it's just 2 things, "play MP3", or "make MP3 for TTS and play generated MP3". I added all the various capabilities for the cloudPlayer driver so it works in automations as you'd expect.

There's not really a "play text and DON'T restore" unless I implement all the stuff needed for createSession

1 Like

Ah, easily replicated. I'll put in a fix for that, since I play a lot from iOS too... just never noticed it as I don't often mute. I couldn't even get it to restore volume no matter how many times I called "unMute". I'll have to store the current volume before sending the mute command. It seems to be an issue on Sonos' side of things (or Apple).

I'm working on pulling in all the states I can get from the device locally, so I'll probably wait until I've got (local) "current volume" state before putting that in. Otherwise it would be a call to getVolume before calling setMute, and since Hubitat doesn't have any sort of "await" available for apps, I'd need to do the "getVolume" call synchronously to ensure it returned before calling "setMute", and I'm very loathe to ever use synchronous http calls.

If the device driver is storing a (local) "currentVolume" then it'll be trivial at that point to just toss in a "preMuteVolume" state and restore it whenever the device is unmuted.

Edit: Further testing... if I play from Apple Music on my iPhone/MBP onto a speaker, then open the Sonos App (The official one, From Sonos) on my iPhone or Mac, and click on Mute it does the same thing my Hubitat app does. When I click on "Unmute" it fails to restore the volume. I can work-around it eventually by storing "preMute" volume, but there's still the issue of "what to do if muted from the Sonos (official) App on a phone then unmuted on the Hubitat app" since I won't have stored a "preMute" volume. I'll do what I can about it, but it's an issue that's really between Sonos and Apple to fix.

I could probably just store a "currentVolume" and a second "preMute" volume, and always update the the "preMute" volume whenever the "currentVolume" changes UNLESS said change is in response to an "isMuted" change. Then I can have my app restore the volume whenever unmuted AND it could restore the "preMute" volume anytime the player is unmuted, regardless of where the unmute came from, "fixing" the issue even if you mute/unmute from the official Sonos app.

1 Like

Hey @daniel.winks,

First off, massive kudos on the [ALPHA] Sonos Cloud Controller—it's truly been a game-changer! :rocket: I've been eagerly anticipating this, and I'm sure I'm not alone in breathing a sigh of relief. I had one foot out the door, leaning towards Home Assistant due to the limited functionality of Sonos with Hubitat, which was affecting my use cases.

The Sonos Grouping functionality, recognition of stereo pair players, and the way it handles favorites have been nothing short of top-notch. :ok_hand: What caught my attention and has me super excited is the potential addition of Current States. Incorporating this level of detail would undoubtedly elevate the integration to new heights.

I completely understand that you've got a lot on your plate, both personally, professionally and especially with ongoing upgrades. If I may, I'd love to share a few future suggestions with you to include of Current States that are WebCore-friendly. These include:

  • mute
  • volume
  • groupMute
  • groupVolume
  • groupRole
  • grouped
  • playbackStatus
  • audioTrackData

One thing I've noticed is a bit of a hiccup when pulling players into WebCore. Specifically, I seem to be missing the ability to adjust the volume of each player individually. In SmartThings, it was a breeze with "setVolume," and in the Hubitat integration, it's labeled as "Set Volume." Checking out the features in the Sonos Cloud device, I see "setLevel," but it doesn't quite carry over to WebCore. Interestingly, "loadFavorite" does.

Hope this all makes sense! :musical_note: Excited about the possibilities and looking forward to what's in store.

Thanks again,
DBQ.

Yep, I'm working on all of those. So far I have working, current states for:

albumName
artistName
trackName
trackDuration
currentPlayMode ('NORMAL', 'REPEAT_ALL', 'REPEAT_ONE', 'SHUFFLE_NOREPEAT', 'SHUFFLE', or 'SHUFFLE_REPEAT_ONE')
transportState ('STOPPED', 'PLAYING', 'PAUSED_PLAYBACK', or 'TRANSITIONING')
bass
treble
loudness
volume
crossfadeMode
mute

The odd naming for things like "currentPlayMode", "transportState" are direct from the XML from the device. I haven't quite decided if I want to rename them or not. I'm leaning toward making it as "direct from the speaker" as it can be, so I'm not injecting my own 'opinion' on naming schemes on things.

Should have a nice update here by the end of the weekend, working on the "control" side of things now, which should bring it to a state of being a full replacement for the built-in app PLUS a few cloud-only things like the non-interruption TTS.

I had one foot out the door, leaning towards Home Assistant due to the limited functionality of Sonos with Hubitat, which was affecting my use cases.

Yeah. I started my home automation with one of the VERY old models of ST. Quickly hated it and moved straight to Home Assistant. I think I started on HA back at version 0.17. It was a long time ago. I wrote the GitLab-CI integration for HA among a few other things for it, so needless to say, I'm WELL aquainted with HA as a platform.

The "version 0.0.1 alpha" of this Sonos app for Hubitat was written over a year ago, specifically to address what you're mentioning here. The built-in app worked OK for what it does, but there's a lot of features lacking. When I was on HA, I had both their built-in integration and the HACS integration for "Sonos Cloud", and had automations that grouped speakers, ungrouped speakers, and did non-interrupting TTS.

For a year or two I actually ran an entire HA instance with literally nothing but Sonos integrations on it for those things... but that annoyed me so I knocked together a really basic app here that gave me "ungroup" and "non-interrupting tts", specifically so I could shut down the mini-PC I was running HA on. I've been running that super basic version of this app for a while, and finally figured I should really flesh it out some more and get it out to the community here.

By the end of the weekend (or maybe a little later if I get derailed), I should more control and functionality here in Hubitat with Sonos that what HA ever provided me. Or at the very least it'll be fully on-par, depending on whether or not they're missing anything.

5 Likes