Use pysonos for Sonos media player (#16753)

pull/16761/head
Anders Melchiorsen 2018-09-20 23:50:11 +02:00 committed by Paulus Schoutsen
parent 092c146eae
commit 78b6439ee6
7 changed files with 67 additions and 67 deletions

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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',

View File

@ -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'

View File

@ -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()