Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add KEF speakers integration #28959

Merged
merged 14 commits into from Jan 3, 2020
1 change: 1 addition & 0 deletions .coveragerc
Expand Up @@ -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/*
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/kef/__init__.py
@@ -0,0 +1 @@
"""The KEF Wireless Speakers component."""
8 changes: 8 additions & 0 deletions homeassistant/components/kef/manifest.json
@@ -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 changes: 273 additions & 0 deletions homeassistant/components/kef/media_player.py
@@ -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]),
basnijholt marked this conversation as resolved.
Show resolved Hide resolved
}
)


async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
basnijholt marked this conversation as resolved.
Show resolved Hide resolved
"""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}.")
4 changes: 4 additions & 0 deletions requirements_all.txt
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions requirements_test_all.txt
Expand Up @@ -197,6 +197,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

Expand Down