From 78b6439ee6a07e3aacbd6555dca2595e74d2e8b5 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 20 Sep 2018 23:50:11 +0200 Subject: [PATCH] Use pysonos for Sonos media player (#16753) --- .../components/media_player/sonos.py | 38 +++++----- homeassistant/components/sonos/__init__.py | 6 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- script/gen_requirements_all.py | 2 +- tests/components/media_player/test_sonos.py | 70 +++++++++---------- tests/components/sonos/test_init.py | 6 +- 7 files changed, 67 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 72ac0a046a3..fd735a5b830 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -1,5 +1,5 @@ """ -Support to interface with Sonos players (via SoCo). +Support to interface with Sonos players. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.sonos/ @@ -31,11 +31,11 @@ DEPENDENCIES = ('sonos',) _LOGGER = logging.getLogger(__name__) -# Quiet down soco logging to just actual problems. -logging.getLogger('soco').setLevel(logging.WARNING) -logging.getLogger('soco.events').setLevel(logging.ERROR) -logging.getLogger('soco.data_structures_entry').setLevel(logging.ERROR) -_SOCO_SERVICES_LOGGER = logging.getLogger('soco.services') +# Quiet down pysonos logging to just actual problems. +logging.getLogger('pysonos').setLevel(logging.WARNING) +logging.getLogger('pysonos.events').setLevel(logging.ERROR) +logging.getLogger('pysonos.data_structures_entry').setLevel(logging.ERROR) +_SOCO_SERVICES_LOGGER = logging.getLogger('pysonos.services') SUPPORT_SONOS = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ SUPPORT_PLAY | SUPPORT_PAUSE | SUPPORT_STOP | SUPPORT_SELECT_SOURCE |\ @@ -143,18 +143,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): def _setup_platform(hass, config, add_entities, discovery_info): """Set up the Sonos platform.""" - import soco + import pysonos if DATA_SONOS not in hass.data: hass.data[DATA_SONOS] = SonosData() advertise_addr = config.get(CONF_ADVERTISE_ADDR) if advertise_addr: - soco.config.EVENT_ADVERTISE_IP = advertise_addr + pysonos.config.EVENT_ADVERTISE_IP = advertise_addr players = [] if discovery_info: - player = soco.SoCo(discovery_info.get('host')) + player = pysonos.SoCo(discovery_info.get('host')) # If device already exists by config if player.uid in hass.data[DATA_SONOS].uids: @@ -174,11 +174,11 @@ def _setup_platform(hass, config, add_entities, discovery_info): hosts = hosts.split(',') if isinstance(hosts, str) else hosts for host in hosts: try: - players.append(soco.SoCo(socket.gethostbyname(host))) + players.append(pysonos.SoCo(socket.gethostbyname(host))) except OSError: _LOGGER.warning("Failed to initialize '%s'", host) else: - players = soco.discover( + players = pysonos.discover( interface_addr=config.get(CONF_INTERFACE_ADDR)) if not players: @@ -287,7 +287,7 @@ def soco_error(errorcodes=None): @ft.wraps(funct) def wrapper(*args, **kwargs): """Wrap for all soco UPnP exception.""" - from soco.exceptions import SoCoUPnPException, SoCoException + from pysonos.exceptions import SoCoUPnPException, SoCoException # Temporarily disable SoCo logging because it will log the # UPnP exception otherwise @@ -612,9 +612,9 @@ class SonosDevice(MediaPlayerDevice): current_uri_metadata = media_info["CurrentURIMetaData"] if current_uri_metadata not in ('', 'NOT_IMPLEMENTED', None): # currently soco does not have an API for this - import soco - current_uri_metadata = soco.xml.XML.fromstring( - soco.utils.really_utf8(current_uri_metadata)) + import pysonos + current_uri_metadata = pysonos.xml.XML.fromstring( + pysonos.utils.really_utf8(current_uri_metadata)) md_title = current_uri_metadata.findtext( './/{http://purl.org/dc/elements/1.1/}title') @@ -950,7 +950,7 @@ class SonosDevice(MediaPlayerDevice): If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue. """ if kwargs.get(ATTR_MEDIA_ENQUEUE): - from soco.exceptions import SoCoUPnPException + from pysonos.exceptions import SoCoUPnPException try: self.soco.add_uri_to_queue(media_id) except SoCoUPnPException: @@ -981,7 +981,7 @@ class SonosDevice(MediaPlayerDevice): @soco_error() def snapshot(self, with_group=True): """Snapshot the player.""" - from soco.snapshot import Snapshot + from pysonos.snapshot import Snapshot self._soco_snapshot = Snapshot(self.soco) self._soco_snapshot.snapshot() @@ -996,7 +996,7 @@ class SonosDevice(MediaPlayerDevice): @soco_error() def restore(self, with_group=True): """Restore snapshot for the player.""" - from soco.exceptions import SoCoException + from pysonos.exceptions import SoCoException try: # need catch exception if a coordinator is going to slave. # this state will recover with group part. @@ -1060,7 +1060,7 @@ class SonosDevice(MediaPlayerDevice): @soco_coordinator def set_alarm(self, **data): """Set the alarm clock on the player.""" - from soco import alarms + from pysonos import alarms alarm = None for one_alarm in alarms.get_alarms(self.soco): # pylint: disable=protected-access diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 60f3a8858b2..30573ee03ee 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -4,7 +4,7 @@ from homeassistant.helpers import config_entry_flow DOMAIN = 'sonos' -REQUIREMENTS = ['SoCo==0.16'] +REQUIREMENTS = ['pysonos==0.0.1'] async def async_setup(hass, config): @@ -29,9 +29,9 @@ async def async_setup_entry(hass, entry): async def _async_has_devices(hass): """Return if there are devices that can be discovered.""" - import soco + import pysonos - return await hass.async_add_executor_job(soco.discover) + return await hass.async_add_executor_job(pysonos.discover) config_entry_flow.register_discovery_flow( diff --git a/requirements_all.txt b/requirements_all.txt index 21104fab416..f9296ae28cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -66,9 +66,6 @@ PyXiaomiGateway==0.10.0 # homeassistant.components.remember_the_milk RtmAPI==0.7.0 -# homeassistant.components.sonos -SoCo==0.16 - # homeassistant.components.sensor.travisci TravisPy==0.3.5 @@ -1060,6 +1057,9 @@ pysma==0.2 # homeassistant.components.switch.snmp pysnmp==4.4.5 +# homeassistant.components.sonos +pysonos==0.0.1 + # homeassistant.components.notify.stride pystride==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83566a0bc30..78fc33dde71 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -24,9 +24,6 @@ HAP-python==2.2.2 # homeassistant.components.sensor.rmvtransport PyRMVtransport==0.1 -# homeassistant.components.sonos -SoCo==0.16 - # homeassistant.components.device_tracker.automatic aioautomatic==0.6.5 @@ -167,6 +164,9 @@ pyotp==2.2.6 # homeassistant.components.qwikswitch pyqwikswitch==0.8 +# homeassistant.components.sonos +pysonos==0.0.1 + # homeassistant.components.sensor.darksky # homeassistant.components.weather.darksky python-forecastio==1.4.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 3cde13a2a97..c4776e74f93 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -79,6 +79,7 @@ TEST_REQUIREMENTS = ( 'pynx584', 'pyopenuv', 'pyotp', + 'pysonos', 'pyqwikswitch', 'PyRMVtransport', 'python-forecastio', @@ -92,7 +93,6 @@ TEST_REQUIREMENTS = ( 'ring_doorbell', 'rxv', 'sleepyq', - 'SoCo', 'somecomfort', 'sqlalchemy', 'statsd', diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py index 5a845738fa3..cb3da3ab899 100644 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -2,10 +2,10 @@ import datetime import socket import unittest -import soco.snapshot +import pysonos.snapshot from unittest import mock -import soco -from soco import alarms +import pysonos +from pysonos import alarms from homeassistant.setup import setup_component from homeassistant.components.media_player import sonos, DOMAIN @@ -17,16 +17,16 @@ from tests.common import get_test_home_assistant ENTITY_ID = 'media_player.kitchen' -class socoDiscoverMock(): - """Mock class for the soco.discover method.""" +class pysonosDiscoverMock(): + """Mock class for the pysonos.discover method.""" def discover(interface_addr): - """Return tuple of soco.SoCo objects representing found speakers.""" + """Return tuple of pysonos.SoCo objects representing found speakers.""" return {SoCoMock('192.0.2.1')} class AvTransportMock(): - """Mock class for the avTransport property on soco.SoCo object.""" + """Mock class for the avTransport property on pysonos.SoCo object.""" def __init__(self): """Initialize ethe Transport mock.""" @@ -41,7 +41,7 @@ class AvTransportMock(): class MusicLibraryMock(): - """Mock class for the music_library property on soco.SoCo object.""" + """Mock class for the music_library property on pysonos.SoCo object.""" def get_sonos_favorites(self): """Return favorites.""" @@ -49,10 +49,10 @@ class MusicLibraryMock(): class SoCoMock(): - """Mock class for the soco.SoCo object.""" + """Mock class for the pysonos.SoCo object.""" def __init__(self, ip): - """Initialize soco object.""" + """Initialize SoCo object.""" self.ip_address = ip self.is_visible = True self.volume = 50 @@ -153,7 +153,7 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.SonosDevice.available = self.real_available self.hass.stop() - @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch('pysonos.SoCo', new=SoCoMock) @mock.patch('socket.create_connection', side_effect=socket.error()) def test_ensure_setup_discovery(self, *args): """Test a single device using the autodiscovery provided by HASS.""" @@ -165,9 +165,9 @@ class TestSonosMediaPlayer(unittest.TestCase): self.assertEqual(len(devices), 1) self.assertEqual(devices[0].name, 'Kitchen') - @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch('pysonos.SoCo', new=SoCoMock) @mock.patch('socket.create_connection', side_effect=socket.error()) - @mock.patch('soco.discover') + @mock.patch('pysonos.discover') def test_ensure_setup_config_interface_addr(self, discover_mock, *args): """Test an interface address config'd by the HASS config file.""" discover_mock.return_value = {SoCoMock('192.0.2.1')} @@ -184,7 +184,7 @@ class TestSonosMediaPlayer(unittest.TestCase): self.assertEqual(len(self.hass.data[sonos.DATA_SONOS].devices), 1) self.assertEqual(discover_mock.call_count, 1) - @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch('pysonos.SoCo', new=SoCoMock) @mock.patch('socket.create_connection', side_effect=socket.error()) def test_ensure_setup_config_hosts_string_single(self, *args): """Test a single address config'd by the HASS config file.""" @@ -201,7 +201,7 @@ class TestSonosMediaPlayer(unittest.TestCase): self.assertEqual(len(devices), 1) self.assertEqual(devices[0].name, 'Kitchen') - @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch('pysonos.SoCo', new=SoCoMock) @mock.patch('socket.create_connection', side_effect=socket.error()) def test_ensure_setup_config_hosts_string_multiple(self, *args): """Test multiple address string config'd by the HASS config file.""" @@ -218,7 +218,7 @@ class TestSonosMediaPlayer(unittest.TestCase): self.assertEqual(len(devices), 2) self.assertEqual(devices[0].name, 'Kitchen') - @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch('pysonos.SoCo', new=SoCoMock) @mock.patch('socket.create_connection', side_effect=socket.error()) def test_ensure_setup_config_hosts_list(self, *args): """Test a multiple address list config'd by the HASS config file.""" @@ -235,8 +235,8 @@ class TestSonosMediaPlayer(unittest.TestCase): self.assertEqual(len(devices), 2) self.assertEqual(devices[0].name, 'Kitchen') - @mock.patch('soco.SoCo', new=SoCoMock) - @mock.patch.object(soco, 'discover', new=socoDiscoverMock.discover) + @mock.patch('pysonos.SoCo', new=SoCoMock) + @mock.patch.object(pysonos, 'discover', new=pysonosDiscoverMock.discover) @mock.patch('socket.create_connection', side_effect=socket.error()) def test_ensure_setup_sonos_discovery(self, *args): """Test a single device using the autodiscovery provided by Sonos.""" @@ -245,11 +245,11 @@ class TestSonosMediaPlayer(unittest.TestCase): self.assertEqual(len(devices), 1) self.assertEqual(devices[0].name, 'Kitchen') - @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch('pysonos.SoCo', new=SoCoMock) @mock.patch('socket.create_connection', side_effect=socket.error()) @mock.patch.object(SoCoMock, 'set_sleep_timer') def test_sonos_set_sleep_timer(self, set_sleep_timerMock, *args): - """Ensuring soco methods called for sonos_set_sleep_timer service.""" + """Ensure pysonos methods called for sonos_set_sleep_timer service.""" sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { 'host': '192.0.2.1' }) @@ -259,11 +259,11 @@ class TestSonosMediaPlayer(unittest.TestCase): device.set_sleep_timer(30) set_sleep_timerMock.assert_called_once_with(30) - @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch('pysonos.SoCo', new=SoCoMock) @mock.patch('socket.create_connection', side_effect=socket.error()) @mock.patch.object(SoCoMock, 'set_sleep_timer') def test_sonos_clear_sleep_timer(self, set_sleep_timerMock, *args): - """Ensuring soco methods called for sonos_clear_sleep_timer service.""" + """Ensure pysonos method called for sonos_clear_sleep_timer service.""" sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { 'host': '192.0.2.1' }) @@ -273,20 +273,20 @@ class TestSonosMediaPlayer(unittest.TestCase): device.set_sleep_timer(None) set_sleep_timerMock.assert_called_once_with(None) - @mock.patch('soco.SoCo', new=SoCoMock) - @mock.patch('soco.alarms.Alarm') + @mock.patch('pysonos.SoCo', new=SoCoMock) + @mock.patch('pysonos.alarms.Alarm') @mock.patch('socket.create_connection', side_effect=socket.error()) - def test_set_alarm(self, soco_mock, alarm_mock, *args): - """Ensuring soco methods called for sonos_set_sleep_timer service.""" + def test_set_alarm(self, pysonos_mock, alarm_mock, *args): + """Ensure pysonos methods called for sonos_set_sleep_timer service.""" sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { 'host': '192.0.2.1' }) device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] device.hass = self.hass - alarm1 = alarms.Alarm(soco_mock) + alarm1 = alarms.Alarm(pysonos_mock) alarm1.configure_mock(_alarm_id="1", start_time=None, enabled=False, include_linked_zones=False, volume=100) - with mock.patch('soco.alarms.get_alarms', return_value=[alarm1]): + with mock.patch('pysonos.alarms.get_alarms', return_value=[alarm1]): attrs = { 'time': datetime.time(12, 00), 'enabled': True, @@ -303,11 +303,11 @@ class TestSonosMediaPlayer(unittest.TestCase): self.assertEqual(alarm1.volume, 30) alarm1.save.assert_called_once_with() - @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch('pysonos.SoCo', new=SoCoMock) @mock.patch('socket.create_connection', side_effect=socket.error()) - @mock.patch.object(soco.snapshot.Snapshot, 'snapshot') + @mock.patch.object(pysonos.snapshot.Snapshot, 'snapshot') def test_sonos_snapshot(self, snapshotMock, *args): - """Ensuring soco methods called for sonos_snapshot service.""" + """Ensure pysonos methods called for sonos_snapshot service.""" sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { 'host': '192.0.2.1' }) @@ -319,12 +319,12 @@ class TestSonosMediaPlayer(unittest.TestCase): self.assertEqual(snapshotMock.call_count, 1) self.assertEqual(snapshotMock.call_args, mock.call()) - @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch('pysonos.SoCo', new=SoCoMock) @mock.patch('socket.create_connection', side_effect=socket.error()) - @mock.patch.object(soco.snapshot.Snapshot, 'restore') + @mock.patch.object(pysonos.snapshot.Snapshot, 'restore') def test_sonos_restore(self, restoreMock, *args): - """Ensuring soco methods called for sonos_restor service.""" - from soco.snapshot import Snapshot + """Ensure pysonos methods called for sonos_restore service.""" + from pysonos.snapshot import Snapshot sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { 'host': '192.0.2.1' diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index ab4eed31fee..455ce6d4cc2 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/test_init.py @@ -12,7 +12,7 @@ async def test_creating_entry_sets_up_media_player(hass): """Test setting up Sonos loads the media player.""" with patch('homeassistant.components.media_player.sonos.async_setup_entry', return_value=mock_coro(True)) as mock_setup, \ - patch('soco.discover', return_value=True): + patch('pysonos.discover', return_value=True): result = await hass.config_entries.flow.async_init( sonos.DOMAIN, context={'source': config_entries.SOURCE_USER}) assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -26,7 +26,7 @@ async def test_configuring_sonos_creates_entry(hass): """Test that specifying config will create an entry.""" with patch('homeassistant.components.sonos.async_setup_entry', return_value=mock_coro(True)) as mock_setup, \ - patch('soco.discover', return_value=True): + patch('pysonos.discover', return_value=True): await async_setup_component(hass, sonos.DOMAIN, { 'sonos': { 'some_config': 'to_trigger_import' @@ -41,7 +41,7 @@ async def test_not_configuring_sonos_not_creates_entry(hass): """Test that no config will not create an entry.""" with patch('homeassistant.components.sonos.async_setup_entry', return_value=mock_coro(True)) as mock_setup, \ - patch('soco.discover', return_value=True): + patch('pysonos.discover', return_value=True): await async_setup_component(hass, sonos.DOMAIN, {}) await hass.async_block_till_done()