Skip to content
Merged
2 changes: 1 addition & 1 deletion homeassistant/components/soundtouch/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": []
}
27 changes: 27 additions & 0 deletions homeassistant/components/soundtouch/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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
Expand All @@ -12,6 +13,7 @@
SUPPORT_PLAY,
SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK,
SUPPORT_SELECT_SOURCE,
SUPPORT_TURN_OFF,
SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE,
Expand Down Expand Up @@ -80,6 +82,7 @@
| SUPPORT_TURN_ON
| SUPPORT_PLAY
| SUPPORT_PLAY_MEDIA
| SUPPORT_SELECT_SOURCE
)

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
Expand Down Expand Up @@ -234,6 +237,19 @@ def state(self):

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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I presume Source is an enum? I'd recommend using something like:

return [x.value for x in list(Source)]

to avoid the need to keep this in sync when/if upstream changes and adds new potential sources.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is indeed, but it looks like most of them don't have a method for selecting it (which kind of links to your second comment)

https://github.com/CharlesBlonde/libsoundtouch/blob/25163a7ae39de2724e2946e8b1b61484d2a590d6/libsoundtouch/utils.py#L43-50

]

@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
Expand Down Expand Up @@ -357,6 +373,17 @@ def play_media(self, media_type, media_id, **kwargs):
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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I think it would make sense to have a select_source(Source) method in the upstream lib and use that instead of having separate methods per source type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, but the upstream lib isn't maintained anymore, so we're stuck for now.
There are efforts to replace the library, but some features are missing.
CharlesBlonde/libsoundtouch#38

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.
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
130 changes: 129 additions & 1 deletion tests/components/soundtouch/test_media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import pytest

from homeassistant.components.media_player.const import (
ATTR_INPUT_SOURCE,
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
)
Expand Down Expand Up @@ -254,6 +255,36 @@ def __init__(self):
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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down