From 931fce82392af6834539017f3c9c62065eb44f5a Mon Sep 17 00:00:00 2001 From: Charles Blonde Date: Thu, 20 Apr 2017 06:52:37 +0200 Subject: [PATCH] Add Bose soundtouch discovery support and upgrade libsoundtouch library (#7005) * Add Bose soundtouch discovery support and upgrade libsoundtouch library * Remove DEVICE global variable * Update netdisco to lastest version --- homeassistant/components/discovery.py | 1 + .../components/media_player/services.yaml | 25 +- .../components/media_player/soundtouch.py | 286 ++++++++--------- requirements_all.txt | 2 +- .../media_player/test_soundtouch.py | 301 ++++++++++++------ 5 files changed, 341 insertions(+), 274 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index ed766fc3997..ea8710158d1 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -50,6 +50,7 @@ SERVICE_HANDLERS = { 'apple_tv': ('media_player', 'apple_tv'), 'frontier_silicon': ('media_player', 'frontier_silicon'), 'openhome': ('media_player', 'openhome'), + 'bose_soundtouch': ('media_player', 'soundtouch'), } CONF_IGNORE = 'ignore' diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index fa26e1613dc..e23616a47a9 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -222,30 +222,39 @@ 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 + master: + description: Name of the master entity 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. + master: + description: Name of the master entity that will coordinate the multi-room zone. Platform dependent. example: 'media_player.soundtouch_home' + slaves: + description: Name of slaves entities to add to the new zone + example: 'media_player.soundtouch_bedroom' 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. + master: + description: Name of the master entity that is coordinating the multi-room zone. Platform dependent. example: 'media_player.soundtouch_home' + slaves: + description: Name of slaves entities to add to the existing zone + example: 'media_player.soundtouch_bedroom' 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. + master: + description: Name of the master entity that is coordinating the multi-room zone. Platform dependent. example: 'media_player.soundtouch_home' + slaves: + description: Name of slaves entities to remove from the existing zone + example: 'media_player.soundtouch_bedroom' diff --git a/homeassistant/components/media_player/soundtouch.py b/homeassistant/components/media_player/soundtouch.py index 90a870a8c65..fb2e02494df 100644 --- a/homeassistant/components/media_player/soundtouch.py +++ b/homeassistant/components/media_player/soundtouch.py @@ -15,7 +15,7 @@ from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, CONF_PORT, STATE_PAUSED, STATE_PLAYING, STATE_UNAVAILABLE) -REQUIREMENTS = ['libsoundtouch==0.1.0'] +REQUIREMENTS = ['libsoundtouch==0.3.0'] _LOGGER = logging.getLogger(__name__) @@ -29,33 +29,33 @@ MAP_STATUS = { "PLAY_STATE": STATE_PLAYING, "BUFFERING_STATE": STATE_PLAYING, "PAUSE_STATE": STATE_PAUSED, - "STOp_STATE": STATE_OFF + "STOP_STATE": STATE_OFF } +DATA_SOUNDTOUCH = "soundtouch" + SOUNDTOUCH_PLAY_EVERYWHERE = vol.Schema({ - 'master': cv.entity_id, + vol.Required('master'): cv.entity_id }) SOUNDTOUCH_CREATE_ZONE_SCHEMA = vol.Schema({ - 'master': cv.entity_id, - 'slaves': cv.entity_ids + vol.Required('master'): cv.entity_id, + vol.Required('slaves'): cv.entity_ids }) SOUNDTOUCH_ADD_ZONE_SCHEMA = vol.Schema({ - 'master': cv.entity_id, - 'slaves': cv.entity_ids + vol.Required('master'): cv.entity_id, + vol.Required('slaves'): cv.entity_ids }) SOUNDTOUCH_REMOVE_ZONE_SCHEMA = vol.Schema({ - 'master': cv.entity_id, - 'slaves': cv.entity_ids + vol.Required('master'): cv.entity_id, + vol.Required('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 | \ @@ -70,180 +70,99 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Bose Soundtouch platform.""" - name = config.get(CONF_NAME) + if DATA_SOUNDTOUCH not in hass.data: + hass.data[DATA_SOUNDTOUCH] = [] - remote_config = { - 'name': 'HomeAssistant', - 'description': config.get(CONF_NAME), - 'id': 'ha.component.soundtouch', - 'port': config.get(CONF_PORT), - 'host': config.get(CONF_HOST) - } + if discovery_info: + # Discovery + host = discovery_info["host"] + port = int(discovery_info["port"]) - soundtouch_device = SoundTouchDevice(name, remote_config) - DEVICES.append(soundtouch_device) - add_devices([soundtouch_device]) + # if device already exists by config + if host in [device.config['host'] for device in + hass.data[DATA_SOUNDTOUCH]]: + return + + remote_config = { + 'id': 'ha.component.soundtouch', + 'host': host, + 'port': port + } + soundtouch_device = SoundTouchDevice(None, remote_config) + hass.data[DATA_SOUNDTOUCH].append(soundtouch_device) + add_devices([soundtouch_device]) + else: + # Config + name = config.get(CONF_NAME) + remote_config = { + 'id': 'ha.component.soundtouch', + 'port': config.get(CONF_PORT), + 'host': config.get(CONF_HOST) + } + soundtouch_device = SoundTouchDevice(name, remote_config) + hass.data[DATA_SOUNDTOUCH].append(soundtouch_device) + add_devices([soundtouch_device]) descriptions = load_yaml_config_file( path.join(path.dirname(__file__), 'services.yaml')) + def service_handle(service): + """Internal func for applying a service.""" + master_device_id = service.data.get('master') + slaves_ids = service.data.get('slaves') + slaves = [] + if slaves_ids: + slaves = [device for device in hass.data[DATA_SOUNDTOUCH] if + device.entity_id in slaves_ids] + + master = next([device for device in hass.data[DATA_SOUNDTOUCH] 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)) + return + + if service.service == SERVICE_PLAY_EVERYWHERE: + slaves = [d for d in hass.data[DATA_SOUNDTOUCH] if + d.entity_id != master_device_id] + master.create_zone(slaves) + elif service.service == SERVICE_CREATE_ZONE: + master.create_zone(slaves) + elif service.service == SERVICE_REMOVE_ZONE_SLAVE: + master.remove_zone_slave(slaves) + elif service.service == SERVICE_ADD_ZONE_SLAVE: + master.add_zone_slave(slaves) + hass.services.register(DOMAIN, SERVICE_PLAY_EVERYWHERE, - play_everywhere_service, + service_handle, descriptions.get(SERVICE_PLAY_EVERYWHERE), schema=SOUNDTOUCH_PLAY_EVERYWHERE) hass.services.register(DOMAIN, SERVICE_CREATE_ZONE, - create_zone_service, + service_handle, descriptions.get(SERVICE_CREATE_ZONE), schema=SOUNDTOUCH_CREATE_ZONE_SCHEMA) hass.services.register(DOMAIN, SERVICE_REMOVE_ZONE_SLAVE, - remove_zone_slave, + service_handle, descriptions.get(SERVICE_REMOVE_ZONE_SLAVE), schema=SOUNDTOUCH_REMOVE_ZONE_SCHEMA) hass.services.register(DOMAIN, SERVICE_ADD_ZONE_SLAVE, - add_zone_slave, + service_handle, 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']) + if name is None: + self._name = self._device.config.name + else: + self._name = name self._status = self._device.status() self._volume = self._device.volume() self._config = config @@ -297,7 +216,7 @@ class SoundTouchDevice(MediaPlayerDevice): self._status = self._device.status() def turn_on(self): - """Turn the media player on.""" + """Turn on media player.""" self._device.power_on() self._status = self._device.status() @@ -392,3 +311,52 @@ class SoundTouchDevice(MediaPlayerDevice): self._device.select_preset(preset) else: _LOGGER.warning("Unable to find preset with id " + str(media_id)) + + def create_zone(self, slaves): + """ + Create a zone (multi-room) and play on selected devices. + + :param slaves: slaves on which to play + + """ + if not slaves: + _LOGGER.warning("Unable to create zone without slaves") + else: + _LOGGER.info( + "Creating zone with master " + str(self.device.config.name)) + self.device.create_zone([slave.device for slave in slaves]) + + def remove_zone_slave(self, slaves): + """ + 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 slaves: slaves to remove from the zone + + """ + if not slaves: + _LOGGER.warning("Unable to find slaves to remove") + else: + _LOGGER.info("Removing slaves from zone with master " + + str(self.device.config.name)) + self.device.remove_zone_slave([slave.device for slave in slaves]) + + def add_zone_slave(self, slaves): + """ + Add slave(s) to and existing zone (multi-room). + + Zone must already exist and slaves array can not be empty. + + :param slaves:slaves to add + + """ + if not slaves: + _LOGGER.warning("Unable to find slaves to add") + else: + _LOGGER.info( + "Adding slaves to zone with master " + str( + self.device.config.name)) + self.device.add_zone_slave([slave.device for slave in slaves]) diff --git a/requirements_all.txt b/requirements_all.txt index 7ab7178cbfa..43ab2a051e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -358,7 +358,7 @@ knxip==0.3.3 libnacl==1.5.0 # homeassistant.components.media_player.soundtouch -libsoundtouch==0.1.0 +libsoundtouch==0.3.0 # homeassistant.components.light.lifx_legacy liffylights==0.9.4 diff --git a/tests/components/media_player/test_soundtouch.py b/tests/components/media_player/test_soundtouch.py index 84551241694..4958f5ee263 100644 --- a/tests/components/media_player/test_soundtouch.py +++ b/tests/components/media_player/test_soundtouch.py @@ -155,21 +155,67 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): 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.""" + """Test setup OK with custom config.""" 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) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + self.assertEqual(len(all_devices), 1) + self.assertEqual(all_devices[0].name, 'soundtouch') + self.assertEqual(all_devices[0].config['port'], 8090) self.assertEqual(mocked_sountouch_device.call_count, 1) + @mock.patch('libsoundtouch.soundtouch_device', side_effect=None) + def test_ensure_setup_discovery(self, mocked_sountouch_device): + """Test setup with discovery.""" + new_device = {"port": "8090", + "host": "192.168.1.1", + "properties": {}, + "hostname": "hostname.local"} + soundtouch.setup_platform(self.hass, + None, + mock.MagicMock(), + new_device) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + self.assertEqual(len(all_devices), 1) + self.assertEqual(all_devices[0].config['port'], 8090) + self.assertEqual(all_devices[0].config['host'], '192.168.1.1') + self.assertEqual(mocked_sountouch_device.call_count, 1) + + @mock.patch('libsoundtouch.soundtouch_device', side_effect=None) + def test_ensure_setup_discovery_no_duplicate(self, + mocked_sountouch_device): + """Test setup OK if device already exists.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(len(self.hass.data[soundtouch.DATA_SOUNDTOUCH]), 1) + new_device = {"port": "8090", + "host": "192.168.1.1", + "properties": {}, + "hostname": "hostname.local"} + soundtouch.setup_platform(self.hass, + None, + mock.MagicMock(), + new_device # New device + ) + self.assertEqual(len(self.hass.data[soundtouch.DATA_SOUNDTOUCH]), 2) + existing_device = {"port": "8090", + "host": "192.168.0.1", + "properties": {}, + "hostname": "hostname.local"} + soundtouch.setup_platform(self.hass, + None, + mock.MagicMock(), + existing_device # Existing device + ) + self.assertEqual(mocked_sountouch_device.call_count, 2) + self.assertEqual(len(self.hass.data[soundtouch.DATA_SOUNDTOUCH]), 2) + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', @@ -183,7 +229,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): 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.hass.data[soundtouch.DATA_SOUNDTOUCH][0].update() self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 2) @@ -201,13 +247,14 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): 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) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + self.assertEqual(all_devices[0].state, STATE_PLAYING) + self.assertEqual(all_devices[0].media_image_url, "image.url") + self.assertEqual(all_devices[0].media_title, "artist - track") + self.assertEqual(all_devices[0].media_track, "track") + self.assertEqual(all_devices[0].media_artist, "artist") + self.assertEqual(all_devices[0].media_album_name, "album") + self.assertEqual(all_devices[0].media_duration, 1) @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') @mock.patch('libsoundtouch.device.SoundTouchDevice.status', @@ -223,7 +270,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): 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) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + self.assertEqual(all_devices[0].media_title, None) @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') @mock.patch('libsoundtouch.device.SoundTouchDevice.status', @@ -239,13 +287,14 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): 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) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + self.assertEqual(all_devices[0].state, STATE_PLAYING) + self.assertEqual(all_devices[0].media_image_url, "image.url") + self.assertEqual(all_devices[0].media_title, "station") + self.assertEqual(all_devices[0].media_track, None) + self.assertEqual(all_devices[0].media_artist, None) + self.assertEqual(all_devices[0].media_album_name, None) + self.assertEqual(all_devices[0].media_duration, None) @mock.patch('libsoundtouch.device.SoundTouchDevice.volume', side_effect=MockVolume) @@ -261,7 +310,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): 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) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + self.assertEqual(all_devices[0].volume_level, 0.12) @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') @mock.patch('libsoundtouch.device.SoundTouchDevice.status', @@ -277,7 +327,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): 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) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + self.assertEqual(all_devices[0].state, STATE_OFF) @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') @mock.patch('libsoundtouch.device.SoundTouchDevice.status', @@ -293,7 +344,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): 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) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + self.assertEqual(all_devices[0].state, STATE_PAUSED) @mock.patch('libsoundtouch.device.SoundTouchDevice.volume', side_effect=MockVolumeMuted) @@ -309,7 +361,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): 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) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + self.assertEqual(all_devices[0].is_volume_muted, True) @mock.patch('libsoundtouch.soundtouch_device') def test_media_commands(self, mocked_sountouch_device): @@ -318,7 +371,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): default_component(), mock.MagicMock()) self.assertEqual(mocked_sountouch_device.call_count, 1) - self.assertEqual(soundtouch.DEVICES[0].supported_features, 17853) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + self.assertEqual(all_devices[0].supported_features, 17853) @mock.patch('libsoundtouch.device.SoundTouchDevice.power_off') @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') @@ -331,7 +385,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].turn_off() + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_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) @@ -348,7 +403,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].turn_on() + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_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) @@ -365,7 +421,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].volume_up() + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_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) @@ -382,7 +439,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].volume_down() + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_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) @@ -399,7 +457,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].set_volume_level(0.17) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_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) @@ -416,7 +475,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].mute_volume(None) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_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) @@ -433,7 +493,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].media_play() + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_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) @@ -450,7 +511,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].media_pause() + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_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) @@ -467,7 +529,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].media_play_pause() + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_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) @@ -486,13 +549,14 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] 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() + all_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() + all_devices[0].media_previous_track() self.assertEqual(mocked_status.call_count, 3) self.assertEqual(mocked_previous_track.call_count, 1) @@ -509,13 +573,14 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] 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) + all_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) + all_devices[0].play_media('PLAYLIST', 2) self.assertEqual(mocked_presets.call_count, 2) self.assertEqual(mocked_select_preset.call_count, 1) @@ -533,26 +598,30 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].entity_id = "entity_1" - soundtouch.DEVICES[1].entity_id = "entity_2" + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_devices[0].entity_id = "media_player.entity_1" + all_devices[1].entity_id = "media_player.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.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_PLAY_EVERYWHERE, + {"master": "media_player.entity_1"}, True) 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.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_PLAY_EVERYWHERE, + {"master": "media_player.entity_X"}, True) 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) + all_devices.pop(1) + self.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_PLAY_EVERYWHERE, + {"master": "media_player.entity_1"}, True) self.assertEqual(mocked_create_zone.call_count, 1) @mock.patch('libsoundtouch.device.SoundTouchDevice.create_zone') @@ -569,63 +638,34 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].entity_id = "entity_1" - soundtouch.DEVICES[1].entity_id = "entity_2" + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_devices[0].entity_id = "media_player.entity_1" + all_devices[1].entity_id = "media_player.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.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_CREATE_ZONE, + {"master": "media_player.entity_1", + "slaves": ["media_player.entity_2"]}, True) 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.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_CREATE_ZONE, + {"master": "media_player.entity_X", + "slaves": ["media_player.entity_2"]}, True) 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.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_CREATE_ZONE, + {"master": "media_player.entity_X", + "slaves": []}, True) 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') @@ -633,6 +673,48 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): side_effect=_mock_soundtouch_device) def test_remove_zone_slave(self, mocked_sountouch_device, mocked_status, mocked_volume, mocked_remove_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()) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_devices[0].entity_id = "media_player.entity_1" + all_devices[1].entity_id = "media_player.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 + self.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_REMOVE_ZONE_SLAVE, + {"master": "media_player.entity_1", + "slaves": ["media_player.entity_2"]}, True) + self.assertEqual(mocked_remove_zone_slave.call_count, 1) + + # unknown master. add zone slave is not called + self.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_REMOVE_ZONE_SLAVE, + {"master": "media_player.entity_X", + "slaves": ["media_player.entity_2"]}, True) + self.assertEqual(mocked_remove_zone_slave.call_count, 1) + + # no slave to add, add zone slave is not called + self.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_REMOVE_ZONE_SLAVE, + {"master": "media_player.entity_1", + "slaves": []}, True) + self.assertEqual(mocked_remove_zone_slave.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 removing a slave from a zone.""" soundtouch.setup_platform(self.hass, default_component(), @@ -640,23 +722,30 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].entity_id = "entity_1" - soundtouch.DEVICES[1].entity_id = "entity_2" + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_devices[0].entity_id = "media_player.entity_1" + all_devices[1].entity_id = "media_player.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) + # add one slave + self.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_ADD_ZONE_SLAVE, + {"master": "media_player.entity_1", + "slaves": ["media_player.entity_2"]}, True) + self.assertEqual(mocked_add_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) + # unknown master. add zone slave is not called + self.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_ADD_ZONE_SLAVE, + {"master": "media_player.entity_X", + "slaves": ["media_player.entity_2"]}, True) + self.assertEqual(mocked_add_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) + self.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_ADD_ZONE_SLAVE, + {"master": "media_player.entity_1", + "slaves": ["media_player.entity_X"]}, True) + self.assertEqual(mocked_add_zone_slave.call_count, 1)