Skip to content

Add Samsung WAM player provider#3334

Draft
Oliver-Stevens wants to merge 2 commits intomusic-assistant:devfrom
Oliver-Stevens:feature/samsung-wam-provider
Draft

Add Samsung WAM player provider#3334
Oliver-Stevens wants to merge 2 commits intomusic-assistant:devfrom
Oliver-Stevens:feature/samsung-wam-provider

Conversation

@Oliver-Stevens
Copy link

Adds native support for Samsung's Wireless Audio Multiroom (WAM) speakers via the pywam library.

Tested locally with the following speaker models:

  • Samsung M3 (WAM350/351)
  • Samsung M5 (WAM550/551)

What's included:

  • Auto-discovery via SSDP, with manual IP fallback in provider settings
  • Full playback control (play, pause, stop, source selection)
  • Volume and mute control, with hardware button state synchronised back to MA
  • Native multi-room grouping, handling both MA-initiated and externally-initiated group changes
  • Connection recovery with automatic reconnect and retry logic on commands

Notes:

  • Flow mode is enforced as the WAM API doesn't support native URL enqueueing
  • Stop is implemented as pause + mute as the firmware's native stop command doesn't work for URL streams
  • New dependencies: pywam

Adds native support for Samsung's Wireless Audio Multiroom (WAM) speakers
via the pywam library.

- Auto-discovery via SSDP with manual IP fallback
- Full playback control (play, pause, stop, source selection)
- Volume and mute control, with hardware button state synchronised to MA
- Native multi-room grouping with support for externally-initiated changes
- Command retry with exponential backoff and automatic connection recovery
- Tiered dependency logging to control verbosity
@github-actions
Copy link
Contributor

github-actions bot commented Mar 8, 2026

🔒 Dependency Security Report

📦 Modified Dependencies

music_assistant/providers/samsung_wam/manifest.json

Added:

The following dependencies were added or modified:

diff --git a/requirements_all.txt b/requirements_all.txt
index fec509c3..6d9dedab 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -63,6 +63,7 @@ pylast==7.0.2
 python-fullykiosk==0.0.14
 python-slugify==8.0.4
 pytz==2025.2
+pywam==0.1.0rc6
 pywidevine==1.9.0
 radios==0.3.2
 rokuecp==0.19.5

New/modified packages to review:

  • pywam==0.1.0rc6

🔍 Vulnerability Scan Results

No known vulnerabilities found
✅ No known vulnerabilities found


Automated Security Checks

  • Vulnerability Scan: Passed - No known vulnerabilities
  • Trusted Sources: Some packages missing source repository
  • Typosquatting Check: No suspicious package names detected
  • License Compatibility: All licenses are OSI-approved and compatible
  • Supply Chain Risk: Passed - packages appear mature and maintained

Manual Review

Maintainer approval required:

  • I have reviewed the changes above and approve these dependency updates

To approve: Comment /approve-dependencies or manually add the dependencies-reviewed label.

@Oliver-Stevens
Copy link
Author

Pinning pywam to 0.1.0rc6 as v0.2.0 requires Python >=3.13. The two versions are functionally identical with only changes to error message formatting and docstrings.

@Oliver-Stevens
Copy link
Author

Oliver-Stevens commented Mar 8, 2026

Doc draft as follows:


Samsung Wireless Audio (WAM)

Music Assistant has support for Samsung's Wireless Audio Multiroom (WAM) speakers, providing auto-discovery, playback control, and native multi-room grouping.

Features

  • Samsung WAM speakers are auto-discovered on the local network via SSDP. Manual IP addresses can also be added in the provider settings for devices that aren't found automatically.
  • Full playback control: play, pause, and stop
  • Volume control via the Music Assistant UI or the speaker's physical buttons, with volume and mute state kept in sync
  • Source selection: switch between all available inputs (Wi-Fi, Bluetooth, AUX, Optical, TV SoundConnect)
  • Native multi-room grouping: create, modify, and dissolve speaker groups directly within Music Assistant, with external group changes (e.g. via the official Samsung app) detected and reflected automatically
  • Gapless playback, crossfade, shuffle, and repeat
  • Speaker renaming: changing the player name in Music Assistant settings also updates the friendly hostname on the device itself

