Home Assistant Unofficial Reference 2024.12.1
media_player.py
Go to the documentation of this file.
1 """Platform for the KEF Wireless Speakers."""
2 
3 from __future__ import annotations
4 
5 from datetime import timedelta
6 from functools import partial
7 import ipaddress
8 import logging
9 
10 from aiokef import AsyncKefSpeaker
11 from aiokef.aiokef import DSP_OPTION_MAPPING
12 from getmac import get_mac_address
13 import voluptuous as vol
14 
16  PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA,
17  MediaPlayerEntity,
18  MediaPlayerEntityFeature,
19  MediaPlayerState,
20 )
21 from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE
22 from homeassistant.core import HomeAssistant
23 from homeassistant.exceptions import PlatformNotReady
24 from homeassistant.helpers import config_validation as cv, entity_platform
25 from homeassistant.helpers.entity_platform import AddEntitiesCallback
26 from homeassistant.helpers.event import async_track_time_interval
27 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
28 
29 _LOGGER = logging.getLogger(__name__)
30 
31 DEFAULT_NAME = "KEF"
32 DEFAULT_PORT = 50001
33 DEFAULT_MAX_VOLUME = 0.5
34 DEFAULT_VOLUME_STEP = 0.05
35 DEFAULT_INVERSE_SPEAKER_MODE = False
36 DEFAULT_SUPPORTS_ON = True
37 
38 DOMAIN = "kef"
39 
40 SCAN_INTERVAL = timedelta(seconds=30)
41 
42 SOURCES = {"LSX": ["Wifi", "Bluetooth", "Aux", "Opt"]}
43 SOURCES["LS50"] = SOURCES["LSX"] + ["Usb"]
44 
45 CONF_MAX_VOLUME = "maximum_volume"
46 CONF_VOLUME_STEP = "volume_step"
47 CONF_INVERSE_SPEAKER_MODE = "inverse_speaker_mode"
48 CONF_SUPPORTS_ON = "supports_on"
49 CONF_STANDBY_TIME = "standby_time"
50 
51 SERVICE_MODE = "set_mode"
52 SERVICE_DESK_DB = "set_desk_db"
53 SERVICE_WALL_DB = "set_wall_db"
54 SERVICE_TREBLE_DB = "set_treble_db"
55 SERVICE_HIGH_HZ = "set_high_hz"
56 SERVICE_LOW_HZ = "set_low_hz"
57 SERVICE_SUB_DB = "set_sub_db"
58 SERVICE_UPDATE_DSP = "update_dsp"
59 
60 DSP_SCAN_INTERVAL = timedelta(seconds=3600)
61 
62 PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
63  {
64  vol.Required(CONF_HOST): cv.string,
65  vol.Required(CONF_TYPE): vol.In(["LS50", "LSX"]),
66  vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
67  vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
68  vol.Optional(CONF_MAX_VOLUME, default=DEFAULT_MAX_VOLUME): cv.small_float,
69  vol.Optional(CONF_VOLUME_STEP, default=DEFAULT_VOLUME_STEP): cv.small_float,
70  vol.Optional(
71  CONF_INVERSE_SPEAKER_MODE, default=DEFAULT_INVERSE_SPEAKER_MODE
72  ): cv.boolean,
73  vol.Optional(CONF_SUPPORTS_ON, default=DEFAULT_SUPPORTS_ON): cv.boolean,
74  vol.Optional(CONF_STANDBY_TIME): vol.In([20, 60]),
75  }
76 )
77 
78 
79 def get_ip_mode(host):
80  """Get the 'mode' used to retrieve the MAC address."""
81  try:
82  ip_address = ipaddress.ip_address(host)
83  except ValueError:
84  return "hostname"
85 
86  if ip_address.version == 6:
87  return "ip6"
88  return "ip"
89 
90 
92  hass: HomeAssistant,
93  config: ConfigType,
94  async_add_entities: AddEntitiesCallback,
95  discovery_info: DiscoveryInfoType | None = None,
96 ) -> None:
97  """Set up the KEF platform."""
98  if DOMAIN not in hass.data:
99  hass.data[DOMAIN] = {}
100 
101  host = config[CONF_HOST]
102  speaker_type = config[CONF_TYPE]
103  port = config[CONF_PORT]
104  name = config[CONF_NAME]
105  maximum_volume = config[CONF_MAX_VOLUME]
106  volume_step = config[CONF_VOLUME_STEP]
107  inverse_speaker_mode = config[CONF_INVERSE_SPEAKER_MODE]
108  supports_on = config[CONF_SUPPORTS_ON]
109  standby_time = config.get(CONF_STANDBY_TIME)
110 
111  sources = SOURCES[speaker_type]
112 
113  _LOGGER.debug(
114  "Setting up %s with host: %s, port: %s, name: %s, sources: %s",
115  DOMAIN,
116  host,
117  port,
118  name,
119  sources,
120  )
121 
122  mode = get_ip_mode(host)
123  mac = await hass.async_add_executor_job(partial(get_mac_address, **{mode: host}))
124  if mac is None or mac == "00:00:00:00:00:00":
125  raise PlatformNotReady("Cannot get the ip address of kef speaker.")
126 
127  unique_id = f"kef-{mac}"
128 
129  media_player = KefMediaPlayer(
130  name,
131  host,
132  port,
133  maximum_volume,
134  volume_step,
135  standby_time,
136  inverse_speaker_mode,
137  supports_on,
138  sources,
139  speaker_type,
140  loop=hass.loop,
141  unique_id=unique_id,
142  )
143 
144  if host in hass.data[DOMAIN]:
145  _LOGGER.debug("%s is already configured", host)
146  else:
147  hass.data[DOMAIN][host] = media_player
148  async_add_entities([media_player], update_before_add=True)
149 
150  platform = entity_platform.async_get_current_platform()
151 
152  platform.async_register_entity_service(
153  SERVICE_MODE,
154  {
155  vol.Optional("desk_mode"): cv.boolean,
156  vol.Optional("wall_mode"): cv.boolean,
157  vol.Optional("phase_correction"): cv.boolean,
158  vol.Optional("high_pass"): cv.boolean,
159  vol.Optional("sub_polarity"): vol.In(["-", "+"]),
160  vol.Optional("bass_extension"): vol.In(["Less", "Standard", "Extra"]),
161  },
162  "set_mode",
163  )
164  platform.async_register_entity_service(SERVICE_UPDATE_DSP, None, "update_dsp")
165 
166  def add_service(name, which, option):
167  options = DSP_OPTION_MAPPING[which]
168  dtype = type(options[0]) # int or float
169  platform.async_register_entity_service(
170  name,
171  {
172  vol.Required(option): vol.All(
173  vol.Coerce(float), vol.Coerce(dtype), vol.In(options)
174  )
175  },
176  f"set_{which}",
177  )
178 
179  add_service(SERVICE_DESK_DB, "desk_db", "db_value")
180  add_service(SERVICE_WALL_DB, "wall_db", "db_value")
181  add_service(SERVICE_TREBLE_DB, "treble_db", "db_value")
182  add_service(SERVICE_HIGH_HZ, "high_hz", "hz_value")
183  add_service(SERVICE_LOW_HZ, "low_hz", "hz_value")
184  add_service(SERVICE_SUB_DB, "sub_db", "db_value")
185 
186 
188  """Kef Player Object."""
189 
190  _attr_icon = "mdi:speaker-wireless"
191 
192  def __init__(
193  self,
194  name,
195  host,
196  port,
197  maximum_volume,
198  volume_step,
199  standby_time,
200  inverse_speaker_mode,
201  supports_on,
202  sources,
203  speaker_type,
204  loop,
205  unique_id,
206  ):
207  """Initialize the media player."""
208  self._attr_name_attr_name = name
209  self._attr_source_list_attr_source_list = sources
210  self._speaker_speaker = AsyncKefSpeaker(
211  host,
212  port,
213  volume_step,
214  maximum_volume,
215  standby_time,
216  inverse_speaker_mode,
217  loop=loop,
218  )
219  self._attr_unique_id_attr_unique_id = unique_id
220  self._supports_on_supports_on = supports_on
221  self._speaker_type_speaker_type = speaker_type
222 
223  self._attr_available_attr_available = False
224  self._dsp_dsp = None
225  self._update_dsp_task_remover_update_dsp_task_remover = None
226 
227  self._attr_supported_features_attr_supported_features = (
228  MediaPlayerEntityFeature.VOLUME_SET
229  | MediaPlayerEntityFeature.VOLUME_STEP
230  | MediaPlayerEntityFeature.VOLUME_MUTE
231  | MediaPlayerEntityFeature.SELECT_SOURCE
232  | MediaPlayerEntityFeature.TURN_OFF
233  | MediaPlayerEntityFeature.NEXT_TRACK # only in Bluetooth and Wifi
234  | MediaPlayerEntityFeature.PAUSE # only in Bluetooth and Wifi
235  | MediaPlayerEntityFeature.PLAY # only in Bluetooth and Wifi
236  | MediaPlayerEntityFeature.PREVIOUS_TRACK # only in Bluetooth and Wifi
237  )
238  if supports_on:
239  self._attr_supported_features_attr_supported_features |= MediaPlayerEntityFeature.TURN_ON
240 
241  async def async_update(self) -> None:
242  """Update latest state."""
243  _LOGGER.debug("Running async_update")
244  try:
245  self._attr_available_attr_available = await self._speaker_speaker.is_online()
246  if self.availableavailable:
247  (
248  self._attr_volume_level_attr_volume_level,
249  self._attr_is_volume_muted_attr_is_volume_muted,
250  ) = await self._speaker_speaker.get_volume_and_is_muted()
251  state = await self._speaker_speaker.get_state()
252  self._attr_source_attr_source = state.source
253  self._attr_state_attr_state = (
254  MediaPlayerState.ON if state.is_on else MediaPlayerState.OFF
255  )
256  if self._dsp_dsp is None:
257  # Only do this when necessary because it is a slow operation
258  await self.update_dspupdate_dsp()
259  else:
260  self._attr_is_volume_muted_attr_is_volume_muted = None
261  self._attr_source_attr_source = None
262  self._attr_volume_level_attr_volume_level = None
263  self._attr_state_attr_state = MediaPlayerState.OFF
264  except (ConnectionError, TimeoutError) as err:
265  _LOGGER.debug("Error in `update`: %s", err)
266  self._attr_state_attr_state = None
267 
268  async def async_turn_off(self) -> None:
269  """Turn the media player off."""
270  await self._speaker_speaker.turn_off()
271 
272  async def async_turn_on(self) -> None:
273  """Turn the media player on."""
274  if not self._supports_on_supports_on:
275  raise NotImplementedError
276  await self._speaker_speaker.turn_on()
277 
278  async def async_volume_up(self) -> None:
279  """Volume up the media player."""
280  await self._speaker_speaker.increase_volume()
281 
282  async def async_volume_down(self) -> None:
283  """Volume down the media player."""
284  await self._speaker_speaker.decrease_volume()
285 
286  async def async_set_volume_level(self, volume: float) -> None:
287  """Set volume level, range 0..1."""
288  await self._speaker_speaker.set_volume(volume)
289 
290  async def async_mute_volume(self, mute: bool) -> None:
291  """Mute (True) or unmute (False) media player."""
292  if mute:
293  await self._speaker_speaker.mute()
294  else:
295  await self._speaker_speaker.unmute()
296 
297  async def async_select_source(self, source: str) -> None:
298  """Select input source."""
299  if self.source_listsource_list is not None and source in self.source_listsource_list:
300  await self._speaker_speaker.set_source(source)
301  else:
302  raise ValueError(f"Unknown input source: {source}.")
303 
304  async def async_media_play(self) -> None:
305  """Send play command."""
306  await self._speaker_speaker.set_play_pause()
307 
308  async def async_media_pause(self) -> None:
309  """Send pause command."""
310  await self._speaker_speaker.set_play_pause()
311 
312  async def async_media_previous_track(self) -> None:
313  """Send previous track command."""
314  await self._speaker_speaker.prev_track()
315 
316  async def async_media_next_track(self) -> None:
317  """Send next track command."""
318  await self._speaker_speaker.next_track()
319 
320  async def update_dsp(self, _=None) -> None:
321  """Update the DSP settings."""
322  if self._speaker_type_speaker_type == "LS50" and self.statestatestatestatestate == MediaPlayerState.OFF:
323  # The LSX is able to respond when off the LS50 has to be on.
324  return
325 
326  mode = await self._speaker_speaker.get_mode()
327  self._dsp_dsp = {
328  "desk_db": await self._speaker_speaker.get_desk_db(),
329  "wall_db": await self._speaker_speaker.get_wall_db(),
330  "treble_db": await self._speaker_speaker.get_treble_db(),
331  "high_hz": await self._speaker_speaker.get_high_hz(),
332  "low_hz": await self._speaker_speaker.get_low_hz(),
333  "sub_db": await self._speaker_speaker.get_sub_db(),
334  **mode._asdict(),
335  }
336 
337  async def async_added_to_hass(self) -> None:
338  """Subscribe to DSP updates."""
339  self._update_dsp_task_remover_update_dsp_task_remover = async_track_time_interval(
340  self.hasshass, self.update_dspupdate_dsp, DSP_SCAN_INTERVAL
341  )
342 
343  async def async_will_remove_from_hass(self) -> None:
344  """Unsubscribe to DSP updates."""
345  self._update_dsp_task_remover_update_dsp_task_remover()
346  self._update_dsp_task_remover_update_dsp_task_remover = None
347 
348  @property
350  """Return the DSP settings of the KEF device."""
351  return self._dsp_dsp or {}
352 
353  async def set_mode(
354  self,
355  desk_mode=None,
356  wall_mode=None,
357  phase_correction=None,
358  high_pass=None,
359  sub_polarity=None,
360  bass_extension=None,
361  ):
362  """Set the speaker mode."""
363  await self._speaker_speaker.set_mode(
364  desk_mode=desk_mode,
365  wall_mode=wall_mode,
366  phase_correction=phase_correction,
367  high_pass=high_pass,
368  sub_polarity=sub_polarity,
369  bass_extension=bass_extension,
370  )
371  self._dsp_dsp = None
372 
373  async def set_desk_db(self, db_value):
374  """Set desk_db of the KEF speakers."""
375  await self._speaker_speaker.set_desk_db(db_value)
376  self._dsp_dsp = None
377 
378  async def set_wall_db(self, db_value):
379  """Set wall_db of the KEF speakers."""
380  await self._speaker_speaker.set_wall_db(db_value)
381  self._dsp_dsp = None
382 
383  async def set_treble_db(self, db_value):
384  """Set treble_db of the KEF speakers."""
385  await self._speaker_speaker.set_treble_db(db_value)
386  self._dsp_dsp = None
387 
388  async def set_high_hz(self, hz_value):
389  """Set high_hz of the KEF speakers."""
390  await self._speaker_speaker.set_high_hz(hz_value)
391  self._dsp_dsp = None
392 
393  async def set_low_hz(self, hz_value):
394  """Set low_hz of the KEF speakers."""
395  await self._speaker_speaker.set_low_hz(hz_value)
396  self._dsp_dsp = None
397 
398  async def set_sub_db(self, db_value):
399  """Set sub_db of the KEF speakers."""
400  await self._speaker_speaker.set_sub_db(db_value)
401  self._dsp_dsp = None
def set_mode(self, desk_mode=None, wall_mode=None, phase_correction=None, high_pass=None, sub_polarity=None, bass_extension=None)
def __init__(self, name, host, port, maximum_volume, volume_step, standby_time, inverse_speaker_mode, supports_on, sources, speaker_type, loop, unique_id)
str|float get_state(dict[str, float] data, str key)
Definition: sensor.py:26
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: media_player.py:96
CALLBACK_TYPE async_track_time_interval(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, timedelta interval, *str|None name=None, bool|None cancel_on_shutdown=None)
Definition: event.py:1679