Skip to content

Commit

Permalink
Use pysonos for Sonos media player (#16753)
Browse files Browse the repository at this point in the history
  • Loading branch information
amelchio authored and balloob committed Sep 20, 2018
1 parent 092c146 commit 78b6439
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 67 deletions.
38 changes: 19 additions & 19 deletions homeassistant/components/media_player/sonos.py
Original file line number Diff line number Diff line change
@@ -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/
Expand Down Expand Up @@ -31,11 +31,11 @@

_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 |\
Expand Down Expand Up @@ -143,18 +143,18 @@ def add_entities(devices, update_before_add=False):

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:
Expand All @@ -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:
Expand Down Expand Up @@ -287,7 +287,7 @@ def decorator(funct):
@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
Expand Down Expand Up @@ -612,9 +612,9 @@ def update_media_radio(self, variables, track_info):
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')
Expand Down Expand Up @@ -950,7 +950,7 @@ def play_media(self, media_type, media_id, **kwargs):
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:
Expand Down Expand Up @@ -981,7 +981,7 @@ def unjoin(self):
@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()
Expand All @@ -996,7 +996,7 @@ def snapshot(self, with_group=True):
@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.
Expand Down Expand Up @@ -1060,7 +1060,7 @@ def clear_sleep_timer(self):
@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
Expand Down
6 changes: 3 additions & 3 deletions homeassistant/components/sonos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


DOMAIN = 'sonos'
REQUIREMENTS = ['SoCo==0.16']
REQUIREMENTS = ['pysonos==0.0.1']


async def async_setup(hass, config):
Expand All @@ -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(
Expand Down
6 changes: 3 additions & 3 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion script/gen_requirements_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
'pynx584',
'pyopenuv',
'pyotp',
'pysonos',
'pyqwikswitch',
'PyRMVtransport',
'python-forecastio',
Expand All @@ -92,7 +93,6 @@
'ring_doorbell',
'rxv',
'sleepyq',
'SoCo',
'somecomfort',
'sqlalchemy',
'statsd',
Expand Down
70 changes: 35 additions & 35 deletions tests/components/media_player/test_sonos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -17,16 +17,16 @@
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."""
Expand All @@ -41,18 +41,18 @@ def GetMediaInfo(self, _):


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."""
return []


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
Expand Down Expand Up @@ -153,7 +153,7 @@ def tearDown(self):
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."""
Expand All @@ -165,9 +165,9 @@ def test_ensure_setup_discovery(self, *args):
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')}
Expand All @@ -184,7 +184,7 @@ def test_ensure_setup_config_interface_addr(self, discover_mock, *args):
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."""
Expand All @@ -201,7 +201,7 @@ def test_ensure_setup_config_hosts_string_single(self, *args):
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."""
Expand All @@ -218,7 +218,7 @@ def test_ensure_setup_config_hosts_string_multiple(self, *args):
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."""
Expand All @@ -235,8 +235,8 @@ def test_ensure_setup_config_hosts_list(self, *args):
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."""
Expand All @@ -245,11 +245,11 @@ def test_ensure_setup_sonos_discovery(self, *args):
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'
})
Expand All @@ -259,11 +259,11 @@ def test_sonos_set_sleep_timer(self, set_sleep_timerMock, *args):
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'
})
Expand All @@ -273,20 +273,20 @@ def test_sonos_clear_sleep_timer(self, set_sleep_timerMock, *args):
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,
Expand All @@ -303,11 +303,11 @@ def test_set_alarm(self, soco_mock, alarm_mock, *args):
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'
})
Expand All @@ -319,12 +319,12 @@ def test_sonos_snapshot(self, snapshotMock, *args):
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'
Expand Down
Loading

0 comments on commit 78b6439

Please sign in to comment.