Skip to content

Add Wiim provider based on pywiim#3067

Draft
davidanthoff wants to merge 43 commits intomusic-assistant:devfrom
davidanthoff:da/pywiim
Draft

Add Wiim provider based on pywiim#3067
davidanthoff wants to merge 43 commits intomusic-assistant:devfrom
davidanthoff:da/pywiim

Conversation

@davidanthoff
Copy link
Copy Markdown
Contributor

@davidanthoff davidanthoff commented Jan 31, 2026

UPDATED: This uses the https://github.com/mjcumming/pywiim package to provide a player integration for Wiim and Linkplay devices. It is an alternative to #2947.

This PR here seems to work quite well now, and I am at a point where I'm very much leaning towards using the pywiim package for the integration. Would be great if folks could try this PR here and review it, it probably is ready for that!

The main thing I'm still working on for a first release is some tweaking of source selection. Done.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 12, 2026

🔒 Dependency Security Report

📦 Modified Dependencies

music_assistant/providers/wiim/manifest.json

Added:

The following dependencies were added or modified:

diff --git a/requirements_all.txt b/requirements_all.txt
index 4dee3319..0f44533c 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -63,6 +63,7 @@ python-fullykiosk==0.0.14
 python-slugify==8.0.4
 pytz==2025.2
 pywidevine==1.9.0
+pywiim==2.1.86
 radios==0.3.2
 rokuecp==0.19.5
 shortuuid==1.0.13

New/modified packages to review:

  • pywiim==2.1.86

🔍 Vulnerability Scan Results

No known vulnerabilities found
✅ No known vulnerabilities found


Automated Security Checks

  • Vulnerability Scan: Passed - No known vulnerabilities
  • Trusted Sources: All packages have verified source repositories
  • 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.

@davidanthoff davidanthoff mentioned this pull request Feb 13, 2026
@Hedda
Copy link
Copy Markdown

Hedda commented Feb 14, 2026

provide a player integration for Wiim and Linkplay devices

While it might be seen as bikesheeding the naming of these providers at a quick glanc I still stongky think that you should consider renaming this to “Linkplay Player Provider” (instead of ”WiiM Player Provider”) because it will not only be compatible with official WiiM-only devices but also with third-party Linkplay-compatible devices too. The arguments and rationele reasoning for that rename was put forward in the previous PR here -> #2947 (comment)

In summery;

Understand that all WiiM devices are Linkplay devices but not all Linkplay devices are WiiM branded devices as Linkplay license their technology for the streaming protocol used.

WiiM is just Linkplay’s first-party brand products that make use of the Linkplay streaming protocol, but Linkplay also license the same Linkplay streaming protocol to third-parties which makes their products alao compatible with this even though they are not WiiM branded products.

To compare, naming this after the WiiM product-line brand instead of the Linkplay protocol it uses would be link renaming the Google Cast provider to “Google Nest” because that is the first-party product brand supports it even though there are loads of third-party audio streamers/recievers that also support the same protocol.

So naming it “ Linkplay Player Provider” instead should make it more clear that this should be compatible will all Linkplay devices, including third-party brands using Linkplay technology, and not only limited to WiiM branded devices.

Linkplay Technology is actually also the company who own the WiiM brand. That is, WiiM is a first-party brand but there are also many third-party brands out there that are Linkplay compatible audio streamers.

WiiM is just a consumer brand of the Linkplay Technology, but yes they also license their "Linkplay" tech to other companies too.

If look at the wiimhome.com website you will see that it is says © 2025 Linkplay Technology Inc.

And if look at the "About us" company page on the linkplay.com website it mentions that WiiM is their own brand of electronics.

Linkplay Technology Inc. has full owership + control of both of Linkplay as a protocol as well as WiiM as a brand and their code.

As such there is nothing preventing that company from making a single integration compatible with Linkplay and WiiM.

You can kind of see WiiM as a hardware reference platform which show off what the Linkplay ecosystem can do.

