Home Assistant Unofficial Reference 2024.12.1
media_player.py
Go to the documentation of this file.
1 """Support for interacting with Snapcast clients."""
2 
3 from __future__ import annotations
4 
5 from snapcast.control.server import Snapserver
6 import voluptuous as vol
7 
9  MediaPlayerEntity,
10  MediaPlayerEntityFeature,
11  MediaPlayerState,
12 )
13 from homeassistant.config_entries import ConfigEntry
14 from homeassistant.const import CONF_HOST, CONF_PORT
15 from homeassistant.core import HomeAssistant
16 from homeassistant.helpers import config_validation as cv, entity_platform
17 from homeassistant.helpers.entity_platform import AddEntitiesCallback
18 
19 from .const import (
20  ATTR_LATENCY,
21  ATTR_MASTER,
22  CLIENT_PREFIX,
23  CLIENT_SUFFIX,
24  DOMAIN,
25  GROUP_PREFIX,
26  GROUP_SUFFIX,
27  SERVICE_JOIN,
28  SERVICE_RESTORE,
29  SERVICE_SET_LATENCY,
30  SERVICE_SNAPSHOT,
31  SERVICE_UNJOIN,
32 )
33 
34 STREAM_STATUS = {
35  "idle": MediaPlayerState.IDLE,
36  "playing": MediaPlayerState.PLAYING,
37  "unknown": None,
38 }
39 
40 
42  """Register snapcast services."""
43  platform = entity_platform.async_get_current_platform()
44 
45  platform.async_register_entity_service(SERVICE_SNAPSHOT, None, "snapshot")
46  platform.async_register_entity_service(SERVICE_RESTORE, None, "async_restore")
47  platform.async_register_entity_service(
48  SERVICE_JOIN, {vol.Required(ATTR_MASTER): cv.entity_id}, handle_async_join
49  )
50  platform.async_register_entity_service(SERVICE_UNJOIN, None, handle_async_unjoin)
51  platform.async_register_entity_service(
52  SERVICE_SET_LATENCY,
53  {vol.Required(ATTR_LATENCY): cv.positive_int},
54  handle_set_latency,
55  )
56 
57 
59  hass: HomeAssistant,
60  config_entry: ConfigEntry,
61  async_add_entities: AddEntitiesCallback,
62 ) -> None:
63  """Set up the snapcast config entry."""
64  snapcast_server: Snapserver = hass.data[DOMAIN][config_entry.entry_id].server
65 
67 
68  host = config_entry.data[CONF_HOST]
69  port = config_entry.data[CONF_PORT]
70  hpid = f"{host}:{port}"
71 
72  groups: list[MediaPlayerEntity] = [
73  SnapcastGroupDevice(group, hpid, config_entry.entry_id)
74  for group in snapcast_server.groups
75  ]
76  clients: list[MediaPlayerEntity] = [
77  SnapcastClientDevice(client, hpid, config_entry.entry_id)
78  for client in snapcast_server.clients
79  ]
80  async_add_entities(clients + groups)
81  hass.data[DOMAIN][
82  config_entry.entry_id
83  ].hass_async_add_entities = async_add_entities
84 
85 
86 async def handle_async_join(entity, service_call):
87  """Handle the entity service join."""
88  if not isinstance(entity, SnapcastClientDevice):
89  raise TypeError("Entity is not a client. Can only join clients.")
90  await entity.async_join(service_call.data[ATTR_MASTER])
91 
92 
93 async def handle_async_unjoin(entity, service_call):
94  """Handle the entity service unjoin."""
95  if not isinstance(entity, SnapcastClientDevice):
96  raise TypeError("Entity is not a client. Can only unjoin clients.")
97  await entity.async_unjoin()
98 
99 
100 async def handle_set_latency(entity, service_call):
101  """Handle the entity service set_latency."""
102  if not isinstance(entity, SnapcastClientDevice):
103  raise TypeError("Latency can only be set for a Snapcast client.")
104  await entity.async_set_latency(service_call.data[ATTR_LATENCY])
105 
106 
108  """Representation of a Snapcast group device."""
109 
110  _attr_should_poll = False
111  _attr_supported_features = (
112  MediaPlayerEntityFeature.VOLUME_MUTE
113  | MediaPlayerEntityFeature.VOLUME_SET
114  | MediaPlayerEntityFeature.SELECT_SOURCE
115  )
116 
117  def __init__(self, group, uid_part, entry_id):
118  """Initialize the Snapcast group device."""
119  self._attr_available_attr_available = True
120  self._group_group = group
121  self._entry_id_entry_id = entry_id
122  self._attr_unique_id_attr_unique_id = f"{GROUP_PREFIX}{uid_part}_{self._group.identifier}"
123 
124  async def async_added_to_hass(self) -> None:
125  """Subscribe to group events."""
126  self._group_group.set_callback(self.schedule_update_ha_stateschedule_update_ha_state)
127  self.hasshass.data[DOMAIN][self._entry_id_entry_id].groups.append(self)
128 
129  async def async_will_remove_from_hass(self) -> None:
130  """Disconnect group object when removed."""
131  self._group_group.set_callback(None)
132  self.hasshass.data[DOMAIN][self._entry_id_entry_id].groups.remove(self)
133 
134  def set_availability(self, available: bool) -> None:
135  """Set availability of group."""
136  self._attr_available_attr_available = available
137  self.schedule_update_ha_stateschedule_update_ha_state()
138 
139  @property
140  def state(self) -> MediaPlayerState | None:
141  """Return the state of the player."""
142  if self.is_volume_mutedis_volume_mutedis_volume_muted:
143  return MediaPlayerState.IDLE
144  return STREAM_STATUS.get(self._group_group.stream_status)
145 
146  @property
147  def identifier(self):
148  """Return the snapcast identifier."""
149  return self._group_group.identifier
150 
151  @property
152  def name(self):
153  """Return the name of the device."""
154  return f"{self._group.friendly_name} {GROUP_SUFFIX}"
155 
156  @property
157  def source(self):
158  """Return the current input source."""
159  return self._group_group.stream
160 
161  @property
162  def volume_level(self):
163  """Return the volume level."""
164  return self._group_group.volume / 100
165 
166  @property
167  def is_volume_muted(self):
168  """Volume muted."""
169  return self._group_group.muted
170 
171  @property
172  def source_list(self):
173  """List of available input sources."""
174  return list(self._group_group.streams_by_name().keys())
175 
176  async def async_select_source(self, source: str) -> None:
177  """Set input source."""
178  streams = self._group_group.streams_by_name()
179  if source in streams:
180  await self._group_group.set_stream(streams[source].identifier)
181  self.async_write_ha_stateasync_write_ha_state()
182 
183  async def async_mute_volume(self, mute: bool) -> None:
184  """Send the mute command."""
185  await self._group_group.set_muted(mute)
186  self.async_write_ha_stateasync_write_ha_state()
187 
188  async def async_set_volume_level(self, volume: float) -> None:
189  """Set the volume level."""
190  await self._group_group.set_volume(round(volume * 100))
191  self.async_write_ha_stateasync_write_ha_state()
192 
193  def snapshot(self):
194  """Snapshot the group state."""
195  self._group_group.snapshot()
196 
197  async def async_restore(self):
198  """Restore the group state."""
199  await self._group_group.restore()
200  self.async_write_ha_stateasync_write_ha_state()
201 
202 
204  """Representation of a Snapcast client device."""
205 
206  _attr_should_poll = False
207  _attr_supported_features = (
208  MediaPlayerEntityFeature.VOLUME_MUTE
209  | MediaPlayerEntityFeature.VOLUME_SET
210  | MediaPlayerEntityFeature.SELECT_SOURCE
211  )
212 
213  def __init__(self, client, uid_part, entry_id):
214  """Initialize the Snapcast client device."""
215  self._attr_available_attr_available = True
216  self._client_client = client
217  # Note: Host part is needed, when using multiple snapservers
218  self._attr_unique_id_attr_unique_id = f"{CLIENT_PREFIX}{uid_part}_{self._client.identifier}"
219  self._entry_id_entry_id = entry_id
220 
221  async def async_added_to_hass(self) -> None:
222  """Subscribe to client events."""
223  self._client_client.set_callback(self.schedule_update_ha_stateschedule_update_ha_state)
224  self.hasshass.data[DOMAIN][self._entry_id_entry_id].clients.append(self)
225 
226  async def async_will_remove_from_hass(self) -> None:
227  """Disconnect client object when removed."""
228  self._client_client.set_callback(None)
229  self.hasshass.data[DOMAIN][self._entry_id_entry_id].clients.remove(self)
230 
231  def set_availability(self, available: bool) -> None:
232  """Set availability of group."""
233  self._attr_available_attr_available = available
234  self.schedule_update_ha_stateschedule_update_ha_state()
235 
236  @property
237  def identifier(self):
238  """Return the snapcast identifier."""
239  return self._client_client.identifier
240 
241  @property
242  def name(self):
243  """Return the name of the device."""
244  return f"{self._client.friendly_name} {CLIENT_SUFFIX}"
245 
246  @property
247  def source(self):
248  """Return the current input source."""
249  return self._client_client.group.stream
250 
251  @property
252  def volume_level(self):
253  """Return the volume level."""
254  return self._client_client.volume / 100
255 
256  @property
257  def is_volume_muted(self):
258  """Volume muted."""
259  return self._client_client.muted
260 
261  @property
262  def source_list(self):
263  """List of available input sources."""
264  return list(self._client_client.group.streams_by_name().keys())
265 
266  @property
267  def state(self) -> MediaPlayerState | None:
268  """Return the state of the player."""
269  if self._client_client.connected:
270  if self.is_volume_mutedis_volume_mutedis_volume_muted or self._client_client.group.muted:
271  return MediaPlayerState.IDLE
272  return STREAM_STATUS.get(self._client_client.group.stream_status)
273  return MediaPlayerState.STANDBY
274 
275  @property
277  """Return the state attributes."""
278  state_attrs = {}
279  if self.latencylatency is not None:
280  state_attrs["latency"] = self.latencylatency
281  return state_attrs
282 
283  @property
284  def latency(self):
285  """Latency for Client."""
286  return self._client_client.latency
287 
288  async def async_select_source(self, source: str) -> None:
289  """Set input source."""
290  streams = self._client_client.group.streams_by_name()
291  if source in streams:
292  await self._client_client.group.set_stream(streams[source].identifier)
293  self.async_write_ha_stateasync_write_ha_state()
294 
295  async def async_mute_volume(self, mute: bool) -> None:
296  """Send the mute command."""
297  await self._client_client.set_muted(mute)
298  self.async_write_ha_stateasync_write_ha_state()
299 
300  async def async_set_volume_level(self, volume: float) -> None:
301  """Set the volume level."""
302  await self._client_client.set_volume(round(volume * 100))
303  self.async_write_ha_stateasync_write_ha_state()
304 
305  async def async_join(self, master):
306  """Join the group of the master player."""
307  master_entity = next(
308  entity
309  for entity in self.hasshass.data[DOMAIN][self._entry_id_entry_id].clients
310  if entity.entity_id == master
311  )
312  if not isinstance(master_entity, SnapcastClientDevice):
313  raise TypeError("Master is not a client device. Can only join clients.")
314 
315  master_group = next(
316  group
317  for group in self._client_client.groups_available()
318  if master_entity.identifier in group.clients
319  )
320  await master_group.add_client(self._client_client.identifier)
321  self.async_write_ha_stateasync_write_ha_state()
322 
323  async def async_unjoin(self):
324  """Unjoin the group the player is currently in."""
325  await self._client_client.group.remove_client(self._client_client.identifier)
326  self.async_write_ha_stateasync_write_ha_state()
327 
328  def snapshot(self):
329  """Snapshot the client state."""
330  self._client_client.snapshot()
331 
332  async def async_restore(self):
333  """Restore the client state."""
334  await self._client_client.restore()
335  self.async_write_ha_stateasync_write_ha_state()
336 
337  async def async_set_latency(self, latency):
338  """Set the latency of the client."""
339  await self._client_client.set_latency(latency)
340  self.async_write_ha_stateasync_write_ha_state()
None schedule_update_ha_state(self, bool force_refresh=False)
Definition: entity.py:1244
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: media_player.py:62
def handle_async_join(entity, service_call)
Definition: media_player.py:86
def handle_async_unjoin(entity, service_call)
Definition: media_player.py:93
def handle_set_latency(entity, service_call)