music-assistant-server

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