"python.linting.pylintEnabled": true,
"python.linting.pylintArgs": ["--rcfile=${workspaceFolder}/setup.cfg"],
"python.linting.enabled": true,
- "python.pythonPath": "/Users/marcelvanderveldt/Workdir/music-assistant/server/.venv/bin/python3",
+ "python.pythonPath": "/Users/marcelvanderveldt/Workdir/music-assistant/server/.venv/bin/python3.9",
"python.linting.flake8Enabled": true,
"python.linting.flake8Args": ["--config=${workspaceFolder}/setup.cfg"],
"python.linting.mypyEnabled": false,
import logging
from typing import AsyncGenerator, List, Optional
+from async_timeout import timeout
+
LOGGER = logging.getLogger("AsyncProcess")
DEFAULT_CHUNKSIZE = 512000
+DEFAULT_TIMEOUT = 5
class AsyncProcess:
if len(chunk) < chunk_size:
break
- async def read(self, chunk_size: int = DEFAULT_CHUNKSIZE) -> bytes:
+ async def read(
+ self, chunk_size: int = DEFAULT_CHUNKSIZE, time_out: int = DEFAULT_TIMEOUT
+ ) -> bytes:
"""Read x bytes from the process stdout."""
try:
- return await self._proc.stdout.readexactly(chunk_size)
+ async with timeout(time_out):
+ return await self._proc.stdout.readexactly(chunk_size)
except asyncio.IncompleteReadError as err:
return err.partial
"""PlayerManager: Orchestrates all players from player providers."""
+import asyncio
import logging
from typing import Dict, List, Optional, Set, Tuple, Union
EVENT_PLAYER_REMOVED,
)
from music_assistant.helpers.typing import MusicAssistant
-from music_assistant.helpers.util import callback, run_periodic, try_parse_int
+from music_assistant.helpers.util import callback, try_parse_int
from music_assistant.helpers.web import api_route
from music_assistant.models.media_types import MediaItem, MediaType
from music_assistant.models.player import (
for player in self:
await player.on_remove()
- @run_periodic(30)
async def poll_task(self):
"""Check for updates on players that need to be polled."""
- for player in self:
- if not player.player_state.available:
- continue
- if not player.should_poll:
- continue
- await player.on_poll()
+ count = 0
+ while True:
+ for player in self:
+ if not player.player_state.available:
+ continue
+ if not player.should_poll:
+ continue
+ if player.state == PlaybackState.Playing or count == POLL_INTERVAL:
+ await player.on_poll()
+ if count == POLL_INTERVAL:
+ count = 0
+ else:
+ count += 1
+ await asyncio.sleep(1)
@property
def players(self) -> Dict[str, Player]:
host=service[4],
port=service[5],
)
+ cast_info.fill_out_missing_chromecast_info(self.mass.zeroconf)
player_id = cast_info.uuid
LOGGER.debug(
"Chromecast discovered: %s (%s)", cast_info.friendly_name, player_id
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
"""
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
- uuid: Optional[str] = ""
- model_name: str = ""
- friendly_name: Optional[str] = ""
+ _info_requested: bool = field(init=False, default=False)
def __post_init__(self):
"""Convert UUID to string."""
"""Return the host+port tuple."""
return self.host, self.port
- @property
- def manufacturer(self) -> str:
- """Return the 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) -> 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(
+ self.host,
+ 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(
+ self.host, services=self.services, zconf=zconf
+ )
+ if not self.uuid and http_device_status:
+ self.uuid = 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:
@property
def powered(self) -> bool:
"""Return power state of this player."""
+ if self.is_group_player:
+ return self._chromecast.is_idle
return not self.cast_status.volume_muted if self.cast_status else False
@property
"""Call when player is added to the player manager."""
chromecast = await self.mass.loop.run_in_executor(
None,
- pychromecast.get_chromecast_from_service,
- (
+ pychromecast.get_chromecast_from_cast_info,
+ pychromecast.discovery.CastInfo(
self.services,
self._cast_info.uuid,
self._cast_info.model_name,
mz_controller = MultizoneController(chromecast.uuid)
chromecast.register_handler(mz_controller)
chromecast.mz_controller = mz_controller
- self._chromecast.start()
+ chromecast.start()
def set_cast_info(self, cast_info: ChromecastInfo) -> None:
"""Set (or update) the cast discovery info."""
argparse==1.4.0
+async-timeout==3.0.1
pychromecast==9.1.1
aiohttp[speedups]==3.7.4
asyncio-throttle==1.0.1