From 01052f516b5d8a014bb9eed071cb556f74dca6fd Mon Sep 17 00:00:00 2001 From: Andreas Rydbrink Date: Fri, 29 Mar 2019 03:03:02 +0100 Subject: [PATCH] Add HEOS media player component (#21721) ## Description: Denon HEOS media player. **Pull request in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) with documentation (if applicable):** home-assistant/home-assistant.io#8848 ## Example entry for `configuration.yaml` (if applicable): ```yaml heos: host: HEOS-1 ``` ## Checklist: - [X] The code change is tested and works locally. - [X] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass** - [X] There is no commented out code in this PR. If user exposed functionality or configuration variables are added/changed: - [X] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) If the code communicates with devices, web services, or third-party tools: - [X] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]). - [X] New dependencies are only imported inside functions that use them ([example][ex-import]). - [X] New or updated dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`. - [X] New files were added to `.coveragerc`. If the code does not interact with devices: - [ ] Tests have been added to verify that the new code works. [ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard/__init__.py#L14 [ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard/__init__.py#L23 Co-authored-by: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> --- .coveragerc | 4 + homeassistant/components/heos/__init__.py | 52 ++++++ homeassistant/components/heos/media_player.py | 152 ++++++++++++++++++ requirements_all.txt | 3 + 4 files changed, 211 insertions(+) create mode 100644 homeassistant/components/heos/__init__.py create mode 100644 homeassistant/components/heos/media_player.py diff --git a/.coveragerc b/.coveragerc index 3cba8519314..662d880af1b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -235,11 +235,15 @@ omit = homeassistant/components/harmony/remote.py homeassistant/components/haveibeenpwned/sensor.py homeassistant/components/hdmi_cec/* +<<<<<<< HEAD homeassistant/components/heatmiser/climate.py homeassistant/components/hikvision/binary_sensor.py homeassistant/components/hikvisioncam/switch.py homeassistant/components/hipchat/notify.py homeassistant/components/hitron_coda/device_tracker.py +======= + homeassistant/components/heos/* +>>>>>>> Update HEOS to support multiple speaker and conformance. homeassistant/components/hive/* homeassistant/components/hlk_sw16/* homeassistant/components/homekit_controller/* diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py new file mode 100644 index 00000000000..e9b775b05d0 --- /dev/null +++ b/homeassistant/components/heos/__init__.py @@ -0,0 +1,52 @@ +"""Denon HEOS Media Player.""" + +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.components.media_player.const import ( + DOMAIN as MEDIA_PLAYER_DOMAIN) +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +DOMAIN = 'heos' +REQUIREMENTS = ['aioheos==0.4.0'] + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string + }) +}, extra=vol.ALLOW_EXTRA) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistantType, config: ConfigType): + """Set up the HEOS component.""" + from aioheos import AioHeosController + + host = config[DOMAIN][CONF_HOST] + controller = AioHeosController(hass.loop, host) + + try: + await asyncio.wait_for(controller.connect(), timeout=5.0) + except asyncio.TimeoutError: + _LOGGER.error('Timeout during setup.') + return False + + async def controller_close(event): + """Close connection when HASS shutsdown.""" + await controller.close() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, controller_close) + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][MEDIA_PLAYER_DOMAIN] = controller + + hass.async_create_task(async_load_platform( + hass, MEDIA_PLAYER_DOMAIN, DOMAIN, {}, config)) + + return True diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py new file mode 100644 index 00000000000..8047ffd0775 --- /dev/null +++ b/homeassistant/components/heos/media_player.py @@ -0,0 +1,152 @@ +"""Denon HEOS Media Player.""" + +from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player.const import ( + DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) +from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING + +from . import DOMAIN as HEOS_DOMAIN + +DEPENDENCIES = ["heos"] + +SUPPORT_HEOS = ( + SUPPORT_PLAY + | SUPPORT_STOP + | SUPPORT_PAUSE + | SUPPORT_PLAY_MEDIA + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_STEP +) + +PLAY_STATE_TO_STATE = { + "play": STATE_PLAYING, + "pause": STATE_PAUSED, + "stop": STATE_IDLE, +} + + +async def async_setup_platform(hass, config, async_add_devices, + discover_info=None): + """Set up the HEOS platform.""" + controller = hass.data[HEOS_DOMAIN][DOMAIN] + players = controller.get_players() + devices = [HeosMediaPlayer(p) for p in players] + async_add_devices(devices, True) + + +class HeosMediaPlayer(MediaPlayerDevice): + """The HEOS player.""" + + def __init__(self, player): + """Initialize.""" + self._player = player + + def _update_state(self): + self.async_schedule_update_ha_state() + + async def async_update(self): + """Update the player.""" + self._player.request_update() + + async def async_added_to_hass(self): + """Device added to hass.""" + self._player.state_change_callback = self._update_state + + @property + def unique_id(self): + """Get unique id of the player.""" + return self._player.player_id + + @property + def name(self): + """Return the name of the device.""" + return self._player.name + + @property + def volume_level(self): + """Volume level of the device (0..1).""" + volume = self._player.volume + return float(volume) / 100 + + @property + def state(self): + """Get state.""" + return PLAY_STATE_TO_STATE.get(self._player.play_state) + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def media_content_type(self): + """Content type of current playing media.""" + return MEDIA_TYPE_MUSIC + + @property + def media_artist(self): + """Artist of current playing media.""" + return self._player.media_artist + + @property + def media_title(self): + """Album name of current playing media.""" + return self._player.media_title + + @property + def media_album_name(self): + """Album name of current playing media.""" + return self._player.media_album + + @property + def media_image_url(self): + """Return the image url of current playing media.""" + return self._player.media_image_url + + @property + def media_content_id(self): + """Return the content ID of current playing media.""" + return self._player.media_id + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._player.mute == "on" + + async def async_mute_volume(self, mute): + """Mute volume.""" + self._player.set_mute(mute) + + async def async_media_next_track(self): + """Go TO next track.""" + self._player.play_next() + + async def async_media_previous_track(self): + """Go TO previous track.""" + self._player.play_previous() + + @property + def supported_features(self): + """Flag of media commands that are supported.""" + return SUPPORT_HEOS + + async def async_set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self._player.set_volume(volume * 100) + + async def async_media_play(self): + """Play media player.""" + self._player.play() + + async def async_media_stop(self): + """Stop media player.""" + self._player.stop() + + async def async_media_pause(self): + """Pause media player.""" + self._player.pause() diff --git a/requirements_all.txt b/requirements_all.txt index 1832aa5cf65..31b62eb824b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -123,6 +123,9 @@ aioftp==0.12.0 # homeassistant.components.harmony.remote aioharmony==0.1.8 +# homeassistant.components.heos +aioheos==0.4.0 + # homeassistant.components.emulated_hue # homeassistant.components.http aiohttp_cors==0.7.0