Show Sonos playlists under favorites
Proposed change
Fixes a bug where sonos native playlists are not shown. They are now shown under favorites, just like in the sonos app.
Type of change
- [ ] Dependency upgrade
- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New integration (thank you!)
- [x] New feature (which adds functionality to an existing integration)
- [ ] Deprecation (breaking change to happen in the future)
- [ ] Breaking change (fix/feature causing existing functionality to break)
- [ ] Code quality improvements to existing code or addition of tests
Additional information
- This PR fixes or closes issue: fixes #
- This PR is related to issue:
- Link to documentation pull request:
- Link to developer documentation pull request:
- Link to frontend pull request:
Checklist
- [x] The code change is tested and works locally.
- [x] Local tests pass. Your PR cannot be merged unless tests pass
- [x] There is no commented out code in this PR.
- [x] I have followed the development checklist
- [x] I have followed the perfect PR recommendations
- [x] The code has been formatted using Ruff (
ruff format homeassistant tests) - [ ] Tests have been added to verify that the new code works.
If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated for www.home-assistant.io
If the code communicates with devices, web services, or third-party tools:
- [ ] The manifest file has all fields filled out correctly.
Updated and included derived files by running:python3 -m script.hassfest. - [ ] New or updated dependencies have been added to
requirements_all.txt.
Updated by runningpython3 -m script.gen_requirements_all. - [ ] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.
To help with the load of incoming pull requests:
- [ ] I have reviewed two other open pull requests in this repository.
Hey there @jjlawren, @peterager, mind taking a look at this pull request as it has been labeled with an integration (sonos) you are listed as a code owner for? Thanks!
Code owner commands
Code owners of sonos can trigger bot actions by commenting:
@home-assistant closeCloses the pull request.@home-assistant rename Awesome new titleRenames the pull request.@home-assistant reopenReopen the pull request.@home-assistant unassign sonosRemoves the current integration label and assignees on the pull request, add the integration domain after the command.@home-assistant add-label needs-more-informationAdd a label (needs-more-information, problem in dependency, problem in custom component) to the pull request.@home-assistant remove-label needs-more-informationRemove a label (needs-more-information, problem in dependency, problem in custom component) on the pull request.
Please fix the CI issues and add tests for the new functionality.
Please fix the CI issues and add tests for the new functionality.
Thanks for the notification, the formatting issues should be resolved now.
Would someone be able to give me some guidance on the tests to be added? Unfortunately my HA runs in docker, so I'm copying code in and out of a terminal into GitHub all the time, not the most convenient setup for proper development...
Please fix the CI issues and add tests for the new functionality.
Thanks for the notification, the formatting issues should be resolved now.
Would someone be able to give me some guidance on the tests to be added? Unfortunately my HA runs in docker, so I'm copying code in and out of a terminal into GitHub all the time, not the most convenient setup for proper development...
You'll want to setup a developer environment. That is documented here:
https://developers.home-assistant.io/docs/development_environment/
The mock implementation of get_music_library_information will need to be updated to return the data structure when "sonos_playlists" is passed in. To get the data structure, what I do is run Home Assistant in the debugger, set a break point; and capture the data structure that my Sonos Device sends back. This way we have a sample data set for sonos_playlists based on what the device sends.
https://github.com/home-assistant/core/blob/8aee79085ab159ffe90c866f1d7e402f31d00c3c/tests/components/sonos/conftest.py#L507-L520
The next step is then to add tests to test_media_browser.py and test_media_player.py - to verify it can be browsed and played properly.
I just managed to set up a test environment and capture the data. It seems like the dataset was already available under sonos_playlists.json, https://github.com/home-assistant/core/pull/142357/commits/d327ec904c9e6ac65b39713439c1e4ad13f62cd0
Quick question, simply having the project open in my vscode keeps my CPU at 100%, and with all the components the explorer is so huge I'd almost run out of RAM just looking at it. Is there something I still need to do to actually be able to work on this project, like somehow hiding/disabling most of the components or so?
items:
FV:2/14: Christmas
FV:2/1: I'm feeling lucky mix
SQ:8: ABBA
...
SQ:9: Treshold
SQ:0: Within & Evanescence
unit_of_measurement: items
friendly_name: Sonos favorites
This is what the favorites sensor now looks like. I don't have any music albums or audio books favorited, so I can't tell what the sensor looks like with those in there. I'd assume they'd also turn up there. If so, I would assume Sonos playlists also should. Or are those references too?
I'm currently attempting to commit two small changes to the tests that will cause this code to also be tested (and mocked) properly. That commit should be incoming soon. I might see what the code would look like if I'd make it a separate variable and only merge it in in browse, but then I think it would also have to be merged in in the favorites sensor. I guess it really depends on how audio books and music albums work. Does anybody know about that, or if not, know how I could find out (without requiring more external services on my Sonos?)
I've looked into splitting it into a separate variable and only merging it in the browser, but doing so caused me to run into the following.
What I've got now is this:
for favorite in favorites:
item_class = (
favorite.reference.item_class
if hasattr(favorite, "reference")
else favorite.item_class
)
if item_class != media_content_id:
continue
children.append(
BrowseMedia(
title=favorite.title,
media_class=SONOS_TO_MEDIA_CLASSES[item_class],
media_content_id=favorite.item_id,
media_content_type="favorite_item_id",
can_play=True,
can_expand=False,
thumbnail=get_thumbnail_url_full(
media=media,
is_internal=True,
media_content_type="favorite_item_id",
media_content_id=favorite.item_id,
get_browse_image_url=get_browse_image_url,
item=favorite,
),
)
)
return BrowseMedia(
title=content_type.title(),
media_class=MediaClass.DIRECTORY,
media_content_id="",
media_content_type="favorites",
can_play=False,
can_expand=True,
children=children,
)
What I would get then is:
for favorite in favorites:
item_class = favorite.reference.item_class
if item_class != media_content_id:
continue
children.append(
BrowseMedia(
title=favorite.title,
media_class=SONOS_TO_MEDIA_CLASSES[item_class],
media_content_id=favorite.item_id,
media_content_type="favorite_item_id",
can_play=True,
can_expand=False,
thumbnail=get_thumbnail_url_full(
media=media,
is_internal=True,
media_content_type="favorite_item_id",
media_content_id=favorite.item_id,
get_browse_image_url=get_browse_image_url,
item=favorite,
),
)
)
for favorite in favorites:
item_class = favorite.item_class
if item_class != media_content_id:
continue
children.append(
BrowseMedia(
title=favorite.title,
media_class=SONOS_TO_MEDIA_CLASSES[item_class],
media_content_id=favorite.item_id,
media_content_type="favorite_item_id",
can_play=True,
can_expand=False,
thumbnail=get_thumbnail_url_full(
media=media,
is_internal=True,
media_content_type="favorite_item_id",
media_content_id=favorite.item_id,
get_browse_image_url=get_browse_image_url,
item=favorite,
),
)
)
return BrowseMedia(
title=content_type.title(),
media_class=MediaClass.DIRECTORY,
media_content_id="",
media_content_type="favorites",
can_play=False,
can_expand=True,
children=children,
)
That's a lot of code duplication... Of course I could do clever tricks with zip or so, but I don't think that would be worth it.
What I could do, is what I committed last. This wraps the playlist in a "favorite" Didl wrapper, which makees it so no other code needs to be touched.
self._favorites = []
for fav in new_favorites:
try:
# exclude non-playable favorites with no linked resources
if fav.reference.resources:
self._favorites.append(fav)
except SoCoException as ex:
# Skip unknown types
_LOGGER.error("Unhandled favorite '%s': %s", fav.title, ex)
for playlist in new_playlists:
playlist_reference = DidlFavorite(
title=playlist.title,
parent_id=playlist.parent_id,
item_id=playlist.item_id,
resources=playlist.resources,
desc=playlist.desc,
)
playlist_reference.reference = playlist
try:
self._favorites.append(playlist_reference)
except SoCoException as ex:
_LOGGER.error(
"Unhandled favorite: '%s': %s", playlist_reference.title, ex
)
Currently committing...
Good progress. Looking cleaner. I'll get a review and see if we can get the CI running.
Thank you @danielvandenberg95 for the contribution! This will be in 2025.6.0