Settings

For information about the settings seen in the MA UI refer to the Player Provider Settings and Individual Player Settings pages.

Known Issues / Notes

  • Flow mode: This provider enforces flow mode, as the WAM API does not support native URL enqueuing.
  • Stop vs. pause: The WAM firmware has no discrete stop command for URL playback, so stop is implemented as pause + mute.
  • External source changes: If the input is changed using a physical button on the speaker (e.g. pressing the Bluetooth button) while Music Assistant is streaming, the provider will automatically terminate the stream cleanly before switching to the new source. This prevents audio from continuing to play in the background.

@marcelveldt-traveling
Copy link

Pinning pywam to 0.1.0rc6 as v0.2.0 requires Python >=3.13. The two versions are functionally identical with only changes to error message formatting and docstrings.

I think it's safe to bump music-assistant to 3.13 or even 3.14. @MarvinSchenkel FYI

from .models import DiscoveryEventType, DiscoveryInfo, DiscoverySource


class DiscoverySsdpListener:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now this is fine to add but since this is the second or 3rd SSDP implementation we now added to individual providers, we should create a new core controller for discovery and move SSDP and MDNS discovery into that. So basically, just like Home Assistant, we have one discovery core controller (or builtin provider) that can handle the base logic for all providers that want it.

Anyways, outside the scope of this PR - this code looks fine!

@MarvinSchenkel can you put a task on the backlog for this ?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No more need for a task anymore, I had a free hour while traveling;
#3378

elif prov.logger.isEnabledFor(logging.DEBUG):
logging.getLogger("pywam").setLevel(logging.DEBUG)
logging.getLogger("pywam.client").setLevel(logging.INFO)
logging.getLogger("async_upnp_client").setLevel(logging.DEBUG)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider raising this. I think the async_upnp_client is really chatty.


# --- Configuration Entries ---

CONF_ENTRY_SAMPLE_RATES_WAM = ConfigEntry.from_dict(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a helper function here that can help you with this.

"""
await self.discovery.stop()
self.groups.stop_sync_task()
self.wam_players.clear()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to close the connections to the speakers here as well before clearing?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In combination with the feedback above, we could simply do

for player in self.players:
    self.logger.debug("Unloading player %s", player.name)
    await self.mass.players.unregister(player.player_id)

"""Samsung WAM player provider."""

supported_models: dict[str, dict[str, Any]]
wam_players: dict[str, WamPlayer]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to keep track of this yourself, you already have access to self.players from the base class

self.logger.debug("Pre-flight connection to new player at %s", info.ip_address)

temp_speaker = Speaker(info.ip_address)
await temp_speaker.connect()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should guard for ConnectionErrors here to avoid crashing the discovery process.

self.states.clear()
self._child_to_leader.clear()
for player in self.players.values():
synced_to = getattr(player, "synced_to_internal", None)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need all those getattrs here? Looks like synced_to_internal is a property anyway.

"pywam==0.1.0rc6",
"async-upnp-client==0.46.2"
],
"documentation": "URL_TO_DOCUMENTATION",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"documentation": "URL_TO_DOCUMENTATION",
"documentation": "https://music-assistant.io/player-support/samsung_wam/",

Copy link
Contributor

@MarvinSchenkel MarvinSchenkel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few comments, but we are really close already 🙌 . Marking this PR as draft again so we can keep track of what needs our attention. Please mark this as 'Ready for review' again when you have addressed the feedback.

@MarvinSchenkel MarvinSchenkel marked this pull request as draft March 10, 2026 15:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants