From c9b353f7a7e799d7cce338f924e290507e63766d Mon Sep 17 00:00:00 2001 From: Charles Blonde Date: Wed, 23 Nov 2016 08:22:52 +0100 Subject: [PATCH] Add Bose SoundTouch device support - v2 (#4523) * Add Bose SoundTouch device support * Update soundtouch.py --- .../components/media_player/services.yaml | 33 + .../components/media_player/soundtouch.py | 393 +++++++++++ requirements_all.txt | 3 + .../media_player/test_soundtouch.py | 652 ++++++++++++++++++ 4 files changed, 1081 insertions(+) create mode 100644 homeassistant/components/media_player/soundtouch.py create mode 100644 tests/components/media_player/test_soundtouch.py diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index ee0225d2a76..9482661b464 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -204,3 +204,36 @@ sonos_clear_sleep_timer: entity_id: description: Name(s) of entites that will have the timer cleared. example: 'media_player.living_room_sonos' + + +soundtouch_play_everywhere: + description: Play on all Bose Soundtouch devices + + fields: + entity_id: + description: Name of entites that will coordinate the grouping. Platform dependent. It is a shortcut for creating a multi-room zone with all devices + example: 'media_player.soundtouch_home' + +soundtouch_create_zone: + description: Create a multi-room zone + + fields: + entity_id: + description: Name of entites that will coordinate the multi-room zone. Platform dependent. + example: 'media_player.soundtouch_home' + +soundtouch_add_zone_slave: + description: Add a slave to a multi-room zone + + fields: + entity_id: + description: Name of entites that will be added to the multi-room zone. Platform dependent. + example: 'media_player.soundtouch_home' + +soundtouch_remove_zone_slave: + description: Remove a slave from the multi-room zone + + fields: + entity_id: + description: Name of entites that will be remove from the multi-room zone. Platform dependent. + example: 'media_player.soundtouch_home' \ No newline at end of file diff --git a/homeassistant/components/media_player/soundtouch.py b/homeassistant/components/media_player/soundtouch.py new file mode 100644 index 00000000000..c33422f871e --- /dev/null +++ b/homeassistant/components/media_player/soundtouch.py @@ -0,0 +1,393 @@ +"""Support for interface with a Bose Soundtouch.""" +import logging + +from os import path +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.media_player import ( + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, + SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, + SUPPORT_VOLUME_SET, SUPPORT_TURN_ON, MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.config import load_yaml_config_file +from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, CONF_PORT, + STATE_PAUSED, STATE_PLAYING, + STATE_UNAVAILABLE) + +REQUIREMENTS = ['libsoundtouch==0.1.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'media_player' +SERVICE_PLAY_EVERYWHERE = 'soundtouch_play_everywhere' +SERVICE_CREATE_ZONE = 'soundtouch_create_zone' +SERVICE_ADD_ZONE_SLAVE = 'soundtouch_add_zone_slave' +SERVICE_REMOVE_ZONE_SLAVE = 'soundtouch_remove_zone_slave' + +MAP_STATUS = { + "PLAY_STATE": STATE_PLAYING, + "BUFFERING_STATE": STATE_PLAYING, + "PAUSE_STATE": STATE_PAUSED, + "STOp_STATE": STATE_OFF +} + +SOUNDTOUCH_PLAY_EVERYWHERE = vol.Schema({ + 'master': cv.entity_id, +}) + +SOUNDTOUCH_CREATE_ZONE_SCHEMA = vol.Schema({ + 'master': cv.entity_id, + 'slaves': cv.entity_ids +}) + +SOUNDTOUCH_ADD_ZONE_SCHEMA = vol.Schema({ + 'master': cv.entity_id, + 'slaves': cv.entity_ids +}) + +SOUNDTOUCH_REMOVE_ZONE_SCHEMA = vol.Schema({ + 'master': cv.entity_id, + 'slaves': cv.entity_ids +}) + +DEFAULT_NAME = 'Bose Soundtouch' +DEFAULT_PORT = 8090 + +DEVICES = [] + +SUPPORT_SOUNDTOUCH = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ + SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \ + SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | \ + SUPPORT_VOLUME_SET | SUPPORT_TURN_ON + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Bose Soundtouch platform.""" + name = config.get(CONF_NAME) + + remote_config = { + 'name': 'HomeAssistant', + 'description': config.get(CONF_NAME), + 'id': 'ha.component.soundtouch', + 'port': config.get(CONF_PORT), + 'host': config.get(CONF_HOST) + } + + soundtouch_device = SoundTouchDevice(name, remote_config) + DEVICES.append(soundtouch_device) + add_devices([soundtouch_device]) + + descriptions = load_yaml_config_file( + path.join(path.dirname(__file__), 'services.yaml')) + + hass.services.register(DOMAIN, SERVICE_PLAY_EVERYWHERE, + play_everywhere_service, + descriptions.get(SERVICE_PLAY_EVERYWHERE), + schema=SOUNDTOUCH_PLAY_EVERYWHERE) + hass.services.register(DOMAIN, SERVICE_CREATE_ZONE, + create_zone_service, + descriptions.get(SERVICE_CREATE_ZONE), + schema=SOUNDTOUCH_CREATE_ZONE_SCHEMA) + hass.services.register(DOMAIN, SERVICE_REMOVE_ZONE_SLAVE, + remove_zone_slave, + descriptions.get(SERVICE_REMOVE_ZONE_SLAVE), + schema=SOUNDTOUCH_REMOVE_ZONE_SCHEMA) + hass.services.register(DOMAIN, SERVICE_ADD_ZONE_SLAVE, + add_zone_slave, + descriptions.get(SERVICE_ADD_ZONE_SLAVE), + schema=SOUNDTOUCH_ADD_ZONE_SCHEMA) + + +def play_everywhere_service(service): + """ + Create a zone (multi-room) and play on all devices. + + :param service: Home Assistant service with 'master' data set + + :Example: + + - service: media_player.soundtouch_play_everywhere + data: + master: media_player.soundtouch_living_room + + """ + master_device_id = service.data.get('master') + slaves = [d for d in DEVICES if d.entity_id != master_device_id] + master = next([device for device in DEVICES if + device.entity_id == master_device_id].__iter__(), None) + if master is None: + _LOGGER.warning( + "Unable to find master with entity_id:" + str(master_device_id)) + elif not slaves: + _LOGGER.warning("Unable to create zone without slaves") + else: + _LOGGER.info( + "Creating zone with master " + str(master.device.config.name)) + master.device.create_zone([slave.device for slave in slaves]) + + +def create_zone_service(service): + """ + Create a zone (multi-room) on a master and play on specified slaves. + + At least one master and one slave must be specified + + :param service: Home Assistant service with 'master' and 'slaves' data set + + :Example: + + - service: media_player.soundtouch_create_zone + data: + master: media_player.soundtouch_living_room + slaves: + - media_player.soundtouch_room + - media_player.soundtouch_kitchen + + """ + master_device_id = service.data.get('master') + slaves_ids = service.data.get('slaves') + slaves = [device for device in DEVICES if device.entity_id in slaves_ids] + master = next([device for device in DEVICES if + device.entity_id == master_device_id].__iter__(), None) + if master is None: + _LOGGER.warning( + "Unable to find master with entity_id:" + master_device_id) + elif not slaves: + _LOGGER.warning("Unable to create zone without slaves") + else: + _LOGGER.info( + "Creating zone with master " + str(master.device.config.name)) + master.device.create_zone([slave.device for slave in slaves]) + + +def add_zone_slave(service): + """ + Add slave(s) to and existing zone (multi-room). + + Zone must already exist and slaves array can not be empty. + + :param service: Home Assistant service with 'master' and 'slaves' data set + + :Example: + + - service: media_player.soundtouch_add_zone_slave + data: + master: media_player.soundtouch_living_room + slaves: + - media_player.soundtouch_room + + """ + master_device_id = service.data.get('master') + slaves_ids = service.data.get('slaves') + slaves = [device for device in DEVICES if device.entity_id in slaves_ids] + master = next([device for device in DEVICES if + device.entity_id == master_device_id].__iter__(), None) + if master is None: + _LOGGER.warning( + "Unable to find master with entity_id:" + str(master_device_id)) + elif not slaves: + _LOGGER.warning("Unable to find slaves to add") + else: + _LOGGER.info( + "Adding slaves to zone with master " + str( + master.device.config.name)) + master.device.add_zone_slave([slave.device for slave in slaves]) + + +def remove_zone_slave(service): + """ + Remove slave(s) from and existing zone (multi-room). + + Zone must already exist and slaves array can not be empty. + Note: If removing last slave, the zone will be deleted and you'll have to + create a new one. You will not be able to add a new slave anymore + + :param service: Home Assistant service with 'master' and 'slaves' data set + + :Example: + + - service: media_player.soundtouch_remove_zone_slave + data: + master: media_player.soundtouch_living_room + slaves: + - media_player.soundtouch_room + + """ + master_device_id = service.data.get('master') + slaves_ids = service.data.get('slaves') + slaves = [device for device in DEVICES if device.entity_id in slaves_ids] + master = next([device for device in DEVICES if + device.entity_id == master_device_id].__iter__(), None) + if master is None: + _LOGGER.warning( + "Unable to find master with entity_id:" + master_device_id) + elif not slaves: + _LOGGER.warning("Unable to find slaves to remove") + else: + _LOGGER.info("Removing slaves from zone with master " + + str(master.device.config.name)) + master.device.remove_zone_slave([slave.device for slave in slaves]) + + +class SoundTouchDevice(MediaPlayerDevice): + """Representation of a SoundTouch Bose device.""" + + def __init__(self, name, config): + """Create Soundtouch Entity.""" + from libsoundtouch import soundtouch_device + self._name = name + self._device = soundtouch_device(config['host'], config['port']) + self._status = self._device.status() + self._volume = self._device.volume() + self._config = config + + @property + def config(self): + """Return specific soundtouch configuration.""" + return self._config + + @property + def device(self): + """Return Soundtouch device.""" + return self._device + + def update(self): + """Retrieve the latest data.""" + self._status = self._device.status() + self._volume = self._device.volume() + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + return self._volume.actual / 100 + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + if self._status.source == 'STANDBY': + return STATE_OFF + else: + return MAP_STATUS.get(self._status.play_status, STATE_UNAVAILABLE) + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._volume.muted + + @property + def supported_media_commands(self): + """Flag of media commands that are supported.""" + return SUPPORT_SOUNDTOUCH + + def turn_off(self): + """Turn off media player.""" + self._device.power_off() + self._status = self._device.status() + + def turn_on(self): + """Turn the media player on.""" + self._device.power_on() + self._status = self._device.status() + + def volume_up(self): + """Volume up the media player.""" + self._device.volume_up() + self._volume = self._device.volume() + + def volume_down(self): + """Volume down media player.""" + self._device.volume_down() + self._volume = self._device.volume() + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self._device.set_volume(int(volume * 100)) + self._volume = self._device.volume() + + def mute_volume(self, mute): + """Send mute command.""" + self._device.mute() + self._volume = self._device.volume() + + def media_play_pause(self): + """Simulate play pause media player.""" + self._device.play_pause() + self._status = self._device.status() + + def media_play(self): + """Send play command.""" + self._device.play() + self._status = self._device.status() + + def media_pause(self): + """Send media pause command to media player.""" + self._device.pause() + self._status = self._device.status() + + def media_next_track(self): + """Send next track command.""" + self._device.next_track() + self._status = self._device.status() + + def media_previous_track(self): + """Send the previous track command.""" + self._device.previous_track() + self._status = self._device.status() + + @property + def media_image_url(self): + """Image url of current playing media.""" + return self._status.image + + @property + def media_title(self): + """Title of current playing media.""" + if self._status.station_name is not None: + return self._status.station_name + elif self._status.artist is not None: + return self._status.artist + " - " + self._status.track + else: + return None + + @property + def media_duration(self): + """Duration of current playing media in seconds.""" + return self._status.duration + + @property + def media_artist(self): + """Artist of current playing media.""" + return self._status.artist + + @property + def media_track(self): + """Artist of current playing media.""" + return self._status.track + + @property + def media_album_name(self): + """Album name of current playing media.""" + return self._status.album + + def play_media(self, media_type, media_id, **kwargs): + """Play a piece of media.""" + _LOGGER.info("Starting media with media_id:" + str(media_id)) + presets = self._device.presets() + preset = next([preset for preset in presets if + preset.preset_id == str(media_id)].__iter__(), None) + if preset is not None: + _LOGGER.info("Playing preset: " + preset.name) + self._device.select_preset(preset) + else: + _LOGGER.warning("Unable to find preset with id " + str(media_id)) diff --git a/requirements_all.txt b/requirements_all.txt index 7d123c93b77..6993c777561 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -261,6 +261,9 @@ knxip==0.3.3 # homeassistant.components.device_tracker.owntracks libnacl==1.5.0 +# homeassistant.components.media_player.soundtouch +libsoundtouch==0.1.0 + # homeassistant.components.light.lifx liffylights==0.9.4 diff --git a/tests/components/media_player/test_soundtouch.py b/tests/components/media_player/test_soundtouch.py new file mode 100644 index 00000000000..b95b774845a --- /dev/null +++ b/tests/components/media_player/test_soundtouch.py @@ -0,0 +1,652 @@ +"""Test the Soundtouch component.""" +import logging +import unittest +from unittest import mock +from libsoundtouch.device import SoundTouchDevice as STD, Status, Volume, \ + Preset, Config + +from homeassistant.components.media_player import soundtouch +from homeassistant.const import ( + STATE_OFF, STATE_PAUSED, STATE_PLAYING) +from tests.common import get_test_home_assistant + + +class MockService: + """Mock Soundtouch service.""" + + def __init__(self, master, slaves): + """Create a new service.""" + self.data = { + "master": master, + "slaves": slaves + } + + +def _mock_soundtouch_device(*args, **kwargs): + return MockDevice() + + +class MockDevice(STD): + """Mock device.""" + + def __init__(self): + self._config = MockConfig + + +class MockConfig(Config): + """Mock config.""" + + def __init__(self): + self._name = "name" + + +def _mocked_presets(*args, **kwargs): + """Return a list of mocked presets.""" + return [MockPreset("1")] + + +class MockPreset(Preset): + """Mock preset.""" + + def __init__(self, id): + self._id = id + self._name = "preset" + + +class MockVolume(Volume): + """Mock volume with value.""" + + def __init__(self): + self._actual = 12 + + +class MockVolumeMuted(Volume): + """Mock volume muted.""" + + def __init__(self): + self._actual = 12 + self._muted = True + + +class MockStatusStandby(Status): + """Mock status standby.""" + + def __init__(self): + self._source = "STANDBY" + + +class MockStatusPlaying(Status): + """Mock status playing media.""" + + def __init__(self): + self._source = "" + self._play_status = "PLAY_STATE" + self._image = "image.url" + self._artist = "artist" + self._track = "track" + self._album = "album" + self._duration = 1 + self._station_name = None + + +class MockStatusPlayingRadio(Status): + """Mock status radio.""" + + def __init__(self): + self._source = "" + self._play_status = "PLAY_STATE" + self._image = "image.url" + self._artist = None + self._track = None + self._album = None + self._duration = None + self._station_name = "station" + + +class MockStatusUnknown(Status): + """Mock status unknown media.""" + + def __init__(self): + self._source = "" + 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 MockStatusPause(Status): + """Mock status pause.""" + + def __init__(self): + self._source = "" + self._play_status = "PAUSE_STATE" + + +def default_component(): + """Return a default component.""" + return { + 'host': '192.168.0.1', + 'port': 8090, + 'name': 'soundtouch' + } + + +class TestSoundtouchMediaPlayer(unittest.TestCase): + """Bose Soundtouch test class.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + logging.disable(logging.CRITICAL) + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + logging.disable(logging.NOTSET) + soundtouch.DEVICES = [] + self.hass.stop() + + @mock.patch('libsoundtouch.soundtouch_device', side_effect=None) + def test_ensure_setup_config(self, mocked_sountouch_device): + """Test setup OK.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + # soundtouch.DEVICES[0].entity_id = 'entity_1' + self.assertEqual(len(soundtouch.DEVICES), 1) + self.assertEqual(soundtouch.DEVICES[0].name, 'soundtouch') + self.assertEqual(soundtouch.DEVICES[0].config['port'], 8090) + self.assertEqual(mocked_sountouch_device.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_update(self, mocked_sountouch_device, mocked_status, + mocked_volume): + """Test update device state.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 1) + soundtouch.DEVICES[0].update() + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_volume.call_count, 2) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status', + side_effect=MockStatusPlaying) + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_playing_media(self, mocked_sountouch_device, mocked_status, + mocked_volume): + """Test playing media info.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(soundtouch.DEVICES[0].state, STATE_PLAYING) + self.assertEqual(soundtouch.DEVICES[0].media_image_url, "image.url") + self.assertEqual(soundtouch.DEVICES[0].media_title, "artist - track") + self.assertEqual(soundtouch.DEVICES[0].media_track, "track") + self.assertEqual(soundtouch.DEVICES[0].media_artist, "artist") + self.assertEqual(soundtouch.DEVICES[0].media_album_name, "album") + self.assertEqual(soundtouch.DEVICES[0].media_duration, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status', + side_effect=MockStatusUnknown) + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_playing_unknown_media(self, mocked_sountouch_device, + mocked_status, mocked_volume): + """Test playing media info.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(soundtouch.DEVICES[0].media_title, None) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status', + side_effect=MockStatusPlayingRadio) + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_playing_radio(self, mocked_sountouch_device, mocked_status, + mocked_volume): + """Test playing radio info.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(soundtouch.DEVICES[0].state, STATE_PLAYING) + self.assertEqual(soundtouch.DEVICES[0].media_image_url, "image.url") + self.assertEqual(soundtouch.DEVICES[0].media_title, "station") + self.assertEqual(soundtouch.DEVICES[0].media_track, None) + self.assertEqual(soundtouch.DEVICES[0].media_artist, None) + self.assertEqual(soundtouch.DEVICES[0].media_album_name, None) + self.assertEqual(soundtouch.DEVICES[0].media_duration, None) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume', + side_effect=MockVolume) + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_get_volume_level(self, mocked_sountouch_device, mocked_status, + mocked_volume): + """Test volume level.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(soundtouch.DEVICES[0].volume_level, 0.12) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status', + side_effect=MockStatusStandby) + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_get_state_off(self, mocked_sountouch_device, mocked_status, + mocked_volume): + """Test state device is off.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(soundtouch.DEVICES[0].state, STATE_OFF) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status', + side_effect=MockStatusPause) + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_get_state_pause(self, mocked_sountouch_device, mocked_status, + mocked_volume): + """Test state device is paused.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(soundtouch.DEVICES[0].state, STATE_PAUSED) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume', + side_effect=MockVolumeMuted) + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_is_muted(self, mocked_sountouch_device, mocked_status, + mocked_volume): + """Test device volume is muted.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(soundtouch.DEVICES[0].is_volume_muted, True) + + @mock.patch('libsoundtouch.soundtouch_device') + def test_media_commands(self, mocked_sountouch_device): + """Test supported media commands.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(soundtouch.DEVICES[0].supported_media_commands, 1469) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.power_off') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_should_turn_off(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_power_off): + """Test device is turned off.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].turn_off() + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(mocked_power_off.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.power_on') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_should_turn_on(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_power_on): + """Test device is turned on.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].turn_on() + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(mocked_power_on.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume_up') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_volume_up(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_volume_up): + """Test volume up.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].volume_up() + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 2) + self.assertEqual(mocked_volume_up.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume_down') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_volume_down(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_volume_down): + """Test volume down.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].volume_down() + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 2) + self.assertEqual(mocked_volume_down.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.set_volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_set_volume_level(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_set_volume): + """Test set volume level.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].set_volume_level(0.17) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 2) + mocked_set_volume.assert_called_with(17) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.mute') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_mute(self, mocked_sountouch_device, mocked_status, mocked_volume, + mocked_mute): + """Test mute volume.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].mute_volume(None) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 2) + self.assertEqual(mocked_mute.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.play') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_play(self, mocked_sountouch_device, mocked_status, mocked_volume, + mocked_play): + """Test play command.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].media_play() + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(mocked_play.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.pause') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_pause(self, mocked_sountouch_device, mocked_status, mocked_volume, + mocked_pause): + """Test pause command.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].media_pause() + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(mocked_pause.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.play_pause') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_play_pause_play(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_play_pause): + """Test play/pause.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].media_play_pause() + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(mocked_play_pause.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.previous_track') + @mock.patch('libsoundtouch.device.SoundTouchDevice.next_track') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_next_previous_track(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_next_track, + mocked_previous_track): + """Test next/previous track.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 1) + soundtouch.DEVICES[0].media_next_track() + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_next_track.call_count, 1) + soundtouch.DEVICES[0].media_previous_track() + self.assertEqual(mocked_status.call_count, 3) + self.assertEqual(mocked_previous_track.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.select_preset') + @mock.patch('libsoundtouch.device.SoundTouchDevice.presets', + side_effect=_mocked_presets) + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_play_media(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_presets, mocked_select_preset): + """Test play preset 1.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 1) + soundtouch.DEVICES[0].play_media('PLAYLIST', 1) + self.assertEqual(mocked_presets.call_count, 1) + self.assertEqual(mocked_select_preset.call_count, 1) + soundtouch.DEVICES[0].play_media('PLAYLIST', 2) + self.assertEqual(mocked_presets.call_count, 2) + self.assertEqual(mocked_select_preset.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.create_zone') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_play_everywhere(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_create_zone): + """Test play everywhere.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].entity_id = "entity_1" + soundtouch.DEVICES[1].entity_id = "entity_2" + self.assertEqual(mocked_sountouch_device.call_count, 2) + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_volume.call_count, 2) + + # one master, one slave => create zone + service = MockService("entity_1", []) + soundtouch.play_everywhere_service(service) + self.assertEqual(mocked_create_zone.call_count, 1) + + # unknown master. create zone is must not be called + service = MockService("entity_X", []) + soundtouch.play_everywhere_service(service) + self.assertEqual(mocked_create_zone.call_count, 1) + + # no slaves, create zone must not be called + soundtouch.DEVICES.pop(1) + service = MockService("entity_1", []) + soundtouch.play_everywhere_service(service) + self.assertEqual(mocked_create_zone.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.create_zone') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_create_zone(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_create_zone): + """Test creating a zone.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].entity_id = "entity_1" + soundtouch.DEVICES[1].entity_id = "entity_2" + self.assertEqual(mocked_sountouch_device.call_count, 2) + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_volume.call_count, 2) + + # one master, one slave => create zone + service = MockService("entity_1", ["entity_2"]) + soundtouch.create_zone_service(service) + self.assertEqual(mocked_create_zone.call_count, 1) + + # unknown master. create zone is must not be called + service = MockService("entity_X", []) + soundtouch.create_zone_service(service) + self.assertEqual(mocked_create_zone.call_count, 1) + + # no slaves, create zone must not be called + soundtouch.DEVICES.pop(1) + service = MockService("entity_1", []) + soundtouch.create_zone_service(service) + self.assertEqual(mocked_create_zone.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.add_zone_slave') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_add_zone_slave(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_add_zone_slave): + """Test adding a slave to an existing zone.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].entity_id = "entity_1" + soundtouch.DEVICES[1].entity_id = "entity_2" + self.assertEqual(mocked_sountouch_device.call_count, 2) + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_volume.call_count, 2) + + # remove one slave + service = MockService("entity_1", ["entity_2"]) + soundtouch.add_zone_slave(service) + self.assertEqual(mocked_add_zone_slave.call_count, 1) + + # unknown master. add zone slave is not called + service = MockService("entity_X", ["entity_2"]) + soundtouch.add_zone_slave(service) + self.assertEqual(mocked_add_zone_slave.call_count, 1) + + # no slave to add, add zone slave is not called + service = MockService("entity_1", []) + soundtouch.add_zone_slave(service) + self.assertEqual(mocked_add_zone_slave.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.remove_zone_slave') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_remove_zone_slave(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_remove_zone_slave): + """Test removing a slave from a zone.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].entity_id = "entity_1" + soundtouch.DEVICES[1].entity_id = "entity_2" + self.assertEqual(mocked_sountouch_device.call_count, 2) + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_volume.call_count, 2) + + # remove one slave + service = MockService("entity_1", ["entity_2"]) + soundtouch.remove_zone_slave(service) + self.assertEqual(mocked_remove_zone_slave.call_count, 1) + + # unknown master. remove zone slave is not called + service = MockService("entity_X", ["entity_2"]) + soundtouch.remove_zone_slave(service) + self.assertEqual(mocked_remove_zone_slave.call_count, 1) + + # no slave to add, add zone slave is not called + service = MockService("entity_1", []) + soundtouch.remove_zone_slave(service) + self.assertEqual(mocked_remove_zone_slave.call_count, 1)