-
-
Notifications
You must be signed in to change notification settings - Fork 367
Add Wiim provider based on pywiim #3067
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from 16 commits
cde7474
6ccc16b
786c9a2
098e0be
3d6ceeb
78ea62c
2463617
b7987b9
49b4ac8
1dae323
1bc3342
b9728b8
8f8b922
50b2cd3
a3d885b
4c46cab
e36f63b
a9d65ef
32a4d2d
697ed6a
a48a581
5c5c52e
2f97f0c
a1e579e
d97dded
7ad4036
bd79980
86fef77
0e559eb
4d9eba3
6f8a014
87cd16b
7dbcb2b
d985d4e
0ce3529
0900ed8
6c90989
eee7b7c
889c1d0
a7e8ade
2eb40b6
66d1a56
ebc8392
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| """ | ||
| Provider for WiiM speakers. | ||
|
|
||
| This package provides a Music Assistant provider implementation for WiiM speakers. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| from music_assistant_models.enums import ProviderFeature | ||
|
|
||
| from music_assistant.constants import CONF_ENTRY_MANUAL_DISCOVERY_IPS | ||
|
|
||
| from .provider import WiimProvider | ||
|
|
||
| if TYPE_CHECKING: | ||
| from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig | ||
| from music_assistant_models.provider import ProviderManifest | ||
|
|
||
| from music_assistant.mass import MusicAssistant | ||
| from music_assistant.models import ProviderInstanceType | ||
|
|
||
| SUPPORTED_FEATURES = { | ||
| ProviderFeature.SYNC_PLAYERS, | ||
| } | ||
|
|
||
|
|
||
| async def setup( | ||
| mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig | ||
| ) -> ProviderInstanceType: | ||
| """Initialize provider(instance) with given configuration.""" | ||
| return WiimProvider(mass, manifest, config, SUPPORTED_FEATURES) | ||
|
|
||
|
|
||
| async def get_config_entries( | ||
| mass: MusicAssistant, # noqa: ARG001 | ||
| instance_id: str | None = None, # noqa: ARG001 | ||
| action: str | None = None, # noqa: ARG001 | ||
| values: dict[str, ConfigValueType] | None = None, # noqa: ARG001 | ||
| ) -> tuple[ConfigEntry, ...]: | ||
| """ | ||
| Return Config entries to setup this provider. | ||
|
|
||
| instance_id: id of an existing provider instance (None if new instance setup). | ||
| action: [optional] action key called from config entries UI. | ||
| values: the (intermediate) raw values for config entries sent with the action. | ||
| """ | ||
| return (CONF_ENTRY_MANUAL_DISCOVERY_IPS,) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| """Constants for the WiiM Provider.""" | ||
|
|
||
| from music_assistant_models.player import PlayerSource | ||
|
|
||
| SOURCE_LINE_IN = "line_in" | ||
| SOURCE_AIRPLAY = "airplay" | ||
| SOURCE_SPOTIFY = "spotify" | ||
| SOURCE_UNKNOWN = "unknown" | ||
| SOURCE_TV = "tv" | ||
| SOURCE_RADIO = "radio" | ||
|
|
||
| PLAYER_SOURCE_MAP = { | ||
| SOURCE_LINE_IN: PlayerSource( | ||
| id=SOURCE_LINE_IN, | ||
| name="Line-in", | ||
| passive=False, | ||
| can_play_pause=False, | ||
| can_next_previous=False, | ||
| can_seek=False, | ||
| ), | ||
| SOURCE_TV: PlayerSource( | ||
| id=SOURCE_TV, | ||
| name="TV", | ||
| passive=False, | ||
| can_play_pause=False, | ||
| can_next_previous=False, | ||
| can_seek=False, | ||
| ), | ||
| SOURCE_AIRPLAY: PlayerSource( | ||
| id=SOURCE_AIRPLAY, | ||
| name="AirPlay", | ||
| passive=True, | ||
| can_play_pause=True, | ||
| can_next_previous=True, | ||
| can_seek=True, | ||
| ), | ||
| SOURCE_SPOTIFY: PlayerSource( | ||
| id=SOURCE_SPOTIFY, | ||
| name="Spotify", | ||
| passive=True, | ||
| can_play_pause=True, | ||
| can_next_previous=True, | ||
| can_seek=True, | ||
| ), | ||
| SOURCE_RADIO: PlayerSource( | ||
| id=SOURCE_RADIO, | ||
| name="Radio", | ||
| passive=True, | ||
| can_play_pause=True, | ||
| can_next_previous=True, | ||
| can_seek=True, | ||
| ), | ||
| SOURCE_UNKNOWN: PlayerSource( | ||
| id=SOURCE_UNKNOWN, | ||
| name="Unknown", | ||
| passive=True, | ||
| can_play_pause=True, | ||
| can_next_previous=True, | ||
| can_seek=True, | ||
| ), | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "type": "player", | ||
| "domain": "wiim", | ||
| "name": "WiiM", | ||
| "stage": "beta", | ||
| "description": "Stream music to WiiM devices.", | ||
| "codeowners": ["@davidanthoff"], | ||
| "requirements": ["pywiim==2.1.83", "async-upnp-client"], | ||
davidanthoff marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| "documentation": "https://music-assistant.io/player-support/wiim/" | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,217 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Wiim Player implementation.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import time | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import TYPE_CHECKING, cast | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import pywiim | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from music_assistant_models.enums import PlaybackState, PlayerFeature, PlayerType | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from music_assistant_models.player import DeviceInfo, PlayerSource | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pywiim.upnp.eventer import UpnpEventer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from music_assistant.models.player import Player, PlayerMedia | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if TYPE_CHECKING: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pywiim.upnp.client import UpnpClient | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from .provider import WiimProvider | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class WiimPlayer(Player): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Wiim Player in Music Assistant.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def __init__( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| provider: WiimProvider, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| player_id: str, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: str, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| client: pywiim.WiiMClient, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| upnp_client: UpnpClient, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Initialize the Player.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| super().__init__(provider, player_id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # init some static variables | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._attr_name = name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._attr_type = PlayerType.PLAYER | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._attr_supported_features = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PlayerFeature.VOLUME_SET, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PlayerFeature.VOLUME_MUTE, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PlayerFeature.PAUSE, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PlayerFeature.SET_MEMBERS, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PlayerFeature.NEXT_PREVIOUS, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PlayerFeature.SEEK, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PlayerFeature.SELECT_SOURCE, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PlayerFeature.PLAY_ANNOUNCEMENT, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._attr_can_group_with = {provider.instance_id} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.wiim_client = client | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.wiim_upnp_client = upnp_client | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.wiim_player = pywiim.Player( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| client, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| upnp_client=upnp_client, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| on_state_changed=self.update_ma_state, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.current_uri: str | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def setup(self) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Handle logic when the player is set up in the Player controller.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Create UpnpEventer with same UPnP client (for real-time events) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.wiim_eventer = UpnpEventer( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.wiim_upnp_client, # Share same UPnP client | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.wiim_player, # Player implements apply_diff() for state updates | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.player_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state_updated_callback=self.update_ma_state, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Start UPnP event subscriptions | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await self.wiim_eventer.start() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
davidanthoff marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await self.wiim_player.refresh() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._attr_device_info = DeviceInfo( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
davidanthoff marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model=self.wiim_player.model if self.wiim_player.model else "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| software_version=self.wiim_player.firmware if self.wiim_player.firmware else "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for source in self.wiim_player.source_catalog: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._attr_source_list.append( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PlayerSource( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id=source.get("id", ""), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name=source.get("name", ""), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| passive=not source.get("selectable", False), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| can_play_pause=source.get("supports_pause", False), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| can_seek=source.get("supports_seek", False), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| can_next_previous=source.get("supports_next_track", False) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| and source.get("supports_previous_track", False), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def needs_poll(self) -> bool: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Return if the player needs to be polled for state updates.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def poll_interval(self) -> int: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Return the interval in seconds to poll the player for state updates.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return 5 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
MarvinSchenkel marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def poll(self) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
davidanthoff marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Poll player for state updates.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await self.wiim_player.refresh() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def select_source(self, source: str) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Handle SELECT SOURCE command on the player.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await self.wiim_player.set_source(source) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def volume_set(self, volume_level: int) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Handle VOLUME_SET command on the player.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await self.wiim_player.set_volume(volume_level / 100.0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def volume_mute(self, muted: bool) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Handle VOLUME MUTE command on the player.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await self.wiim_player.set_mute(muted) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def next_track(self) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Next command.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await self.wiim_player.next_track() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def previous_track(self) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Previous command.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await self.wiim_player.previous_track() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def seek(self, position: int) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """SEEK command on the player.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await self.wiim_player.seek(position) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def play(self) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Play command.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await self.wiim_player.resume() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def stop(self) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Stop command.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await self.wiim_player.stop() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def pause(self) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Pause command.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await self.wiim_player.pause() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def play_media(self, media: PlayerMedia) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Play media command.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await self.wiim_player.play_url(media.uri) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.current_uri = media.uri | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def play_announcement( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self, announcement: PlayerMedia, volume_level: int | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Handle (native) playback of an announcement on the player.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
davidanthoff marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await self.wiim_player.play_notification(announcement.uri) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+219
to
+229
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def on_unload(self) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Handle logic when the player is unloaded from the Player controller.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await self.wiim_upnp_client.close() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
davidanthoff marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await self.wiim_client.close() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
davidanthoff marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+231
to
+237
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def set_members( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| player_ids_to_add: list[str] | None = None, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| player_ids_to_remove: list[str] | None = None, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Handle SET_MEMBERS command on the player.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if player_ids_to_add: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for i in player_ids_to_add: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| child_player = cast("WiimPlayer", self.mass.players.get(i)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
davidanthoff marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await child_player.wiim_player.join_group(self.wiim_player) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if player_ids_to_remove: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for i in player_ids_to_remove: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| child_player = cast("WiimPlayer", self.mass.players.get(i)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
davidanthoff marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await child_player.wiim_player.leave_group() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def update_ma_state(self) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Update MA state from SDK's cache/HTTP poll attributes.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger = self.logger | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.debug("Device %s: Updating MA state from SDK cache/HTTP poll", self._attr_name) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
davidanthoff marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._attr_available = self.wiim_player.available | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._attr_volume_level = ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| int(self.wiim_player.volume_level * 100) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self.wiim_player.volume_level is not None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._attr_volume_muted = self.wiim_player.is_muted | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._attr_playback_state = PlaybackState(self.wiim_player.state) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._attr_elapsed_time = self.wiim_player.media_position | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._attr_elapsed_time_last_updated = time.time() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self.wiim_player.is_master: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._attr_group_members = ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [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() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
davidanthoff marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+288
to
+299
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 |
There was a problem hiding this comment.
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?
Outdated
Copilot
AI
Feb 19, 2026
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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?
Uh oh!
There was an error while loading. Please reload this page.