Use pysonos for Sonos media player (#16753)
parent
092c146eae
commit
78b6439ee6
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
Loading…
Reference in New Issue