music-assistant-server

6.4 KBPY
provider.py
6.4 KB169 lines • python
1"""Media Assistant Provider implementation."""
2
3from __future__ import annotations
4
5import asyncio
6import logging
7from typing import TYPE_CHECKING, ClassVar, cast
8
9from music_assistant_models.config_entries import ConfigEntry
10from music_assistant_models.enums import ConfigEntryType, 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, CONF_ROKU_APP_ID
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: ClassVar[dict[str, MediaAssistantPlayer]] = {}
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 get_config_entries(self) -> tuple[ConfigEntry, ...]:
40        """Return Config entries to setup this provider."""
41        return (
42            CONF_ENTRY_MANUAL_DISCOVERY_IPS,
43            ConfigEntry(
44                key=CONF_ROKU_APP_ID,
45                type=ConfigEntryType.STRING,
46                default_value="782875",
47                required=False,
48                advanced=True,
49            ),
50            ConfigEntry(
51                key=CONF_AUTO_DISCOVER,
52                type=ConfigEntryType.BOOLEAN,
53                default_value=True,
54                advanced=True,
55            ),
56        )
57
58    async def handle_async_init(self) -> None:
59        """Handle async initialization of the provider."""
60        self.lock = asyncio.Lock()
61        # silence the rokuecp logger
62        if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL):
63            logging.getLogger("rokuecp").setLevel(logging.DEBUG)
64        else:
65            logging.getLogger("rokuecp").setLevel(self.logger.level + 10)
66
67    async def loaded_in_mass(self) -> None:
68        """Call after the provider has been loaded."""
69        manual_ip_config = cast(
70            "list[str]", self.config.get_value(CONF_ENTRY_MANUAL_DISCOVERY_IPS.key)
71        )
72
73        for ip in manual_ip_config:
74            await self._device_discovered(ip)
75
76        self.logger.info("MediaAssistantProvider loaded")
77
78    async def unload(self, is_removed: bool = False) -> None:
79        """Handle unload/close of the provider."""
80        if self.roku_players is None:
81            return  # type: ignore[unreachable]
82        async with TaskManager(self.mass) as tg:
83            for roku_player in self.roku_players.values():
84                tg.create_task(self._device_disconnect(roku_player))
85
86    async def on_upnp_service_discovered(
87        self, search_target: str, discovery_info: CaseInsensitiveDict
88    ) -> None:
89        """Handle SSDP discovery callbacks."""
90        del search_target
91        if not self.config.get_value(CONF_AUTO_DISCOVER):
92            return
93        ssdp_st: str | None = discovery_info.get("st")
94        if not ssdp_st or "roku:ecp" not in ssdp_st:
95            return
96        if not discovery_info.get("usn"):
97            return
98        device_ip: str | None = discovery_info.get("_host")
99        if not device_ip:
100            return
101        await self._device_discovered(device_ip)
102
103    async def _device_disconnect(self, roku_player: MediaAssistantPlayer) -> None:
104        """Destroy connections to the device."""
105        async with roku_player.lock:
106            if not roku_player.roku:
107                self.logger.debug("Disconnecting from device that's not connected")
108                return
109
110            self.logger.debug("Disconnecting from %s", roku_player.name)
111
112            old_device = roku_player.roku
113            self.roku_players.pop(roku_player.player_id)
114            await old_device.close_session()
115
116    async def _device_discovered(self, ip: str) -> None:
117        """Handle discovered Roku."""
118        async with self.lock:
119            # connecting to Roku to retrieve device Info
120            roku = Roku(ip)
121            try:
122                device = await roku.update()
123                await roku.close_session()
124            except Exception:
125                self.logger.error("Failed to retrieve device info from Roku at: %s", ip)
126                await roku.close_session()
127                return
128
129            if device.info.serial_number is None:
130                return
131
132            player_id = "ROKU_" + device.info.serial_number
133
134            if roku_player := self.roku_players.get(player_id):
135                # existing player
136                if roku_player.device_info.ip_address == ip and roku_player.available:
137                    # nothing to do, device is already connected
138                    return
139                # update description url to newly discovered one
140                roku_player.device_info.add_identifier(IdentifierType.IP_ADDRESS, ip)
141            else:
142                roku_player = MediaAssistantPlayer(
143                    provider=self,
144                    player_id=player_id,
145                    roku_name=device.info.name if device.info.name is not None else "",
146                    roku=Roku(ip),
147                )
148
149                roku_player._attr_device_info = DeviceInfo(
150                    model=device.info.model_name if device.info.model_name is not None else "",
151                    model_id=device.info.model_number,
152                    manufacturer=device.info.brand,
153                )
154                roku_player._attr_device_info.add_identifier(IdentifierType.IP_ADDRESS, ip)
155                roku_player._attr_device_info.add_identifier(
156                    IdentifierType.SERIAL_NUMBER, device.info.serial_number
157                )
158                if device.info.ethernet_mac:
159                    roku_player._attr_device_info.add_identifier(
160                        IdentifierType.MAC_ADDRESS, device.info.ethernet_mac
161                    )
162                elif device.info.wifi_mac:
163                    roku_player._attr_device_info.add_identifier(
164                        IdentifierType.MAC_ADDRESS, device.info.wifi_mac
165                    )
166
167                self.roku_players[player_id] = roku_player
168            await roku_player.setup()
169