Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add KEF speakers integration (#28959)
* add KEF speakers platform for the integration This will work with the KEF LS50 Wireless and KEF LSX speakers. The development of this code happened on https://github.com/basnijholt/media_player.kef * rename DATA_KEF -> DOMAIN * use aiokef v0.2.0 and support LSX and new features * sort imports * fix @MartinHjelmare's suggestions * remove _CONFIGURING * change STATE_UNKNOWN to None * use lat and long for unique_id * bump aiokef to v0.2.2 * use config[ATTR] instead of config.get(ATTR) * use getmac * fix case when MAC is None * use host as instance lifetime id * fix requirements
- Loading branch information
1 parent
fa4fa30
commit 0d5486f
Showing
7 changed files
with
289 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
"""The KEF Wireless Speakers component.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"domain": "kef", | ||
"name": "KEF", | ||
"documentation": "https://www.home-assistant.io/integrations/kef", | ||
"dependencies": [], | ||
"codeowners": ["@basnijholt"], | ||
"requirements": ["aiokef==0.2.2", "getmac==0.8.1"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,273 @@ | ||
"""Platform for the KEF Wireless Speakers.""" | ||
|
||
from datetime import timedelta | ||
from functools import partial | ||
import ipaddress | ||
import logging | ||
|
||
from aiokef.aiokef import AsyncKefSpeaker | ||
from getmac import get_mac_address | ||
import voluptuous as vol | ||
|
||
from homeassistant.components.media_player import ( | ||
PLATFORM_SCHEMA, | ||
SUPPORT_SELECT_SOURCE, | ||
SUPPORT_TURN_OFF, | ||
SUPPORT_TURN_ON, | ||
SUPPORT_VOLUME_MUTE, | ||
SUPPORT_VOLUME_SET, | ||
SUPPORT_VOLUME_STEP, | ||
MediaPlayerDevice, | ||
) | ||
from homeassistant.const import ( | ||
CONF_HOST, | ||
CONF_NAME, | ||
CONF_PORT, | ||
CONF_TYPE, | ||
STATE_OFF, | ||
STATE_ON, | ||
) | ||
from homeassistant.helpers import config_validation as cv | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
DEFAULT_NAME = "KEF" | ||
DEFAULT_PORT = 50001 | ||
DEFAULT_MAX_VOLUME = 0.5 | ||
DEFAULT_VOLUME_STEP = 0.05 | ||
DEFAULT_INVERSE_SPEAKER_MODE = False | ||
|
||
DOMAIN = "kef" | ||
|
||
SCAN_INTERVAL = timedelta(seconds=30) | ||
PARALLEL_UPDATES = 0 | ||
|
||
SOURCES = {"LSX": ["Wifi", "Bluetooth", "Aux", "Opt"]} | ||
SOURCES["LS50"] = SOURCES["LSX"] + ["Usb"] | ||
|
||
SUPPORT_KEF = ( | ||
SUPPORT_VOLUME_SET | ||
| SUPPORT_VOLUME_STEP | ||
| SUPPORT_VOLUME_MUTE | ||
| SUPPORT_SELECT_SOURCE | ||
| SUPPORT_TURN_OFF | ||
| SUPPORT_TURN_ON | ||
) | ||
|
||
CONF_MAX_VOLUME = "maximum_volume" | ||
CONF_VOLUME_STEP = "volume_step" | ||
CONF_INVERSE_SPEAKER_MODE = "inverse_speaker_mode" | ||
CONF_STANDBY_TIME = "standby_time" | ||
|
||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( | ||
{ | ||
vol.Required(CONF_HOST): cv.string, | ||
vol.Required(CONF_TYPE): vol.In(["LS50", "LSX"]), | ||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, | ||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, | ||
vol.Optional(CONF_MAX_VOLUME, default=DEFAULT_MAX_VOLUME): cv.small_float, | ||
vol.Optional(CONF_VOLUME_STEP, default=DEFAULT_VOLUME_STEP): cv.small_float, | ||
vol.Optional( | ||
CONF_INVERSE_SPEAKER_MODE, default=DEFAULT_INVERSE_SPEAKER_MODE | ||
): cv.boolean, | ||
vol.Optional(CONF_STANDBY_TIME): vol.In([20, 60]), | ||
} | ||
) | ||
|
||
|
||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): | ||
"""Set up the KEF platform.""" | ||
if DOMAIN not in hass.data: | ||
hass.data[DOMAIN] = {} | ||
|
||
host = config[CONF_HOST] | ||
speaker_type = config[CONF_TYPE] | ||
port = config[CONF_PORT] | ||
name = config[CONF_NAME] | ||
maximum_volume = config[CONF_MAX_VOLUME] | ||
volume_step = config[CONF_VOLUME_STEP] | ||
inverse_speaker_mode = config[CONF_INVERSE_SPEAKER_MODE] | ||
standby_time = config.get(CONF_STANDBY_TIME) | ||
|
||
sources = SOURCES[speaker_type] | ||
|
||
_LOGGER.debug( | ||
"Setting up %s with host: %s, port: %s, name: %s, sources: %s", | ||
DOMAIN, | ||
host, | ||
port, | ||
name, | ||
sources, | ||
) | ||
|
||
try: | ||
if ipaddress.ip_address(host).version == 6: | ||
mode = "ip6" | ||
else: | ||
mode = "ip" | ||
except ValueError: | ||
mode = "hostname" | ||
mac = await hass.async_add_executor_job(partial(get_mac_address, **{mode: host})) | ||
unique_id = f"kef-{mac}" if mac is not None else None | ||
|
||
media_player = KefMediaPlayer( | ||
name, | ||
host, | ||
port, | ||
maximum_volume, | ||
volume_step, | ||
standby_time, | ||
inverse_speaker_mode, | ||
sources, | ||
ioloop=hass.loop, | ||
unique_id=unique_id, | ||
) | ||
|
||
if host in hass.data[DOMAIN]: | ||
_LOGGER.debug("%s is already configured", host) | ||
else: | ||
hass.data[DOMAIN][host] = media_player | ||
async_add_entities([media_player], update_before_add=True) | ||
|
||
|
||
class KefMediaPlayer(MediaPlayerDevice): | ||
"""Kef Player Object.""" | ||
|
||
def __init__( | ||
self, | ||
name, | ||
host, | ||
port, | ||
maximum_volume, | ||
volume_step, | ||
standby_time, | ||
inverse_speaker_mode, | ||
sources, | ||
ioloop, | ||
unique_id, | ||
): | ||
"""Initialize the media player.""" | ||
self._name = name | ||
self._sources = sources | ||
self._speaker = AsyncKefSpeaker( | ||
host, | ||
port, | ||
volume_step, | ||
maximum_volume, | ||
standby_time, | ||
inverse_speaker_mode, | ||
ioloop=ioloop, | ||
) | ||
self._unique_id = unique_id | ||
|
||
self._state = None | ||
self._muted = None | ||
self._source = None | ||
self._volume = None | ||
self._is_online = None | ||
|
||
@property | ||
def name(self): | ||
"""Return the name of the device.""" | ||
return self._name | ||
|
||
@property | ||
def state(self): | ||
"""Return the state of the device.""" | ||
return self._state | ||
|
||
async def async_update(self): | ||
"""Update latest state.""" | ||
_LOGGER.debug("Running async_update") | ||
try: | ||
self._is_online = await self._speaker.is_online() | ||
if self._is_online: | ||
( | ||
self._volume, | ||
self._muted, | ||
) = await self._speaker.get_volume_and_is_muted() | ||
state = await self._speaker.get_state() | ||
self._source = state.source | ||
self._state = STATE_ON if state.is_on else STATE_OFF | ||
else: | ||
self._muted = None | ||
self._source = None | ||
self._volume = None | ||
self._state = STATE_OFF | ||
except (ConnectionRefusedError, ConnectionError, TimeoutError) as err: | ||
_LOGGER.debug("Error in `update`: %s", err) | ||
self._state = None | ||
|
||
@property | ||
def volume_level(self): | ||
"""Volume level of the media player (0..1).""" | ||
return self._volume | ||
|
||
@property | ||
def is_volume_muted(self): | ||
"""Boolean if volume is currently muted.""" | ||
return self._muted | ||
|
||
@property | ||
def supported_features(self): | ||
"""Flag media player features that are supported.""" | ||
return SUPPORT_KEF | ||
|
||
@property | ||
def source(self): | ||
"""Name of the current input source.""" | ||
return self._source | ||
|
||
@property | ||
def source_list(self): | ||
"""List of available input sources.""" | ||
return self._sources | ||
|
||
@property | ||
def available(self): | ||
"""Return if the speaker is reachable online.""" | ||
return self._is_online | ||
|
||
@property | ||
def unique_id(self): | ||
"""Return the device unique id.""" | ||
return self._unique_id | ||
|
||
@property | ||
def icon(self): | ||
"""Return the device's icon.""" | ||
return "mdi:speaker-wireless" | ||
|
||
async def async_turn_off(self): | ||
"""Turn the media player off.""" | ||
await self._speaker.turn_off() | ||
|
||
async def async_turn_on(self): | ||
"""Turn the media player on.""" | ||
await self._speaker.turn_on() | ||
|
||
async def async_volume_up(self): | ||
"""Volume up the media player.""" | ||
await self._speaker.increase_volume() | ||
|
||
async def async_volume_down(self): | ||
"""Volume down the media player.""" | ||
await self._speaker.decrease_volume() | ||
|
||
async def async_set_volume_level(self, volume): | ||
"""Set volume level, range 0..1.""" | ||
await self._speaker.set_volume(volume) | ||
|
||
async def async_mute_volume(self, mute): | ||
"""Mute (True) or unmute (False) media player.""" | ||
if mute: | ||
await self._speaker.mute() | ||
else: | ||
await self._speaker.unmute() | ||
|
||
async def async_select_source(self, source: str): | ||
"""Select input source.""" | ||
if source in self.source_list: | ||
await self._speaker.set_source(source) | ||
else: | ||
raise ValueError(f"Unknown input source: {source}.") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters