music-assistant-server

6.5 KBPY
provider.py
6.5 KB177 lines • python
1"""Media Assistant Provider implementation."""
2
3from __future__ import annotations
4
5import asyncio
6import logging
7from typing import TYPE_CHECKING, cast
8
9from async_upnp_client.search import async_search
10from music_assistant_models.player import DeviceInfo
11from rokuecp import Roku
12
13from music_assistant.constants import CONF_ENTRY_MANUAL_DISCOVERY_IPS, VERBOSE_LOG_LEVEL
14from music_assistant.helpers.util import TaskManager
15from music_assistant.models.player_provider import PlayerProvider
16
17from .constants import CONF_AUTO_DISCOVER
18from .player import MediaAssistantPlayer
19
20if TYPE_CHECKING:
21    from async_upnp_client.utils import CaseInsensitiveDict
22    from music_assistant_models.enums import ProviderFeature
23
24SUPPORTED_FEATURES: set[ProviderFeature] = set()
25
26
27class MediaAssistantprovider(PlayerProvider):
28    """Media Assistant Player provider."""
29
30    roku_players: dict[str, MediaAssistantPlayer] = {}
31    _discovery_running: bool = False
32    lock: asyncio.Lock
33
34    @property
35    def supported_features(self) -> set[ProviderFeature]:
36        """Return the features supported by this Provider."""
37        return SUPPORTED_FEATURES
38
39    async def handle_async_init(self) -> None:
40        """Handle async initialization of the provider."""
41        self.lock = asyncio.Lock()
42        # silence the async_upnp_client logger
43        if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL):
44            logging.getLogger("async_upnp_client").setLevel(logging.DEBUG)
45        else:
46            logging.getLogger("async_upnp_client").setLevel(self.logger.level + 10)
47        # silence the rokuecp logger
48        if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL):
49            logging.getLogger("rokuecp").setLevel(logging.DEBUG)
50        else:
51            logging.getLogger("rokuecp").setLevel(self.logger.level + 10)
52
53    async def loaded_in_mass(self) -> None:
54        """Call after the provider has been loaded."""
55        manual_ip_config = cast(
56            "list[str]", self.config.get_value(CONF_ENTRY_MANUAL_DISCOVERY_IPS.key)
57        )
58
59        for ip in manual_ip_config:
60            await self._device_discovered(ip)
61
62        self.logger.info("MediaAssistantProvider loaded")
63        await self.discover_players()
64
65    async def unload(self, is_removed: bool = False) -> None:
66        """Handle unload/close of the provider."""
67        if self.roku_players is None:
68            return  # type: ignore[unreachable]
69        async with TaskManager(self.mass) as tg:
70            for roku_player in self.roku_players.values():
71                tg.create_task(self._device_disconnect(roku_player))
72
73    async def discover_players(self) -> None:
74        """Discover Roku players on the network."""
75        if not self.config.get_value(CONF_AUTO_DISCOVER):
76            return
77        if self._discovery_running:
78            return
79        try:
80            self._discovery_running = True
81            self.logger.debug("Roku discovery started...")
82            discovered_devices: set[str] = set()
83
84            async def on_response(discovery_info: CaseInsensitiveDict) -> None:
85                """Process discovered device from ssdp search."""
86                ssdp_st: str | None = discovery_info.get("st")
87                if not ssdp_st:
88                    return
89
90                if "roku:ecp" not in ssdp_st:
91                    # we're only interested in Roku devices
92                    return
93
94                ssdp_usn: str = discovery_info["usn"]
95                ssdp_udn: str | None = discovery_info.get("_udn")
96                if not ssdp_udn and ssdp_usn.startswith("uuid:"):
97                    ssdp_udn = "ROKU_" + ssdp_usn.split(":")[-1]
98                elif ssdp_udn:
99                    ssdp_udn = "ROKU_" + ssdp_udn.split(":")[-1]
100                else:
101                    return
102
103                if ssdp_udn in discovered_devices:
104                    # already processed this device
105                    return
106
107                discovered_devices.add(ssdp_udn)
108
109                await self._device_discovered(discovery_info["_host"])
110
111            await async_search(on_response, search_target="roku:ecp")
112
113        finally:
114            self._discovery_running = False
115
116        def reschedule() -> None:
117            self.mass.create_task(self.discover_players())
118
119        # reschedule self once finished
120        self.mass.loop.call_later(300, reschedule)
121
122    async def _device_disconnect(self, roku_player: MediaAssistantPlayer) -> None:
123        """Destroy connections to the device."""
124        async with roku_player.lock:
125            if not roku_player.roku:
126                self.logger.debug("Disconnecting from device that's not connected")
127                return
128
129            self.logger.debug("Disconnecting from %s", roku_player.name)
130
131            old_device = roku_player.roku
132            self.roku_players.pop(roku_player.player_id)
133            await old_device.close_session()
134
135    async def _device_discovered(self, ip: str) -> None:
136        """Handle discovered Roku."""
137        async with self.lock:
138            # connecting to Roku to retrieve device Info
139            roku = Roku(ip)
140            try:
141                device = await roku.update()
142                await roku.close_session()
143            except Exception:
144                self.logger.error("Failed to retrieve device info from Roku at: %s", ip)
145                await roku.close_session()
146                return
147
148            if device.info.serial_number is None:
149                return
150
151            player_id = "ROKU_" + device.info.serial_number
152
153            if roku_player := self.roku_players.get(player_id):
154                # existing player
155                if roku_player.device_info.ip_address == ip and roku_player.available:
156                    # nothing to do, device is already connected
157                    return
158                # update description url to newly discovered one
159                roku_player.device_info.ip_address = ip
160            else:
161                roku_player = MediaAssistantPlayer(
162                    provider=self,
163                    player_id=player_id,
164                    roku_name=device.info.name if device.info.name is not None else "",
165                    roku=Roku(ip),
166                )
167
168                roku_player._attr_device_info = DeviceInfo(
169                    model=device.info.model_name if device.info.model_name is not None else "",
170                    model_id=device.info.model_number,
171                    manufacturer=device.info.brand,
172                )
173                roku_player._attr_device_info.ip_address = ip
174
175                self.roku_players[player_id] = roku_player
176            await roku_player.setup()
177