From 1d77ce8342c967f373a30905b9879a7b3a541dce Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 22 Nov 2019 19:39:13 +0100 Subject: [PATCH 01/14] 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 --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/kef/__init__.py | 1 + homeassistant/components/kef/manifest.json | 8 + homeassistant/components/kef/media_player.py | 233 +++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 247 insertions(+) create mode 100644 homeassistant/components/kef/__init__.py create mode 100644 homeassistant/components/kef/manifest.json create mode 100644 homeassistant/components/kef/media_player.py diff --git a/.coveragerc b/.coveragerc index e96895429a6c..a7ce4d67c524 100644 --- a/.coveragerc +++ b/.coveragerc @@ -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/* diff --git a/CODEOWNERS b/CODEOWNERS index 23005cb5273b..2546433c4a27 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/homeassistant/components/kef/__init__.py b/homeassistant/components/kef/__init__.py new file mode 100644 index 000000000000..a55c8ca32109 --- /dev/null +++ b/homeassistant/components/kef/__init__.py @@ -0,0 +1 @@ +"""The KEF Wireless Speakers component.""" diff --git a/homeassistant/components/kef/manifest.json b/homeassistant/components/kef/manifest.json new file mode 100644 index 000000000000..ff7caf7d656b --- /dev/null +++ b/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.1.8"] +} diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py new file mode 100644 index 000000000000..f4f79b4df4ee --- /dev/null +++ b/homeassistant/components/kef/media_player.py @@ -0,0 +1,233 @@ +"""Platform for the KEF Wireless Speakers.""" + +import datetime +import logging + +from aiokef.aiokef import INPUT_SOURCES, AsyncKefSpeaker +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, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, +) +from homeassistant.helpers import config_validation as cv + +_CONFIGURING = {} +_LOGGER = logging.getLogger(__name__) + + +DEFAULT_NAME = "KEF" +DEFAULT_PORT = 50001 +DEFAULT_MAX_VOLUME = 0.5 +DEFAULT_VOLUME_STEP = 0.05 +DATA_KEF = "kef" + +SCAN_INTERVAL = datetime.timedelta(seconds=30) +PARALLEL_UPDATES = 0 + +KEF_LS50_SOURCES = sorted(INPUT_SOURCES.keys()) + +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" +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + 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, + } +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the KEF platform.""" + if DATA_KEF not in hass.data: + hass.data[DATA_KEF] = {} + + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + name = config.get(CONF_NAME) + maximum_volume = config.get(CONF_MAX_VOLUME) + volume_step = config.get(CONF_VOLUME_STEP) + + _LOGGER.debug( + "Setting up %s with host: %s, port: %s, name: %s, sources: %s", + DATA_KEF, + host, + port, + name, + KEF_LS50_SOURCES, + ) + + media_player = KefMediaPlayer( + name, + host, + port, + maximum_volume=maximum_volume, + volume_step=volume_step, + sources=KEF_LS50_SOURCES, + ioloop=hass.loop, + ) + unique_id = media_player.unique_id + if unique_id in hass.data[DATA_KEF]: + _LOGGER.debug("%s is already configured.", unique_id) + else: + hass.data[DATA_KEF][unique_id] = 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, sources, ioloop): + """Initialize the media player.""" + self._name = name + self._sources = sources + self._speaker = AsyncKefSpeaker( + host, port, volume_step, maximum_volume, ioloop=ioloop + ) + + self._state = STATE_UNKNOWN + 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() + self._source, is_on = await self._speaker.get_source_and_state() + self._state = STATE_ON if 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 = STATE_UNKNOWN + + @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 f"{self._speaker.host}:{self._speaker.port}" + + @property + def icon(self): + """Return the device's icon.""" + return "mdi:speaker-wireless" + + @property + def should_poll(self): + """It's possible that the speaker is controlled manually.""" + return True + + @property + def force_update(self): + """Force update.""" + return False + + 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}.") diff --git a/requirements_all.txt b/requirements_all.txt index d2ed4e0e1e26..2e9c783b20c6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,6 +171,9 @@ aioimaplib==0.7.15 # homeassistant.components.apache_kafka aiokafka==0.5.1 +# homeassistant.components.kef +aiokef==0.1.8 + # homeassistant.components.lifx aiolifx==0.6.7 From 88f865b97ec7b67ec082f17dd8898226d271af28 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 26 Nov 2019 11:46:59 +0100 Subject: [PATCH 02/14] rename DATA_KEF -> DOMAIN --- homeassistant/components/kef/media_player.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index f4f79b4df4ee..dac3d3a0c596 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -34,7 +34,7 @@ DEFAULT_PORT = 50001 DEFAULT_MAX_VOLUME = 0.5 DEFAULT_VOLUME_STEP = 0.05 -DATA_KEF = "kef" +DOMAIN = "kef" SCAN_INTERVAL = datetime.timedelta(seconds=30) PARALLEL_UPDATES = 0 @@ -65,8 +65,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the KEF platform.""" - if DATA_KEF not in hass.data: - hass.data[DATA_KEF] = {} + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} host = config.get(CONF_HOST) port = config.get(CONF_PORT) @@ -76,7 +76,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= _LOGGER.debug( "Setting up %s with host: %s, port: %s, name: %s, sources: %s", - DATA_KEF, + DOMAIN, host, port, name, @@ -93,10 +93,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ioloop=hass.loop, ) unique_id = media_player.unique_id - if unique_id in hass.data[DATA_KEF]: + if unique_id in hass.data[DOMAIN]: _LOGGER.debug("%s is already configured.", unique_id) else: - hass.data[DATA_KEF][unique_id] = media_player + hass.data[DOMAIN][unique_id] = media_player async_add_entities([media_player], update_before_add=True) From ea3ff4f2947d10dd4be80d6e8d2104b455476063 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 1 Dec 2019 01:09:45 +0100 Subject: [PATCH 03/14] use aiokef v0.2.0 and support LSX and new features --- homeassistant/components/kef/manifest.json | 2 +- homeassistant/components/kef/media_player.py | 60 ++++++++++++++++---- requirements_all.txt | 2 +- 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/kef/manifest.json b/homeassistant/components/kef/manifest.json index ff7caf7d656b..4d9e33715eaf 100644 --- a/homeassistant/components/kef/manifest.json +++ b/homeassistant/components/kef/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/kef", "dependencies": [], "codeowners": ["@basnijholt"], - "requirements": ["aiokef==0.1.8"] + "requirements": ["aiokef==0.2.0"] } diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index dac3d3a0c596..2d2f7d98a56c 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -3,9 +3,8 @@ import datetime import logging -from aiokef.aiokef import INPUT_SOURCES, AsyncKefSpeaker import voluptuous as vol - +from aiokef.aiokef import AsyncKefSpeaker from homeassistant.components.media_player import ( PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, @@ -20,6 +19,7 @@ CONF_HOST, CONF_NAME, CONF_PORT, + CONF_TYPE, STATE_OFF, STATE_ON, STATE_UNKNOWN, @@ -34,12 +34,15 @@ DEFAULT_PORT = 50001 DEFAULT_MAX_VOLUME = 0.5 DEFAULT_VOLUME_STEP = 0.05 +DEFAULT_INVERSE_SPEAKER_MODE = False + DOMAIN = "kef" SCAN_INTERVAL = datetime.timedelta(seconds=30) PARALLEL_UPDATES = 0 -KEF_LS50_SOURCES = sorted(INPUT_SOURCES.keys()) +SOURCES = {"LSX": ["Wifi", "Bluetooth", "Aux", "Opt"]} +SOURCES["LS50"] = SOURCES["LSX"] + ["Usb"] SUPPORT_KEF = ( SUPPORT_VOLUME_SET @@ -52,13 +55,21 @@ 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]), } ) @@ -68,11 +79,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if DOMAIN not in hass.data: hass.data[DOMAIN] = {} - host = config.get(CONF_HOST) + host = config[CONF_HOST] + speaker_type = config[CONF_TYPE] port = config.get(CONF_PORT) name = config.get(CONF_NAME) maximum_volume = config.get(CONF_MAX_VOLUME) volume_step = config.get(CONF_VOLUME_STEP) + inverse_speaker_mode = config.get(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", @@ -80,16 +96,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= host, port, name, - KEF_LS50_SOURCES, + sources, ) media_player = KefMediaPlayer( name, host, port, - maximum_volume=maximum_volume, - volume_step=volume_step, - sources=KEF_LS50_SOURCES, + maximum_volume, + volume_step, + standby_time, + inverse_speaker_mode, + sources, ioloop=hass.loop, ) unique_id = media_player.unique_id @@ -103,12 +121,29 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class KefMediaPlayer(MediaPlayerDevice): """Kef Player Object.""" - def __init__(self, name, host, port, maximum_volume, volume_step, sources, ioloop): + def __init__( + self, + name, + host, + port, + maximum_volume, + volume_step, + standby_time, + inverse_speaker_mode, + sources, + ioloop, + ): """Initialize the media player.""" self._name = name self._sources = sources self._speaker = AsyncKefSpeaker( - host, port, volume_step, maximum_volume, ioloop=ioloop + host, + port, + volume_step, + maximum_volume, + standby_time, + inverse_speaker_mode, + ioloop=ioloop, ) self._state = STATE_UNKNOWN @@ -137,8 +172,9 @@ async def async_update(self): self._volume, self._muted, ) = await self._speaker.get_volume_and_is_muted() - self._source, is_on = await self._speaker.get_source_and_state() - self._state = STATE_ON if is_on else STATE_OFF + 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 diff --git a/requirements_all.txt b/requirements_all.txt index 2e9c783b20c6..6968f05dab9d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -172,7 +172,7 @@ aioimaplib==0.7.15 aiokafka==0.5.1 # homeassistant.components.kef -aiokef==0.1.8 +aiokef==0.2.0 # homeassistant.components.lifx aiolifx==0.6.7 From 1f7c216a5986c541114991b73424fa4f5b229342 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 11 Dec 2019 14:33:19 +0100 Subject: [PATCH 04/14] sort imports --- homeassistant/components/kef/media_player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index 2d2f7d98a56c..4003ea10b747 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -3,8 +3,9 @@ import datetime import logging -import voluptuous as vol from aiokef.aiokef import AsyncKefSpeaker +import voluptuous as vol + from homeassistant.components.media_player import ( PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, From 38c25cc25ec257caf2098404040638e20a290160 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 2 Jan 2020 21:46:07 +0100 Subject: [PATCH 05/14] fix @MartinHjelmare's suggestions --- homeassistant/components/kef/media_player.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index 4003ea10b747..eb93055c8790 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -1,6 +1,6 @@ """Platform for the KEF Wireless Speakers.""" -import datetime +from datetime import timedelta import logging from aiokef.aiokef import AsyncKefSpeaker @@ -39,7 +39,7 @@ DOMAIN = "kef" -SCAN_INTERVAL = datetime.timedelta(seconds=30) +SCAN_INTERVAL = timedelta(seconds=30) PARALLEL_UPDATES = 0 SOURCES = {"LSX": ["Wifi", "Bluetooth", "Aux", "Opt"]} @@ -113,7 +113,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) unique_id = media_player.unique_id if unique_id in hass.data[DOMAIN]: - _LOGGER.debug("%s is already configured.", unique_id) + _LOGGER.debug("%s is already configured", unique_id) else: hass.data[DOMAIN][unique_id] = media_player async_add_entities([media_player], update_before_add=True) @@ -225,16 +225,6 @@ def icon(self): """Return the device's icon.""" return "mdi:speaker-wireless" - @property - def should_poll(self): - """It's possible that the speaker is controlled manually.""" - return True - - @property - def force_update(self): - """Force update.""" - return False - async def async_turn_off(self): """Turn the media player off.""" await self._speaker.turn_off() From 01ad8ae538ed9bf09580291d0e8c3409472454c3 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 2 Jan 2020 21:49:22 +0100 Subject: [PATCH 06/14] remove _CONFIGURING --- homeassistant/components/kef/media_player.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index eb93055c8790..602259360a78 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -27,10 +27,8 @@ ) from homeassistant.helpers import config_validation as cv -_CONFIGURING = {} _LOGGER = logging.getLogger(__name__) - DEFAULT_NAME = "KEF" DEFAULT_PORT = 50001 DEFAULT_MAX_VOLUME = 0.5 From 4c87e805debe5576978cf62944b3afde4809916e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 2 Jan 2020 21:51:07 +0100 Subject: [PATCH 07/14] change STATE_UNKNOWN to None --- homeassistant/components/kef/media_player.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index 602259360a78..b131a89fa36f 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -23,7 +23,6 @@ CONF_TYPE, STATE_OFF, STATE_ON, - STATE_UNKNOWN, ) from homeassistant.helpers import config_validation as cv @@ -145,7 +144,7 @@ def __init__( ioloop=ioloop, ) - self._state = STATE_UNKNOWN + self._state = None self._muted = None self._source = None self._volume = None @@ -181,7 +180,7 @@ async def async_update(self): self._state = STATE_OFF except (ConnectionRefusedError, ConnectionError, TimeoutError) as err: _LOGGER.debug("Error in `update`: %s", err) - self._state = STATE_UNKNOWN + self._state = None @property def volume_level(self): From 9de6edf10ab467fb37d05b14bdc36add4bd45830 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 2 Jan 2020 21:58:24 +0100 Subject: [PATCH 08/14] use lat and long for unique_id --- homeassistant/components/kef/media_player.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index b131a89fa36f..0296fd1469dd 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -18,6 +18,8 @@ ) from homeassistant.const import ( CONF_HOST, + CONF_LATITUDE, + CONF_LONGITUDE, CONF_NAME, CONF_PORT, CONF_TYPE, @@ -97,6 +99,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= sources, ) + latitude = config.data[CONF_LATITUDE] + longitude = config.data[CONF_LONGITUDE] + unique_id = f"kef-{latitude}-{longitude}" + media_player = KefMediaPlayer( name, host, @@ -107,8 +113,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= inverse_speaker_mode, sources, ioloop=hass.loop, + unique_id=unique_id, ) - unique_id = media_player.unique_id + if unique_id in hass.data[DOMAIN]: _LOGGER.debug("%s is already configured", unique_id) else: @@ -130,6 +137,7 @@ def __init__( inverse_speaker_mode, sources, ioloop, + unique_id, ): """Initialize the media player.""" self._name = name @@ -143,6 +151,7 @@ def __init__( inverse_speaker_mode, ioloop=ioloop, ) + self._unique_id = unique_id self._state = None self._muted = None @@ -215,7 +224,7 @@ def available(self): @property def unique_id(self): """Return the device unique id.""" - return f"{self._speaker.host}:{self._speaker.port}" + return self._unique_id @property def icon(self): From ce65dfac394bf54a8de66ef7f5322ae5c72e5d26 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 2 Jan 2020 22:06:27 +0100 Subject: [PATCH 09/14] bump aiokef to v0.2.2 --- homeassistant/components/kef/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/kef/manifest.json b/homeassistant/components/kef/manifest.json index 4d9e33715eaf..b815333144b1 100644 --- a/homeassistant/components/kef/manifest.json +++ b/homeassistant/components/kef/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/kef", "dependencies": [], "codeowners": ["@basnijholt"], - "requirements": ["aiokef==0.2.0"] + "requirements": ["aiokef==0.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6968f05dab9d..288734207264 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -172,7 +172,7 @@ aioimaplib==0.7.15 aiokafka==0.5.1 # homeassistant.components.kef -aiokef==0.2.0 +aiokef==0.2.2 # homeassistant.components.lifx aiolifx==0.6.7 From 6f789a2f7a5de23238a73dd4db47a84d9c11feaf Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 2 Jan 2020 22:19:16 +0100 Subject: [PATCH 10/14] use config[ATTR] instead of config.get(ATTR) --- homeassistant/components/kef/media_player.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index 0296fd1469dd..041bfcb17383 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -81,11 +81,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= host = config[CONF_HOST] speaker_type = config[CONF_TYPE] - port = config.get(CONF_PORT) - name = config.get(CONF_NAME) - maximum_volume = config.get(CONF_MAX_VOLUME) - volume_step = config.get(CONF_VOLUME_STEP) - inverse_speaker_mode = config.get(CONF_INVERSE_SPEAKER_MODE) + 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] From e7e64e8bfc22ae909b41269cc7f6879490d2fcf0 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 2 Jan 2020 22:34:23 +0100 Subject: [PATCH 11/14] use getmac --- homeassistant/components/kef/manifest.json | 2 +- homeassistant/components/kef/media_player.py | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/kef/manifest.json b/homeassistant/components/kef/manifest.json index b815333144b1..30335c409eea 100644 --- a/homeassistant/components/kef/manifest.json +++ b/homeassistant/components/kef/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/kef", "dependencies": [], "codeowners": ["@basnijholt"], - "requirements": ["aiokef==0.2.2"] + "requirements": ["aiokef==0.2.2", "getmac==0.8.1"] } diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index 041bfcb17383..f4143500cca1 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -1,9 +1,12 @@ """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 ( @@ -18,8 +21,6 @@ ) from homeassistant.const import ( CONF_HOST, - CONF_LATITUDE, - CONF_LONGITUDE, CONF_NAME, CONF_PORT, CONF_TYPE, @@ -99,9 +100,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= sources, ) - latitude = config.data[CONF_LATITUDE] - longitude = config.data[CONF_LONGITUDE] - unique_id = f"kef-{latitude}-{longitude}" + 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}" media_player = KefMediaPlayer( name, From c6df8a0d453617e450da4fe266f02e4e43dabc37 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 3 Jan 2020 08:05:11 +0100 Subject: [PATCH 12/14] fix case when MAC is None --- homeassistant/components/kef/media_player.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index f4143500cca1..2d992afe8917 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -108,8 +108,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= except ValueError: mode = "hostname" mac = await hass.async_add_executor_job(partial(get_mac_address, **{mode: host})) - - unique_id = f"kef-{mac}" + unique_id = f"kef-{mac}" if mac is not None else None media_player = KefMediaPlayer( name, From 5db883d6fd653451d6235c008d8b653bad9b1aac Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 3 Jan 2020 08:05:39 +0100 Subject: [PATCH 13/14] use host as instance lifetime id --- homeassistant/components/kef/media_player.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index 2d992afe8917..f0c2de2a86a0 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -123,10 +123,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= unique_id=unique_id, ) - if unique_id in hass.data[DOMAIN]: - _LOGGER.debug("%s is already configured", unique_id) + if host in hass.data[DOMAIN]: + _LOGGER.debug("%s is already configured", host) else: - hass.data[DOMAIN][unique_id] = media_player + hass.data[DOMAIN][host] = media_player async_add_entities([media_player], update_before_add=True) From 9431913582f1745c959d8085ccc531e764d9ccb7 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 3 Jan 2020 08:07:54 +0100 Subject: [PATCH 14/14] fix requirements --- requirements_all.txt | 1 + requirements_test_all.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements_all.txt b/requirements_all.txt index 288734207264..36f92c52a711 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -581,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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff1ded4434cb..84efd81608ae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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