feat: Built-in Web Player Part 2 (#2043)
authorMaxim Raznatovski <nda.mr43@gmail.com>
Mon, 17 Mar 2025 17:05:15 +0000 (18:05 +0100)
committerGitHub <noreply@github.com>
Mon, 17 Mar 2025 17:05:15 +0000 (18:05 +0100)
* refactor: make poll interval a constant

* feat: add native power controlls for the builtin player

* fix: incorrect state on power off

* feat: hide the web player by default

---------

Co-authored-by: Marcel van der Veldt <m.vanderveldt@outlook.com>
music_assistant/providers/builtin_player/__init__.py

index 55ee3ee6107f6ce80c294c6bef396418a2c8a6b3..aa348e49ab71ffef7c8518d52fb23a077836e0da 100644 (file)
@@ -24,7 +24,7 @@ from typing import TYPE_CHECKING, cast
 import shortuuid
 from aiohttp import web
 from music_assistant_models.builtin_player import BuiltinPlayerEvent, BuiltinPlayerState
-from music_assistant_models.constants import PLAYER_CONTROL_NONE
+from music_assistant_models.constants import PLAYER_CONTROL_NATIVE
 from music_assistant_models.enums import (
     BuiltinPlayerEventType,
     ContentType,
@@ -59,6 +59,7 @@ if TYPE_CHECKING:
 
 # If the player does not send an update within this time, it will be considered offline
 DURATION_UNTIL_TIMEOUT = 70
+POLL_INTERVAL = 10
 
 
 async def setup(
@@ -198,6 +199,24 @@ class BuiltinPlayerProvider(PlayerProvider):
             BuiltinPlayerEvent(type=BuiltinPlayerEventType.PLAY_MEDIA, media_url=url),
         )
 
+    async def cmd_power(self, player_id: str, powered: bool) -> None:
+        """Send POWER command to given player.
+
+        - player_id: player_id of the player to handle the command.
+        - powered: bool if player should be powered on or off.
+        """
+        self.mass.signal_event(
+            EventType.BUILTIN_PLAYER,
+            player_id,
+            BuiltinPlayerEvent(
+                type=BuiltinPlayerEventType.POWER_ON
+                if powered
+                else BuiltinPlayerEventType.POWER_OFF
+            ),
+        )
+        if (not powered) and (player := self.mass.players.get(player_id)):
+            player.powered = False
+
     async def poll_player(self, player_id: str) -> None:
         """Poll player for state updates.
 
@@ -216,6 +235,11 @@ class BuiltinPlayerProvider(PlayerProvider):
 
     async def remove_player(self, player_id: str) -> None:
         """Remove a player."""
+        self.mass.signal_event(
+            EventType.BUILTIN_PLAYER,
+            player_id,
+            BuiltinPlayerEvent(type=BuiltinPlayerEventType.TIMEOUT),
+        )
         await self.unregister_player(player_id)
 
     async def register_player(self, player_name: str, player_id: str | None) -> Player:
@@ -240,6 +264,7 @@ class BuiltinPlayerProvider(PlayerProvider):
             PlayerFeature.VOLUME_SET,
             PlayerFeature.VOLUME_MUTE,
             PlayerFeature.PAUSE,
+            PlayerFeature.POWER,
         }
 
         self.instances[player_id] = PlayerInstance(
@@ -257,11 +282,14 @@ class BuiltinPlayerProvider(PlayerProvider):
             type=PlayerType.PLAYER,
             name=player_name,
             available=True,
-            power_control=PLAYER_CONTROL_NONE,
+            power_control=PLAYER_CONTROL_NATIVE,
+            powered=False,
             device_info=DeviceInfo(),
             supported_features=player_features,
             needs_poll=True,
-            poll_interval=10,
+            poll_interval=POLL_INTERVAL,
+            hidden_by_default=True,
+            expose_to_ha_by_default=False,
         )
 
         await self.mass.players.register_or_update(player)
@@ -277,6 +305,7 @@ class BuiltinPlayerProvider(PlayerProvider):
         if player := self.mass.players.get(player_id):
             player.available = False
             player.state = PlayerState.IDLE
+            player.powered = False
 
     async def update_player_state(self, player_id: str, state: BuiltinPlayerState) -> None:
         """Update current state of a player.
@@ -290,15 +319,26 @@ class BuiltinPlayerProvider(PlayerProvider):
             raise RuntimeError("No instance found")
         instance.last_update = time()
 
+        if not player.powered and state.powered:
+            # The player was powered off, so this state message is already out of date
+            # Skip, it.
+            return
+
         player.elapsed_time_last_updated = time()
         player.elapsed_time = float(state.position)
         player.volume_muted = state.muted
         player.volume_level = state.volume
-        if state.playing:
+        if not state.powered:
+            player.powered = False
+            player.state = PlayerState.IDLE
+        elif state.playing:
+            player.powered = True
             player.state = PlayerState.PLAYING
         elif state.paused:
+            player.powered = True
             player.state = PlayerState.PAUSED
         else:
+            player.powered = True
             player.state = PlayerState.IDLE
 
         self.mass.players.update(player_id)