That is, WiiM products might have more features but the integration can techically be made cross-compatible with both.

I understand it is a confusing since "Linkplay Technology" is both name of the company and their technology is named "Linkplay".

Update: Forn reference, here are few blog posts that put better words on the connection between Linkplay and its WiiM brand:

"The WiiM trademark belongs to Linkplay Technology, founded in 2014. It included experienced engineers and programmers from the world’s leading companies Google, Broadcom, Harman and InterVideo, who hardly need a separate introduction. Since its inception, Linkplay has specialized in the development and improvement of voice interactive control systems, home automation and IT technologies. Her clients included Yamaha, Marshall, Edifier, Audio Pro and other well-known companies. And then LinkPlay decided to produce “smart” electronics under its own brand."

"‘WiiM’ is the rather awkward brand name adopted by Linkplay for a (quickly expanding) range of home audio streaming products."

"WiiM is part of LinkPlay Technology Inc., a collaborative team from the likes of Google and Harmon, developing bespoke audio streaming software and hardware for OEM partners, with bases in the US, China and South Korea."

"Linkplay Technology has launched an upgraded version of its affordable audiophile music streaming box called the WiiM Pro Plus"

Again, i question if this shoukd really be named "WiiM Player Provider" and not "Linkplay Player Provider"?

Is or will this provider be hardcoded to only be specifically just for WiiM branded products or will it technically be a generic Linkplay implementation?

Might we also need a seperate "Linkplay Player Provider" as well is this is added as-is?

The main difference is that WiiM is only a brand while Linkplay is a (propriatory) protocol that other manufacturers can also license to use? If I understand correctly, Linkplay Technology has at least in the past licensed its Linkplay technology to others, even if it is today more focused on WiiM which is Linkplay Technology etelectronics brand (though initially it sounds like it was more meant as a reference design that others could copy to make their own Linkplay-based products).

So if this provider is not WiiM-specific but instead a general Linkplay provider implementation that coould technically also be used by other manufacturers that support the Linkplay protocol then suggest name it "Linkplay Player Provider" in Music Assistant .
For reference ,this is is by the way why integration for Home Assistant is today called "Linkplay integration" and not "WiiM integration":

However I see that you have also an open PR for a pure WiiM media player integration which is very relevant to this as well:

PS: Public info on how Linkplay technology is used in other brands and others manufacturer's products is not perfectly clear:

Quotes from https://www.linkplay.com/

"We provide a turnkey solution that includes software, Voice, Wi-Fi, Bluetooth Modules, and global streaming content integrated into one central mobile app for smart, voice-enabled, and IoT products. "

"Our smart solution currently powers over hundreds of brands and smart products in multiple regions around the world, in the United States, Europe, Asia, and South America."

"Brands We’ve Worked With"

image

Ping @Linkplay2020 and @WiimHome for further clearification between the Linkplay streaming protocol and the WiiM brand.

@davidanthoff davidanthoff marked this pull request as ready for review February 14, 2026 23:07
Copilot AI review requested due to automatic review settings February 14, 2026 23:07
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new WiiM/Linkplay player provider to Music Assistant, using the pywiim library as an alternative implementation path to the earlier WIP approach.

Changes:

  • Introduces a new wiim player provider (manifest, provider bootstrap, player implementation, icon).
  • Adds pywiim==2.1.83 to the project’s full requirements set and provider manifest requirements.
  • Implements basic discovery (SSDP via pywiim) plus manual IP discovery support, and core playback/grouping controls.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
