parent
d80da944a3
commit
a40deac714
|
@ -203,29 +203,33 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
||||||
class BluesoundPlayer(MediaPlayerEntity):
|
class BluesoundPlayer(MediaPlayerEntity):
|
||||||
"""Representation of a Bluesound Player."""
|
"""Representation of a Bluesound Player."""
|
||||||
|
|
||||||
_attr_media_content_type = MEDIA_TYPE_MUSIC
|
def __init__(self, hass, host, port=None, name=None, init_callback=None):
|
||||||
|
|
||||||
def __init__(self, hass, host, port=DEFAULT_PORT, name=None, init_callback=None):
|
|
||||||
"""Initialize the media player."""
|
"""Initialize the media player."""
|
||||||
self.host = host
|
self.host = host
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self.port = port
|
self.port = port
|
||||||
self._polling_session = async_get_clientsession(hass)
|
self._polling_session = async_get_clientsession(hass)
|
||||||
self._polling_task = None # The actual polling task.
|
self._polling_task = None # The actual polling task.
|
||||||
self._attr_name = name
|
self._name = name
|
||||||
|
self._icon = None
|
||||||
self._capture_items = []
|
self._capture_items = []
|
||||||
self._services_items = []
|
self._services_items = []
|
||||||
self._preset_items = []
|
self._preset_items = []
|
||||||
self._sync_status = {}
|
self._sync_status = {}
|
||||||
self._status = None
|
self._status = None
|
||||||
self._is_online = None
|
self._last_status_update = None
|
||||||
|
self._is_online = False
|
||||||
self._retry_remove = None
|
self._retry_remove = None
|
||||||
|
self._muted = False
|
||||||
self._master = None
|
self._master = None
|
||||||
self._group_name = None
|
|
||||||
self._bluesound_device_name = None
|
|
||||||
self._is_master = False
|
self._is_master = False
|
||||||
|
self._group_name = None
|
||||||
self._group_list = []
|
self._group_list = []
|
||||||
|
self._bluesound_device_name = None
|
||||||
|
|
||||||
self._init_callback = init_callback
|
self._init_callback = init_callback
|
||||||
|
if self.port is None:
|
||||||
|
self.port = DEFAULT_PORT
|
||||||
|
|
||||||
class _TimeoutException(Exception):
|
class _TimeoutException(Exception):
|
||||||
pass
|
pass
|
||||||
|
@ -248,12 +252,12 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||||
return None
|
return None
|
||||||
self._sync_status = resp["SyncStatus"].copy()
|
self._sync_status = resp["SyncStatus"].copy()
|
||||||
|
|
||||||
if not self.name:
|
if not self._name:
|
||||||
self._attr_name = self._sync_status.get("@name", self.host)
|
self._name = self._sync_status.get("@name", self.host)
|
||||||
if not self._bluesound_device_name:
|
if not self._bluesound_device_name:
|
||||||
self._bluesound_device_name = self._sync_status.get("@name", self.host)
|
self._bluesound_device_name = self._sync_status.get("@name", self.host)
|
||||||
if not self.icon:
|
if not self._icon:
|
||||||
self._attr_icon = self._sync_status.get("@icon", self.host)
|
self._icon = self._sync_status.get("@icon", self.host)
|
||||||
|
|
||||||
master = self._sync_status.get("master")
|
master = self._sync_status.get("master")
|
||||||
if master is not None:
|
if master is not None:
|
||||||
|
@ -287,14 +291,14 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||||
await self.async_update_status()
|
await self.async_update_status()
|
||||||
|
|
||||||
except (asyncio.TimeoutError, ClientError, BluesoundPlayer._TimeoutException):
|
except (asyncio.TimeoutError, ClientError, BluesoundPlayer._TimeoutException):
|
||||||
_LOGGER.info("Node %s is offline, retrying later", self.name)
|
_LOGGER.info("Node %s is offline, retrying later", self._name)
|
||||||
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
|
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
|
||||||
self.start_polling()
|
self.start_polling()
|
||||||
|
|
||||||
except CancelledError:
|
except CancelledError:
|
||||||
_LOGGER.debug("Stopping the polling of node %s", self.name)
|
_LOGGER.debug("Stopping the polling of node %s", self._name)
|
||||||
except Exception:
|
except Exception:
|
||||||
_LOGGER.exception("Unexpected error in %s", self.name)
|
_LOGGER.exception("Unexpected error in %s", self._name)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def start_polling(self):
|
def start_polling(self):
|
||||||
|
@ -398,7 +402,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||||
if response.status == HTTP_OK:
|
if response.status == HTTP_OK:
|
||||||
result = await response.text()
|
result = await response.text()
|
||||||
self._is_online = True
|
self._is_online = True
|
||||||
self._attr_media_position_updated_at = dt_util.utcnow()
|
self._last_status_update = dt_util.utcnow()
|
||||||
self._status = xmltodict.parse(result)["status"].copy()
|
self._status = xmltodict.parse(result)["status"].copy()
|
||||||
|
|
||||||
group_name = self._status.get("groupName")
|
group_name = self._status.get("groupName")
|
||||||
|
@ -434,58 +438,11 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||||
|
|
||||||
except (asyncio.TimeoutError, ClientError):
|
except (asyncio.TimeoutError, ClientError):
|
||||||
self._is_online = False
|
self._is_online = False
|
||||||
self._attr_media_position_updated_at = None
|
self._last_status_update = None
|
||||||
self._status = None
|
self._status = None
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
_LOGGER.info("Client connection error, marking %s as offline", self.name)
|
_LOGGER.info("Client connection error, marking %s as offline", self._name)
|
||||||
raise
|
raise
|
||||||
self.update_state_attr()
|
|
||||||
|
|
||||||
def update_state_attr(self):
|
|
||||||
"""Update state attributes."""
|
|
||||||
if self._status is None:
|
|
||||||
self._attr_state = STATE_OFF
|
|
||||||
self._attr_supported_features = 0
|
|
||||||
elif self.is_grouped and not self.is_master:
|
|
||||||
self._attr_state = STATE_GROUPED
|
|
||||||
self._attr_supported_features = (
|
|
||||||
SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
status = self._status.get("state")
|
|
||||||
self._attr_state = STATE_IDLE
|
|
||||||
if status in ("pause", "stop"):
|
|
||||||
self._attr_state = STATE_PAUSED
|
|
||||||
elif status in ("stream", "play"):
|
|
||||||
self._attr_state = STATE_PLAYING
|
|
||||||
supported = SUPPORT_CLEAR_PLAYLIST
|
|
||||||
if self._status.get("indexing", "0") == "0":
|
|
||||||
supported = (
|
|
||||||
supported
|
|
||||||
| SUPPORT_PAUSE
|
|
||||||
| SUPPORT_PREVIOUS_TRACK
|
|
||||||
| SUPPORT_NEXT_TRACK
|
|
||||||
| SUPPORT_PLAY_MEDIA
|
|
||||||
| SUPPORT_STOP
|
|
||||||
| SUPPORT_PLAY
|
|
||||||
| SUPPORT_SELECT_SOURCE
|
|
||||||
| SUPPORT_SHUFFLE_SET
|
|
||||||
)
|
|
||||||
if self.volume_level is not None and self.volume_level >= 0:
|
|
||||||
supported = (
|
|
||||||
supported
|
|
||||||
| SUPPORT_VOLUME_STEP
|
|
||||||
| SUPPORT_VOLUME_SET
|
|
||||||
| SUPPORT_VOLUME_MUTE
|
|
||||||
)
|
|
||||||
if self._status.get("canSeek", "") == "1":
|
|
||||||
supported = supported | SUPPORT_SEEK
|
|
||||||
self._attr_supported_features = supported
|
|
||||||
self._attr_extra_state_attributes = {}
|
|
||||||
if self._group_list:
|
|
||||||
self._attr_extra_state_attributes = {ATTR_BLUESOUND_GROUP: self._group_list}
|
|
||||||
self._attr_extra_state_attributes[ATTR_MASTER] = self._is_master
|
|
||||||
self._attr_shuffle = self._status.get("shuffle", "0") == "1"
|
|
||||||
|
|
||||||
async def async_trigger_sync_on_all(self):
|
async def async_trigger_sync_on_all(self):
|
||||||
"""Trigger sync status update on all devices."""
|
"""Trigger sync status update on all devices."""
|
||||||
|
@ -585,6 +542,27 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||||
|
|
||||||
return self._services_items
|
return self._services_items
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_content_type(self):
|
||||||
|
"""Content type of current playing media."""
|
||||||
|
return MEDIA_TYPE_MUSIC
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the device."""
|
||||||
|
if self._status is None:
|
||||||
|
return STATE_OFF
|
||||||
|
|
||||||
|
if self.is_grouped and not self.is_master:
|
||||||
|
return STATE_GROUPED
|
||||||
|
|
||||||
|
status = self._status.get("state")
|
||||||
|
if status in ("pause", "stop"):
|
||||||
|
return STATE_PAUSED
|
||||||
|
if status in ("stream", "play"):
|
||||||
|
return STATE_PLAYING
|
||||||
|
return STATE_IDLE
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_title(self):
|
def media_title(self):
|
||||||
"""Title of current playing media."""
|
"""Title of current playing media."""
|
||||||
|
@ -639,7 +617,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
mediastate = self.state
|
mediastate = self.state
|
||||||
if self.media_position_updated_at is None or mediastate == STATE_IDLE:
|
if self._last_status_update is None or mediastate == STATE_IDLE:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
position = self._status.get("secs")
|
position = self._status.get("secs")
|
||||||
|
@ -648,9 +626,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||||
|
|
||||||
position = float(position)
|
position = float(position)
|
||||||
if mediastate == STATE_PLAYING:
|
if mediastate == STATE_PLAYING:
|
||||||
position += (
|
position += (dt_util.utcnow() - self._last_status_update).total_seconds()
|
||||||
dt_util.utcnow() - self.media_position_updated_at
|
|
||||||
).total_seconds()
|
|
||||||
|
|
||||||
return position
|
return position
|
||||||
|
|
||||||
|
@ -665,6 +641,11 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||||
return None
|
return None
|
||||||
return float(duration)
|
return float(duration)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_position_updated_at(self):
|
||||||
|
"""Last time status was updated."""
|
||||||
|
return self._last_status_update
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def volume_level(self):
|
def volume_level(self):
|
||||||
"""Volume level of the media player (0..1)."""
|
"""Volume level of the media player (0..1)."""
|
||||||
|
@ -687,11 +668,21 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||||
mute = bool(int(mute))
|
mute = bool(int(mute))
|
||||||
return mute
|
return mute
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the device."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bluesound_device_name(self):
|
def bluesound_device_name(self):
|
||||||
"""Return the device name as returned by the device."""
|
"""Return the device name as returned by the device."""
|
||||||
return self._bluesound_device_name
|
return self._bluesound_device_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""Return the icon of the device."""
|
||||||
|
return self._icon
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source_list(self):
|
def source_list(self):
|
||||||
"""List of available input sources."""
|
"""List of available input sources."""
|
||||||
|
@ -787,15 +778,58 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_master(self) -> bool:
|
def supported_features(self):
|
||||||
|
"""Flag of media commands that are supported."""
|
||||||
|
if self._status is None:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if self.is_grouped and not self.is_master:
|
||||||
|
return SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE
|
||||||
|
|
||||||
|
supported = SUPPORT_CLEAR_PLAYLIST
|
||||||
|
|
||||||
|
if self._status.get("indexing", "0") == "0":
|
||||||
|
supported = (
|
||||||
|
supported
|
||||||
|
| SUPPORT_PAUSE
|
||||||
|
| SUPPORT_PREVIOUS_TRACK
|
||||||
|
| SUPPORT_NEXT_TRACK
|
||||||
|
| SUPPORT_PLAY_MEDIA
|
||||||
|
| SUPPORT_STOP
|
||||||
|
| SUPPORT_PLAY
|
||||||
|
| SUPPORT_SELECT_SOURCE
|
||||||
|
| SUPPORT_SHUFFLE_SET
|
||||||
|
)
|
||||||
|
|
||||||
|
current_vol = self.volume_level
|
||||||
|
if current_vol is not None and current_vol >= 0:
|
||||||
|
supported = (
|
||||||
|
supported
|
||||||
|
| SUPPORT_VOLUME_STEP
|
||||||
|
| SUPPORT_VOLUME_SET
|
||||||
|
| SUPPORT_VOLUME_MUTE
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._status.get("canSeek", "") == "1":
|
||||||
|
supported = supported | SUPPORT_SEEK
|
||||||
|
|
||||||
|
return supported
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_master(self):
|
||||||
"""Return true if player is a coordinator."""
|
"""Return true if player is a coordinator."""
|
||||||
return self._is_master
|
return self._is_master
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_grouped(self) -> bool:
|
def is_grouped(self):
|
||||||
"""Return true if player is a coordinator."""
|
"""Return true if player is a coordinator."""
|
||||||
return self._master is not None or self._is_master
|
return self._master is not None or self._is_master
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shuffle(self):
|
||||||
|
"""Return true if shuffle is active."""
|
||||||
|
return self._status.get("shuffle", "0") == "1"
|
||||||
|
|
||||||
async def async_join(self, master):
|
async def async_join(self, master):
|
||||||
"""Join the player to a group."""
|
"""Join the player to a group."""
|
||||||
master_device = [
|
master_device = [
|
||||||
|
@ -815,6 +849,17 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||||
else:
|
else:
|
||||||
_LOGGER.error("Master not found %s", master_device)
|
_LOGGER.error("Master not found %s", master_device)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extra_state_attributes(self):
|
||||||
|
"""List members in group."""
|
||||||
|
attributes = {}
|
||||||
|
if self._group_list:
|
||||||
|
attributes = {ATTR_BLUESOUND_GROUP: self._group_list}
|
||||||
|
|
||||||
|
attributes[ATTR_MASTER] = self._is_master
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
|
||||||
def rebuild_bluesound_group(self):
|
def rebuild_bluesound_group(self):
|
||||||
"""Rebuild the list of entities in speaker group."""
|
"""Rebuild the list of entities in speaker group."""
|
||||||
if self._group_name is None:
|
if self._group_name is None:
|
||||||
|
|
Loading…
Reference in New Issue