player = self.get_player(player_id)
if not player:
raise FileNotFoundError("Player not found %s" % player_id)
- if not player.calculated_state.powered:
- await self.cmd_power_on(player_id)
player_queue = self.get_active_player_queue(player_id)
+ if player_queue.queue_id != player_id and not player.calculated_state.powered:
+ # only force player on if its not the actual queue player
+ await self.cmd_power_on(player_id)
# a single item or list of items may be provided
if not isinstance(items, list):
items = [items]
player = self.get_player(player_id)
if not player:
raise FileNotFoundError("Player not found %s" % player_id)
- if not player.calculated_state.powered:
- await self.cmd_power_on(player_id)
- # load items into the queue
+
player_queue = self.get_active_player_queue(player_id)
+ if player_queue.queue_id != player_id and not player.calculated_state.powered:
+ # only force player on if its not the actual queue player
+ await self.cmd_power_on(player_id)
+ # load item into the queue
queue_item = player_queue.create_queue_item(
item_id=uri, provider="url", name=uri, uri=uri
)
player.calculated_state.name,
)
return
+ if player_queue.queue_id != player_id and not player.calculated_state.powered:
+ # only force player on if its not the actual queue player
await self.cmd_power_on(player_id)
# snapshot the (active) queue
prev_queue_items = player_queue.items
"""ChromeCast playerprovider."""
import logging
-from typing import List
+from typing import List, Optional
import pychromecast
from music_assistant.helpers.util import create_task
from pychromecast.controllers.multizone import MultizoneManager
from .const import PROV_ID, PROV_NAME, PROVIDER_CONFIG_ENTRIES
-from .models import ChromecastInfo
+from .helpers import DEFAULT_PORT, ChromecastInfo
from .player import ChromecastPlayer
LOGGER = logging.getLogger(PROV_ID)
def __init__(self, *args, **kwargs):
"""Initialize."""
self.mz_mgr = MultizoneManager()
- self._listener = None
- self._browser = None
+ self._browser: Optional[pychromecast.discovery.CastBrowser] = None
super().__init__(*args, **kwargs)
@property
async def on_start(self) -> bool:
"""Handle initialization of the provider based on config."""
- self._listener = pychromecast.CastListener(
- self.__chromecast_add_update_callback,
- self.__chromecast_remove_callback,
- self.__chromecast_add_update_callback,
+ self._browser = pychromecast.discovery.CastBrowser(
+ pychromecast.discovery.SimpleCastListener(
+ add_callback=self._discover_chromecast,
+ remove_callback=self._remove_chromecast,
+ update_callback=self._discover_chromecast,
+ ),
+ self.mass.zeroconf,
)
-
- def start_discovery():
- self._browser = pychromecast.discovery.start_discovery(
- self._listener, self.mass.zeroconf
- )
-
- create_task(start_discovery)
+ # start discovery in executor
+ create_task(self._browser.start_discovery)
return True
async def on_stop(self):
if not self._browser:
return
# stop discovery
- create_task(pychromecast.stop_discovery, self._browser)
-
- def __chromecast_add_update_callback(self, cast_uuid, cast_service_name):
- """Handle zeroconf discovery of a new or updated chromecast."""
- # pylint: disable=unused-argument
- service = self._listener.services[cast_uuid]
- cast_info = ChromecastInfo(
- services=service[0],
- uuid=service[1],
- model_name=service[2],
- friendly_name=service[3],
- host=service[4],
- port=service[5],
- )
- cast_info.fill_out_missing_chromecast_info(self.mass.zeroconf)
- if cast_info.uuid is None:
- return # Discovered chromecast without uuid?
- player_id = cast_info.uuid
- LOGGER.debug(
- "Chromecast discovered: %s (%s)", cast_info.friendly_name, player_id
+ create_task(self._browser.stop_discovery)
+
+ def _discover_chromecast(self, uuid, _):
+ """Discover a Chromecast."""
+ device_info = self._browser.devices[uuid]
+
+ info = ChromecastInfo(
+ services=device_info.services,
+ uuid=device_info.uuid,
+ model_name=device_info.model_name,
+ friendly_name=device_info.friendly_name,
+ is_audio_group=device_info.port != DEFAULT_PORT,
)
+
+ if info.uuid is None:
+ LOGGER.error("Discovered chromecast without uuid %s", info)
+ return
+
+ info = info.fill_out_missing_chromecast_info(self.mass.zeroconf)
+ if info.is_dynamic_group:
+ LOGGER.warning("Discovered dynamic cast group which will be ignored.")
+ return
+
+ LOGGER.debug("Discovered new or updated chromecast %s", info)
+ player_id = str(info.uuid)
player = self.mass.players.get_player(player_id)
if not player:
- # cast players may reappear with new uuid, try to handle that
- player = self.mass.players.get_player_by_name(
- cast_info.friendly_name, PROV_ID
- )
- if not player:
- player = ChromecastPlayer(self.mass, cast_info)
+ player = ChromecastPlayer(self.mass, info)
# if player was already added, the player will take care of reconnects itself.
- player.set_cast_info(cast_info)
+ player.set_cast_info(info)
create_task(self.mass.players.add_player(player))
- @staticmethod
- def __chromecast_remove_callback(cast_uuid, cast_service_name, cast_service):
- """Handle a Chromecast removed event."""
+ def _remove_chromecast(self, uuid, service, cast_info):
+ """Handle zeroconf discovery of a removed chromecast."""
# pylint: disable=unused-argument
- player_id = str(cast_service[1])
- friendly_name = cast_service[3]
+ player_id = str(service[1])
+ friendly_name = service[3]
LOGGER.debug("Chromecast removed: %s - %s", friendly_name, player_id)
# we ignore this event completely as the Chromecast socket client handles this itself
--- /dev/null
+"""Helpers to deal with Cast devices."""
+from __future__ import annotations
+
+from typing import Optional
+
+import attr
+from pychromecast import dial
+from pychromecast.const import CAST_MANUFACTURERS
+
+DEFAULT_PORT = 8009
+
+
+@attr.s(slots=True, frozen=True)
+class ChromecastInfo:
+ """Class to hold all data about a chromecast for creating connections.
+
+ This also has the same attributes as the mDNS fields by zeroconf.
+ """
+
+ services: set | None = attr.ib()
+ uuid: str | None = attr.ib(
+ converter=attr.converters.optional(str), default=None
+ ) # always convert UUID to string if not None
+ _manufacturer = attr.ib(type=Optional[str], default=None)
+ model_name: str = attr.ib(default="")
+ friendly_name: str | None = attr.ib(default=None)
+ is_audio_group = attr.ib(type=Optional[bool], default=False)
+ is_dynamic_group = attr.ib(type=Optional[bool], default=None)
+
+ @property
+ def is_information_complete(self) -> bool:
+ """Return if all information is filled out."""
+ want_dynamic_group = self.is_audio_group
+ have_dynamic_group = self.is_dynamic_group is not None
+ have_all_except_dynamic_group = all(
+ attr.astuple(
+ self,
+ filter=attr.filters.exclude(
+ attr.fields(ChromecastInfo).is_dynamic_group
+ ),
+ )
+ )
+ return have_all_except_dynamic_group and (
+ not want_dynamic_group or have_dynamic_group
+ )
+
+ @property
+ def manufacturer(self) -> str | None:
+ """Return the manufacturer."""
+ if self._manufacturer:
+ return self._manufacturer
+ if not self.model_name:
+ return None
+ return CAST_MANUFACTURERS.get(self.model_name.lower(), "Google Inc.")
+
+ def fill_out_missing_chromecast_info(self, zconf) -> ChromecastInfo:
+ """Return a new ChromecastInfo object with missing attributes filled in.
+
+ Uses blocking HTTP / HTTPS.
+ """
+ if self.is_information_complete:
+ # We have all information, no need to check HTTP API.
+ return self
+
+ # Fill out missing group information via HTTP API.
+ if self.is_audio_group:
+ is_dynamic_group = False
+ http_group_status = None
+ if self.uuid:
+ http_group_status = dial.get_multizone_status(
+ None,
+ services=self.services,
+ zconf=zconf,
+ )
+ if http_group_status is not None:
+ is_dynamic_group = any(
+ str(g.uuid) == self.uuid
+ for g in http_group_status.dynamic_groups
+ )
+
+ return ChromecastInfo(
+ services=self.services,
+ uuid=self.uuid,
+ friendly_name=self.friendly_name,
+ model_name=self.model_name,
+ is_audio_group=True,
+ is_dynamic_group=is_dynamic_group,
+ )
+
+ # Fill out some missing information (friendly_name, uuid) via HTTP dial.
+ http_device_status = dial.get_device_status(
+ None, services=self.services, zconf=zconf
+ )
+ if http_device_status is None:
+ # HTTP dial didn't give us any new information.
+ return self
+
+ return ChromecastInfo(
+ services=self.services,
+ uuid=(self.uuid or http_device_status.uuid),
+ friendly_name=(self.friendly_name or http_device_status.friendly_name),
+ manufacturer=(self.manufacturer or http_device_status.manufacturer),
+ model_name=(self.model_name or http_device_status.model_name),
+ )
+
+ def __str__(self):
+ """Return pretty printable string for logging."""
+ return f"{self.friendly_name} ({self.uuid})"
+
+
+class CastStatusListener:
+ """Helper class to handle pychromecast status callbacks.
+
+ Necessary because a CastDevice entity can create a new socket client
+ and therefore callbacks from multiple chromecast connections can
+ potentially arrive. This class allows invalidating past chromecast objects.
+ """
+
+ def __init__(self, cast_device, chromecast, mz_mgr, mz_only=False):
+ """Initialize the status listener."""
+ self._cast_device = cast_device
+ self._uuid = chromecast.uuid
+ self._valid = True
+ self._mz_mgr = mz_mgr
+
+ if cast_device._cast_info.is_audio_group:
+ self._mz_mgr.add_multizone(chromecast)
+ if mz_only:
+ return
+
+ chromecast.register_status_listener(self)
+ chromecast.socket_client.media_controller.register_status_listener(self)
+ chromecast.register_connection_listener(self)
+ if not cast_device._cast_info.is_audio_group:
+ self._mz_mgr.register_listener(chromecast.uuid, self)
+
+ def new_cast_status(self, cast_status):
+ """Handle reception of a new CastStatus."""
+ if self._valid:
+ self._cast_device.new_cast_status(cast_status)
+
+ def new_media_status(self, media_status):
+ """Handle reception of a new MediaStatus."""
+ if self._valid:
+ self._cast_device.new_media_status(media_status)
+
+ def new_connection_status(self, connection_status):
+ """Handle reception of a new ConnectionStatus."""
+ if self._valid:
+ self._cast_device.new_connection_status(connection_status)
+
+ @staticmethod
+ def added_to_multizone(group_uuid):
+ """Handle the cast added to a group."""
+
+ def removed_from_multizone(self, group_uuid):
+ """Handle the cast removed from a group."""
+ if self._valid:
+ self._cast_device.multizone_new_media_status(group_uuid, None)
+
+ def multizone_new_cast_status(self, group_uuid, cast_status):
+ """Handle reception of a new CastStatus for a group."""
+
+ def multizone_new_media_status(self, group_uuid, media_status):
+ """Handle reception of a new MediaStatus for a group."""
+ if self._valid:
+ self._cast_device.multizone_new_media_status(group_uuid, media_status)
+
+ def invalidate(self):
+ """Invalidate this status listener.
+
+ All following callbacks won't be forwarded.
+ """
+ # pylint: disable=protected-access
+ if self._cast_device._cast_info.is_audio_group:
+ self._mz_mgr.remove_multizone(self._uuid)
+ else:
+ self._mz_mgr.deregister_listener(self._uuid, self)
+ self._valid = False
+++ /dev/null
-"""
-Class to hold all data about a chromecast for creating connections.
-
-This also has the same attributes as the mDNS fields by zeroconf.
-"""
-import logging
-from dataclasses import dataclass, field
-from typing import Optional, Tuple
-
-from pychromecast import dial
-from pychromecast.const import CAST_MANUFACTURERS
-
-from .const import PROV_ID
-
-LOGGER = logging.getLogger(PROV_ID)
-DEFAULT_PORT = 8009
-
-
-@dataclass()
-class ChromecastInfo:
- """Class to hold all data about a chromecast for creating connections.
-
- This also has the same attributes as the mDNS fields by zeroconf.
- """
-
- services: Optional[set] = field(default_factory=set)
- uuid: Optional[str] = None
- model_name: str = ""
- friendly_name: Optional[str] = None
- is_dynamic_group: bool = False
- manufacturer: Optional[str] = None
- host: Optional[str] = ""
- port: Optional[int] = 0
- _info_requested: bool = field(init=False, default=False)
-
- def __post_init__(self):
- """Convert UUID to string."""
- self.uuid = str(self.uuid)
-
- @property
- def is_audio_group(self) -> bool:
- """Return if this is an audio group."""
- return self.port != DEFAULT_PORT
-
- @property
- def host_port(self) -> Tuple[str, int]:
- """Return the host+port tuple."""
- return self.host, self.port
-
- def fill_out_missing_chromecast_info(self, zconf) -> None:
- """Lookup missing info for the Chromecast player."""
- http_device_status = None
-
- if self._info_requested:
- return
-
- # Fill out missing group information via HTTP API.
- if self.is_audio_group:
- http_group_status = None
- if self.uuid:
- http_group_status = dial.get_multizone_status(
- None,
- services=self.services,
- zconf=zconf,
- )
- if http_group_status is not None:
- self.is_dynamic_group = any(
- str(g.uuid) == self.uuid
- for g in http_group_status.dynamic_groups
- )
- else:
- # Fill out some missing information (friendly_name, uuid) via HTTP dial.
- http_device_status = dial.get_device_status(
- None, services=self.services, zconf=zconf
- )
- if http_device_status:
- self.uuid = str(http_device_status.uuid)
- if not self.friendly_name and http_device_status:
- self.friendly_name = http_device_status.friendly_name
- if not self.model_name and http_device_status:
- self.model_name = http_device_status.model_name
- if not self.manufacturer and http_device_status:
- self.manufacturer = http_device_status.manufacturer
- if not self.manufacturer and self.model_name:
- self.manufacturer = CAST_MANUFACTURERS.get(
- self.model_name.lower(), "Google Inc."
- )
-
- self._info_requested = True
-
-
-class CastStatusListener:
- """Helper class to handle pychromecast status callbacks.
-
- Necessary because a CastDevice entity can create a new socket client
- and therefore callbacks from multiple chromecast connections can
- potentially arrive. This class allows invalidating past chromecast objects.
- """
-
- def __init__(self, cast_device, chromecast, mz_mgr):
- """Initialize the status listener."""
- self._cast_device = cast_device
- self._uuid = chromecast.uuid
- self._valid = True
- self._mz_mgr = mz_mgr
-
- chromecast.register_status_listener(self)
- chromecast.socket_client.media_controller.register_status_listener(self)
- chromecast.register_connection_listener(self)
- if cast_device._cast_info.is_audio_group:
- self._mz_mgr.add_multizone(chromecast)
- else:
- self._mz_mgr.register_listener(chromecast.uuid, self)
-
- def new_cast_status(self, cast_status):
- """Handle reception of a new CastStatus."""
- if self._valid:
- self._cast_device.new_cast_status(cast_status)
-
- def new_media_status(self, media_status):
- """Handle reception of a new MediaStatus."""
- if self._valid:
- self._cast_device.new_media_status(media_status)
-
- def new_connection_status(self, connection_status):
- """Handle reception of a new ConnectionStatus."""
- if self._valid:
- self._cast_device.new_connection_status(connection_status)
-
- def added_to_multizone(self, group_uuid):
- """Handle the cast added to a group."""
- LOGGER.debug(
- "Player %s is added to group %s", self._cast_device.name, group_uuid
- )
-
- def removed_from_multizone(self, group_uuid):
- """Handle the cast removed from a group."""
- if self._valid:
- self._cast_device.multizone_new_media_status(group_uuid, None)
-
- def multizone_new_cast_status(self, group_uuid, cast_status):
- """Handle reception of a new CastStatus for a group."""
-
- def multizone_new_media_status(self, group_uuid, media_status):
- """Handle reception of a new MediaStatus for a group."""
- if self._valid:
- self._cast_device.multizone_new_media_status(group_uuid, media_status)
-
- def invalidate(self):
- """Invalidate this status listener.
-
- All following callbacks won't be forwarded.
- """
- # pylint: disable=protected-access
- if self._cast_device._cast_info.is_audio_group:
- self._mz_mgr.remove_multizone(self._uuid)
- else:
- self._mz_mgr.deregister_listener(self._uuid, self)
- self._valid = False
"""Representation of a Cast device on the network."""
+import asyncio
import logging
import uuid
from typing import List, Optional
import pychromecast
-from asyncio_throttle import Throttler
from music_assistant.helpers.compare import compare_strings
from music_assistant.helpers.typing import MusicAssistant
from music_assistant.helpers.util import create_task, yield_chunks
from music_assistant.models.config_entry import ConfigEntry
from music_assistant.models.player import DeviceInfo, Player, PlayerFeature, PlayerState
from music_assistant.models.player_queue import QueueItem
-from pychromecast.controllers.multizone import MultizoneController
+from pychromecast.controllers.multizone import MultizoneController, MultizoneManager
from pychromecast.socket_client import (
CONNECTION_STATUS_CONNECTED,
CONNECTION_STATUS_DISCONNECTED,
)
from .const import PLAYER_CONFIG_ENTRIES, PROV_ID
-from .models import CastStatusListener, ChromecastInfo
+from .helpers import CastStatusListener, ChromecastInfo
LOGGER = logging.getLogger(PROV_ID)
PLAYER_FEATURES = [PlayerFeature.QUEUE]
self.cast_status = None
self.media_status = None
self.media_status_received = None
- self.mz_mgr = None
- self.mz_manager = None
+ self.mz_mgr: Optional[MultizoneManager] = None
self._available = False
self._status_listener: Optional[CastStatusListener] = None
self._is_speaker_group = False
- self._throttler = Throttler(rate_limit=1, period=0.1)
@property
def player_id(self) -> str:
return False
if self.is_group_player:
return (
- self.media_status.player_is_playing
- or self.media_status.player_is_paused
- or self.media_status.player_is_idle
+ self._chromecast.media_controller.is_active
+ and self.cast_status.app_id == pychromecast.APP_MEDIA_RECEIVER
)
+ # return self._chromecast is not None and self._chromecast.is_idle
+ # return (
+ # self._chromecast.media_controller.app_id
+ # == pychromecast.config.APP_MEDIA_RECEIVER
+ # )
+
# Chromecast does not support power so we (ab)use mute instead
- return (
- not self.cast_status.display_name
- or self.cast_status.display_name == "Default Media Receiver"
- ) and not self.cast_status.volume_muted
+ if self._chromecast.media_controller.is_active:
+ return (
+ self.cast_status.app_id
+ in ["705D30C6", self._chromecast.media_controller.app_id]
+ and not self.cast_status.volume_muted
+ )
+ return not self.cast_status.volume_muted
@property
def should_poll(self) -> bool:
"""Return deviceinfo."""
return DeviceInfo(
model=self._cast_info.model_name,
- address=f"{self._cast_info.host}:{self._cast_info.port}",
+ address=f"{self._chromecast.uri}" if self._chromecast else "",
manufacturer=self._cast_info.manufacturer,
)
self.mass.zeroconf,
)
self._chromecast = chromecast
- self.mz_mgr = self.mass.get_provider(PROV_ID).mz_mgr
-
+ self.mz_mgr: MultizoneManager = self.mass.get_provider(PROV_ID).mz_mgr
self._status_listener = CastStatusListener(self, chromecast, self.mz_mgr)
self._available = False
self.cast_status = chromecast.status
async def cmd_stop(self) -> None:
"""Send stop command to player."""
- if self._chromecast and self._chromecast.media_controller:
- await self.chromecast_command(self._chromecast.quit_app)
+ if self._chromecast.media_controller:
+ await self.chromecast_command(self._chromecast.media_controller.stop)
async def cmd_play(self) -> None:
"""Send play command to player."""
async def cmd_power_on(self) -> None:
"""Send power ON command to player."""
- await self.chromecast_command(self._chromecast.set_volume_muted, False)
+ if self.is_group_player:
+ await self.launch_app()
+ else:
+ # chromecast has no real poweroff so we (ab)use mute instead
+ await self.chromecast_command(self._chromecast.set_volume_muted, False)
async def cmd_power_off(self) -> None:
"""Send power OFF command to player."""
- # chromecast has no real poweroff so we send mute instead
- await self.chromecast_command(self._chromecast.set_volume_muted, True)
+ if self.is_group_player or (
+ self._chromecast.media_controller.is_active
+ and self.cast_status.app_id == self._chromecast.media_controller.app_id
+ ):
+ await self.chromecast_command(self._chromecast.quit_app)
+ if not self.is_group_player:
+ # chromecast has no real poweroff so we (ab)use mute instead
+ await self.chromecast_command(self._chromecast.set_volume_muted, True)
async def cmd_volume_set(self, volume_level: int) -> None:
"""Send new volume level command to player."""
async def cmd_queue_load(self, queue_items: List[QueueItem]) -> None:
"""Load (overwrite) queue with new items."""
player_queue = self.mass.players.get_player_queue(self.player_id)
- cc_queue_items = self.__create_queue_items(queue_items[:50])
+ cc_queue_items = self.__create_queue_items(queue_items[:25])
repeat_enabled = player_queue.use_queue_stream or player_queue.repeat_enabled
queuedata = {
"type": "QUEUE_LOAD",
"shuffle": False, # handled by our queue controller
"queueType": "PLAYLIST",
"startIndex": 0, # Item index to play after this request or keep same item if undefined
- "items": cc_queue_items, # only load 50 tracks at once or the socket will crash
+ "items": cc_queue_items, # only load 25 tracks at once or the socket will crash
}
+ await self.launch_app()
await self.chromecast_command(self.__send_player_queue, queuedata)
- if len(queue_items) > 50:
- await self.cmd_queue_append(queue_items[51:])
+ if len(queue_items) > 25:
+ await self.cmd_queue_append(queue_items[26:])
async def cmd_queue_append(self, queue_items: List[QueueItem]) -> None:
"""Append new items at the end of the queue."""
cc_queue_items = self.__create_queue_items(queue_items)
- async for chunk in yield_chunks(cc_queue_items, 50):
+ async for chunk in yield_chunks(cc_queue_items, 25):
queuedata = {
"type": "QUEUE_INSERT",
"insertBefore": None,
"items": chunk,
}
+ await self.launch_app()
await self.chromecast_command(self.__send_player_queue, queuedata)
def __create_queue_items(self, tracks) -> None:
"""Create list of CC queue items from tracks."""
return [self.__create_queue_item(track) for track in tracks]
- def __create_queue_item(self, queue_item: QueueItem):
+ @staticmethod
+ def __create_queue_item(queue_item: QueueItem):
"""Create CC queue item from track info."""
- player_queue = self.mass.players.get_player_queue(self.player_id)
return {
"opt_itemId": queue_item.queue_item_id,
"autoplay": True,
"item_id": queue_item.queue_item_id,
},
"contentType": "audio/flac",
- "streamType": "LIVE" if player_queue.use_queue_stream else "BUFFERED",
+ "streamType": pychromecast.STREAM_TYPE_BUFFERED,
"metadata": {
"title": queue_item.name,
"artist": "/".join(x.name for x in queue_item.artists),
def __send_player_queue(self, queuedata: dict) -> None:
"""Send new data to the CC queue."""
media_controller = self._chromecast.media_controller
- # pylint: disable=protected-access
- receiver_ctrl = media_controller._socket_client.receiver_controller
+ queuedata["mediaSessionId"] = media_controller.status.media_session_id
+ media_controller.send_message(queuedata, False)
- def send_queue():
- """Plays media after chromecast has switched to requested app."""
- queuedata["mediaSessionId"] = media_controller.status.media_session_id
- media_controller.send_message(queuedata, False)
+ async def launch_app(self):
+ """Launch the default media receiver app and wait until its launched."""
+ media_controller = self._chromecast.media_controller
+ if (
+ media_controller.is_active
+ and self.cast_status.app_id == pychromecast.APP_MEDIA_RECEIVER
+ and media_controller.status.media_session_id is not None
+ ):
+ # already active
+ return
- if not media_controller.status.media_session_id:
- receiver_ctrl.launch_app(
- media_controller.app_id,
- callback_function=send_queue,
- )
- else:
- send_queue()
+ event = asyncio.Event()
+
+ def launched():
+ self.mass.loop.call_soon_threadsafe(event.set)
+
+ # pylint: disable=protected-access
+ receiver_ctrl = media_controller._socket_client.receiver_controller
+ receiver_ctrl.launch_app(
+ media_controller.app_id,
+ callback_function=launched,
+ )
+ await event.wait()
async def chromecast_command(self, func, *args, **kwargs):
"""Execute command on Chromecast."""
- # Chromecast socket really doesn't like multiple commands arriving at the same time
- # so we apply some throtling.
if not self.available:
LOGGER.warning(
"Player %s is not available, command can't be executed", self.name
)
return
- async with self._throttler:
- create_task(func, *args, **kwargs)
+ await create_task(func, *args, **kwargs)