requirements_all.txt Adds pywiim dependency pin.
music_assistant/providers/wiim/init.py Registers the provider and exposes manual IP config entry.
music_assistant/providers/wiim/provider.py Implements discovery + player instantiation/registration and logging setup.
music_assistant/providers/wiim/player.py Implements the WiiM player control surface and state syncing into MA.
music_assistant/providers/wiim/constants.py Adds WiiM source constants/mapping (currently unused).
music_assistant/providers/wiim/manifest.json Declares provider metadata and runtime requirements.
music_assistant/providers/wiim/icon.svg Adds provider icon asset.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI review requested due to automatic review settings February 19, 2026 04:02
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +228 to +232
async def play_media(self, media: PlayerMedia) -> None:
"""Play media command."""
url = await self.provider.mass.streams.resolve_stream_url(self.player_id, media)
await self.wiim_player.play_url(url)
self.current_uri = url
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The current_uri tracking mechanism could become stale if playback is controlled outside of Music Assistant (e.g., through the WiiM app or voice commands). In update_ma_state(), the comparison at line 317 may incorrectly identify external playback as Music Assistant playback if the URIs happen to match. This could cause incorrect active_source assignment and prevent proper media metadata updates.

Consider adding a timestamp or session identifier to more reliably track whether Music Assistant initiated the current playback session, or reset current_uri when detecting source changes.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't think this is true, as we are always using flow mode. Presumably no external playback will end up using the flow URI that MA generates.

Comment on lines +234 to +244
async def play_announcement(
self, announcement: PlayerMedia, volume_level: int | None = None
) -> None:
"""Handle (native) playback of an announcement on the player."""
if volume_level is not None:
self.logger.warning(
"Announcement volume level is not supported for player %s",
self.display_name,
)

await self.wiim_player.play_notification(announcement.uri)
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

Inconsistent feature support declaration. The player declares support for PLAY_ANNOUNCEMENT feature (line 48), but the implementation logs a warning and ignores the volume_level parameter (lines 239-242) rather than implementing it. If volume control for announcements is not supported by the underlying pywiim library, the feature should either be omitted from supported_features or the warning should clarify that the volume will default to current player volume.

Consider checking if pywiim provides any way to set announcement volume, or remove PLAY_ANNOUNCEMENT from supported_features if the functionality is too limited.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Argh, this whole change was directly implementing what the bot in a previous round asked me to do, now it changed its mind?

Comment on lines +304 to +315
if self.wiim_player.is_master:
self._attr_group_members = (
[
self.player_id,
*(i.uuid for i in self.wiim_player.group.slaves if i.uuid is not None),
]
if self.wiim_player.group is not None
else []
)
else:
self._attr_group_members.clear()

Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The group_members list is cleared when the player is not a master (line 314), but this logic appears incomplete. If a player transitions from master to slave (e.g., joins another group), the old group_members list is cleared but not properly updated with the new master's ID. The slave should either report an empty group_members list or the full group including the master.

Verify this matches the expected behavior in Music Assistant's player model. Consider checking how other providers like HEOS or Sonos handle slave players' group_members attribute.

Suggested change
if self.wiim_player.is_master:
self._attr_group_members = (
[
self.player_id,
*(i.uuid for i in self.wiim_player.group.slaves if i.uuid is not None),
]
if self.wiim_player.group is not None
else []
)
else:
self._attr_group_members.clear()
group = self.wiim_player.group
if group is None:
# Not part of a group: expose an empty group_members list.
self._attr_group_members = []
else:
# Part of a group (either master or slave): expose the full group,
# including the master and all slaves.
master = getattr(group, "master", None)
master_uuid = getattr(master, "uuid", None) if master is not None else None
# Fallback: if no master UUID is available but this player is the master,
# use this player's id as the master identifier.
if master_uuid is None and self.wiim_player.is_master:
master_uuid = self.player_id
group_member_ids: list[str] = []
if master_uuid is not None:
group_member_ids.append(master_uuid)
for slave in getattr(group, "slaves", []):
slave_uuid = getattr(slave, "uuid", None)
if slave_uuid is not None and slave_uuid not in group_member_ids:
group_member_ids.append(slave_uuid)
self._attr_group_members = group_member_ids

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ok, the semantics here are not clear to me. My understanding was that the master player should list all the slaves in its _attr_group_members, but that any slave player should have an empty _attr_group_members. That is what this code currently implements. Is that not correct?

