From 8f76f59b8b4bbcccf202cfc4139d510a7cda5050 Mon Sep 17 00:00:00 2001 From: Thomas Johanns Date: Tue, 12 May 2020 22:56:12 +0200 Subject: [PATCH] Implement soundtouch select source (#31669) --- .../components/soundtouch/manifest.json | 2 +- .../components/soundtouch/media_player.py | 27 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../soundtouch/test_media_player.py | 130 +++++++++++++++++- 5 files changed, 159 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/soundtouch/manifest.json b/homeassistant/components/soundtouch/manifest.json index 6477983d94f..c9cc4f32734 100644 --- a/homeassistant/components/soundtouch/manifest.json +++ b/homeassistant/components/soundtouch/manifest.json @@ -2,6 +2,6 @@ "domain": "soundtouch", "name": "Bose Soundtouch", "documentation": "https://www.home-assistant.io/integrations/soundtouch", - "requirements": ["libsoundtouch==0.7.2"], + "requirements": ["libsoundtouch==0.8"], "codeowners": [] } diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index 482d1025034..83c8192ccb2 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -3,6 +3,7 @@ import logging import re from libsoundtouch import soundtouch_device +from libsoundtouch.utils import Source import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity @@ -12,6 +13,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, @@ -80,6 +82,7 @@ SUPPORT_SOUNDTOUCH = ( | SUPPORT_TURN_ON | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA + | SUPPORT_SELECT_SOURCE ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -234,6 +237,19 @@ class SoundTouchDevice(MediaPlayerEntity): return MAP_STATUS.get(self._status.play_status, STATE_UNAVAILABLE) + @property + def source(self): + """Name of the current input source.""" + return self._status.source + + @property + def source_list(self): + """List of available input sources.""" + return [ + Source.AUX.value, + Source.BLUETOOTH.value, + ] + @property def is_volume_muted(self): """Boolean if volume is currently muted.""" @@ -357,6 +373,17 @@ class SoundTouchDevice(MediaPlayerEntity): else: _LOGGER.warning("Unable to find preset with id %s", media_id) + def select_source(self, source): + """Select input source.""" + if source == Source.AUX.value: + _LOGGER.debug("Selecting source AUX") + self._device.select_source_aux() + elif source == Source.BLUETOOTH.value: + _LOGGER.debug("Selecting source Bluetooth") + self._device.select_source_bluetooth() + else: + _LOGGER.warning("Source %s is not supported", source) + def create_zone(self, slaves): """ Create a zone (multi-room) and play on selected devices. diff --git a/requirements_all.txt b/requirements_all.txt index ed16047c8ef..ab861116cd3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -837,7 +837,7 @@ libpyvivotek==0.4.0 librouteros==3.0.0 # homeassistant.components.soundtouch -libsoundtouch==0.7.2 +libsoundtouch==0.8 # homeassistant.components.life360 life360==4.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9155447407..c8e87b73a0f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -356,7 +356,7 @@ libpurecool==0.6.1 librouteros==3.0.0 # homeassistant.components.soundtouch -libsoundtouch==0.7.2 +libsoundtouch==0.8 # homeassistant.components.logi_circle logi_circle==0.2.2 diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py index 0d796cd8785..d351df32101 100644 --- a/tests/components/soundtouch/test_media_player.py +++ b/tests/components/soundtouch/test_media_player.py @@ -11,6 +11,7 @@ from libsoundtouch.device import ( import pytest from homeassistant.components.media_player.const import ( + ATTR_INPUT_SOURCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ) @@ -254,6 +255,36 @@ class MockStatusPause(Status): self._station_name = None +class MockStatusPlayingAux(Status): + """Mock status AUX.""" + + def __init__(self): + """Init the class.""" + self._source = "AUX" + self._play_status = "PLAY_STATE" + self._image = "image.url" + self._artist = None + self._track = None + self._album = None + self._duration = None + self._station_name = None + + +class MockStatusPlayingBluetooth(Status): + """Mock status Bluetooth.""" + + def __init__(self): + """Init the class.""" + self._source = "BLUETOOTH" + self._play_status = "PLAY_STATE" + self._image = "image.url" + self._artist = "artist" + self._track = "track" + self._album = "album" + self._duration = None + self._station_name = None + + async def test_ensure_setup_config(mocked_status, mocked_volume, hass, one_device): """Test setup OK with custom config.""" await setup_soundtouch( @@ -365,6 +396,37 @@ async def test_playing_radio(mocked_status, mocked_volume, hass, one_device): assert entity_1_state.attributes["media_title"] == "station" +async def test_playing_aux(mocked_status, mocked_volume, hass, one_device): + """Test playing AUX info.""" + mocked_status.side_effect = MockStatusPlayingAux + await setup_soundtouch(hass, DEVICE_1_CONFIG) + + assert one_device.call_count == 1 + assert mocked_status.call_count == 2 + assert mocked_volume.call_count == 2 + + entity_1_state = hass.states.get("media_player.soundtouch_1") + assert entity_1_state.state == STATE_PLAYING + assert entity_1_state.attributes["source"] == "AUX" + + +async def test_playing_bluetooth(mocked_status, mocked_volume, hass, one_device): + """Test playing Bluetooth info.""" + mocked_status.side_effect = MockStatusPlayingBluetooth + await setup_soundtouch(hass, DEVICE_1_CONFIG) + + assert one_device.call_count == 1 + assert mocked_status.call_count == 2 + assert mocked_volume.call_count == 2 + + entity_1_state = hass.states.get("media_player.soundtouch_1") + assert entity_1_state.state == STATE_PLAYING + assert entity_1_state.attributes["source"] == "BLUETOOTH" + assert entity_1_state.attributes["media_track"] == "track" + assert entity_1_state.attributes["media_artist"] == "artist" + assert entity_1_state.attributes["media_album_name"] == "album" + + async def test_get_volume_level(mocked_status, mocked_volume, hass, one_device): """Test volume level.""" mocked_volume.side_effect = MockVolume @@ -426,7 +488,7 @@ async def test_media_commands(mocked_status, mocked_volume, hass, one_device): assert mocked_volume.call_count == 2 entity_1_state = hass.states.get("media_player.soundtouch_1") - assert entity_1_state.attributes["supported_features"] == 18365 + assert entity_1_state.attributes["supported_features"] == 20413 @patch("libsoundtouch.device.SoundTouchDevice.power_off") @@ -694,6 +756,72 @@ async def test_play_media_url( mocked_play_url.assert_called_with("http://fqdn/file.mp3") +@patch("libsoundtouch.device.SoundTouchDevice.select_source_aux") +async def test_select_source_aux( + mocked_select_source_aux, mocked_status, mocked_volume, hass, one_device +): + """Test select AUX.""" + await setup_soundtouch(hass, DEVICE_1_CONFIG) + + assert mocked_select_source_aux.call_count == 0 + await hass.services.async_call( + "media_player", + "select_source", + {"entity_id": "media_player.soundtouch_1", ATTR_INPUT_SOURCE: "AUX"}, + True, + ) + + assert mocked_select_source_aux.call_count == 1 + + +@patch("libsoundtouch.device.SoundTouchDevice.select_source_bluetooth") +async def test_select_source_bluetooth( + mocked_select_source_bluetooth, mocked_status, mocked_volume, hass, one_device +): + """Test select Bluetooth.""" + await setup_soundtouch(hass, DEVICE_1_CONFIG) + + assert mocked_select_source_bluetooth.call_count == 0 + await hass.services.async_call( + "media_player", + "select_source", + {"entity_id": "media_player.soundtouch_1", ATTR_INPUT_SOURCE: "BLUETOOTH"}, + True, + ) + + assert mocked_select_source_bluetooth.call_count == 1 + + +@patch("libsoundtouch.device.SoundTouchDevice.select_source_bluetooth") +@patch("libsoundtouch.device.SoundTouchDevice.select_source_aux") +async def test_select_source_invalid_source( + mocked_select_source_aux, + mocked_select_source_bluetooth, + mocked_status, + mocked_volume, + hass, + one_device, +): + """Test select unsupported source.""" + await setup_soundtouch(hass, DEVICE_1_CONFIG) + + assert mocked_select_source_aux.call_count == 0 + assert mocked_select_source_bluetooth.call_count == 0 + + await hass.services.async_call( + "media_player", + "select_source", + { + "entity_id": "media_player.soundtouch_1", + ATTR_INPUT_SOURCE: "SOMETHING_UNSUPPORTED", + }, + True, + ) + + assert mocked_select_source_aux.call_count == 0 + assert mocked_select_source_bluetooth.call_count == 0 + + @patch("libsoundtouch.device.SoundTouchDevice.create_zone") async def test_play_everywhere( mocked_create_zone, mocked_status, mocked_volume, hass, two_zones