mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
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
This commit is contained in:
parent
fa4fa30461
commit
0d5486f772
@ -352,6 +352,7 @@ omit =
|
||||
homeassistant/components/kankun/switch.py
|
||||
homeassistant/components/keba/*
|
||||
homeassistant/components/keenetic_ndms2/device_tracker.py
|
||||
homeassistant/components/kef/*
|
||||
homeassistant/components/keyboard/*
|
||||
homeassistant/components/keyboard_remote/*
|
||||
homeassistant/components/kira/*
|
||||
|
@ -176,6 +176,7 @@ homeassistant/components/juicenet/* @jesserockz
|
||||
homeassistant/components/kaiterra/* @Michsior14
|
||||
homeassistant/components/keba/* @dannerph
|
||||
homeassistant/components/keenetic_ndms2/* @foxel
|
||||
homeassistant/components/kef/* @basnijholt
|
||||
homeassistant/components/keyboard_remote/* @bendavid
|
||||
homeassistant/components/knx/* @Julius2342
|
||||
homeassistant/components/kodi/* @armills
|
||||
|
1
homeassistant/components/kef/__init__.py
Normal file
1
homeassistant/components/kef/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""The KEF Wireless Speakers component."""
|
8
homeassistant/components/kef/manifest.json
Normal file
8
homeassistant/components/kef/manifest.json
Normal file
@ -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"]
|
||||
}
|
273
homeassistant/components/kef/media_player.py
Normal file
273
homeassistant/components/kef/media_player.py
Normal file
@ -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}.")
|
@ -171,6 +171,9 @@ aioimaplib==0.7.15
|
||||
# homeassistant.components.apache_kafka
|
||||
aiokafka==0.5.1
|
||||
|
||||
# homeassistant.components.kef
|
||||
aiokef==0.2.2
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx==0.6.7
|
||||
|
||||
@ -578,6 +581,7 @@ georss_qld_bushfire_alert_client==0.3
|
||||
|
||||
# homeassistant.components.braviatv
|
||||
# homeassistant.components.huawei_lte
|
||||
# homeassistant.components.kef
|
||||
# homeassistant.components.nmap_tracker
|
||||
getmac==0.8.1
|
||||
|
||||
|
@ -200,6 +200,7 @@ georss_qld_bushfire_alert_client==0.3
|
||||
|
||||
# homeassistant.components.braviatv
|
||||
# homeassistant.components.huawei_lte
|
||||
# homeassistant.components.kef
|
||||
# homeassistant.components.nmap_tracker
|
||||
getmac==0.8.1
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user