Comment on lines +316 to +322
if not self.wiim_player.is_slave:
if self.current_uri and self.current_uri == self.wiim_player.media_content_id:
self._attr_active_source = self.player_id
else:
self._attr_active_source = (
self.wiim_player.source if self.wiim_player.source else ""
)
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The active_source logic may not properly handle all edge cases. When not a slave (line 316), the code checks if current_uri matches media_content_id to set active_source to player_id (line 318), otherwise it falls back to wiim_player.source (line 321). However, if current_uri is None and media_content_id is also None, the comparison will succeed, incorrectly setting active_source to player_id.

Add an explicit check that both current_uri and media_content_id are not None before comparing them, or ensure current_uri is always initialized to a non-None value (e.g., empty string).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That also just seems wrong? If self.current_uri is None, then the entire condition is False and we are done?

Copilot AI review requested due to automatic review settings February 19, 2026 06:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI review requested due to automatic review settings February 19, 2026 21:44
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI review requested due to automatic review settings February 23, 2026 08:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +229 to +235
async def on_unload(self) -> None:
"""Handle logic when the player is unloaded from the Player controller."""
await super().on_unload()
await self.wiim_eventer.async_unsubscribe()
await self.wiim_upnp_client.close()
await self.wiim_client.close()

Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

on_unload awaits unsubscribe/close calls sequentially without guarding cleanup in a try/finally. If async_unsubscribe() raises, the UPnP client / WiiM client may never be closed. Consider using try/finally (or asyncio.gather(..., return_exceptions=True)) so all resources get a best-effort close during unload.

Copilot uses AI. Check for mistakes.
Comment on lines +104 to +110
# Start UPnP event subscriptions
await player.wiim_eventer.start()

try:
await player.wiim_player.refresh()

player._attr_device_info = DeviceInfo(
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

This provider introduces non-trivial behavior (player setup with event subscriptions and state mapping in update_ma_state) but there are no unit tests. The repo has provider-focused pytest coverage (see tests/providers/*); adding tests with mocked pywiim objects would help prevent regressions (e.g., current_media/active_source handling across source changes and grouping).

Copilot uses AI. Check for mistakes.
is_removed will be set to True when the provider is removed from the configuration.
"""
for player in self.players:
# if you have any cleanup logic for the players, you can do that here.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Leftover from the demo provider?

"stage": "beta",
"description": "Stream music to WiiM and LinkPlay devices.",
"codeowners": ["@davidanthoff"],
"requirements": ["pywiim==2.1.84", "async-upnp-client==0.46.2"],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If you bump pywiim to the latest version, it should also fix those Sonos discovery warnings in the logs

self.current_uri: str | None = None

@staticmethod
async def setup(ip_address: str, provider: WiimProvider) -> None:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I feel this belongs in provider.py it self rather than as a static method on the player?


# Run the rest of each player setup in parallel, so we don't have to wait for each player
# to be setup before starting the next one.
setup_coroutines = [
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should check somewhere if a device has already been registered. The logic in the setup now seems to create duplicate subscriptions when a device gets rediscovered?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Seems the lint is failing on this file. Could you add a blank line?


def update_ma_state(self) -> None:
"""Update MA state from SDK's cache/HTTP poll attributes."""
self.logger.debug("Device %s: Updating MA state from SDK cache/HTTP poll", self._attr_name)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This could be verbose to avoid clogging the logs too much

@MarvinSchenkel MarvinSchenkel marked this pull request as draft February 23, 2026 12:01
@OzGav OzGav added this to the 2.9.0 milestone Mar 15, 2026
@kebbob
Copy link
Copy Markdown

kebbob commented Apr 14, 2026

Did this get abandoned? For an alternative, or just stale?
Just posting out of curiosity, I'm unable to add to the PR. So do please delete this comment if it's inappropriate.

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.

7 participants