Add Sonos discovery of multiple households (#21337)

* Remove confusing device naming

* Add discovery of multiple households

* Rename SonosDevice to SonosEntity
This commit is contained in:
Anders Melchiorsen 2019-02-24 18:45:08 +01:00 committed by GitHub
parent 47220d71a1
commit a4bb35142c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 89 additions and 88 deletions

View File

@ -4,7 +4,7 @@ from homeassistant.helpers import config_entry_flow
DOMAIN = 'sonos'
REQUIREMENTS = ['pysonos==0.0.6']
REQUIREMENTS = ['pysonos==0.0.7']
async def async_setup(hass, config):

View File

@ -48,7 +48,7 @@ SERVICE_CLEAR_TIMER = 'sonos_clear_sleep_timer'
SERVICE_UPDATE_ALARM = 'sonos_update_alarm'
SERVICE_SET_OPTION = 'sonos_set_option'
DATA_SONOS = 'sonos_devices'
DATA_SONOS = 'sonos_media_player'
SOURCE_LINEIN = 'Line-in'
SOURCE_TV = 'TV'
@ -114,7 +114,7 @@ class SonosData:
def __init__(self):
"""Initialize the data."""
self.uids = set()
self.devices = []
self.entities = []
self.topology_lock = threading.Lock()
@ -129,9 +129,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Sonos from a config entry."""
def add_entities(devices, update_before_add=False):
"""Sync version of async add devices."""
hass.add_job(async_add_entities, devices, update_before_add)
def add_entities(entities, update_before_add=False):
"""Sync version of async add entities."""
hass.add_job(async_add_entities, entities, update_before_add)
hass.async_add_executor_job(
_setup_platform, hass, hass.data[SONOS_DOMAIN].get('media_player', {}),
@ -153,7 +153,7 @@ def _setup_platform(hass, config, add_entities, discovery_info):
if discovery_info:
player = pysonos.SoCo(discovery_info.get('host'))
# If device already exists by config
# If host already exists by config
if player.uid in hass.data[DATA_SONOS].uids:
return
@ -176,53 +176,54 @@ def _setup_platform(hass, config, add_entities, discovery_info):
_LOGGER.warning("Failed to initialize '%s'", host)
else:
players = pysonos.discover(
interface_addr=config.get(CONF_INTERFACE_ADDR))
interface_addr=config.get(CONF_INTERFACE_ADDR),
all_households=True)
if not players:
_LOGGER.warning("No Sonos speakers found")
return
hass.data[DATA_SONOS].uids.update(p.uid for p in players)
add_entities(SonosDevice(p) for p in players)
add_entities(SonosEntity(p) for p in players)
_LOGGER.debug("Added %s Sonos speakers", len(players))
def service_handle(service):
"""Handle for services."""
entity_ids = service.data.get('entity_id')
devices = hass.data[DATA_SONOS].devices
entities = hass.data[DATA_SONOS].entities
if entity_ids:
devices = [d for d in devices if d.entity_id in entity_ids]
entities = [e for e in entities if e.entity_id in entity_ids]
if service.service == SERVICE_JOIN:
master = [device for device in hass.data[DATA_SONOS].devices
if device.entity_id == service.data[ATTR_MASTER]]
master = [e for e in hass.data[DATA_SONOS].entities
if e.entity_id == service.data[ATTR_MASTER]]
if master:
with hass.data[DATA_SONOS].topology_lock:
master[0].join(devices)
master[0].join(entities)
return
if service.service == SERVICE_UNJOIN:
with hass.data[DATA_SONOS].topology_lock:
for device in devices:
device.unjoin()
for entity in entities:
entity.unjoin()
return
for device in devices:
for entity in entities:
if service.service == SERVICE_SNAPSHOT:
device.snapshot(service.data[ATTR_WITH_GROUP])
entity.snapshot(service.data[ATTR_WITH_GROUP])
elif service.service == SERVICE_RESTORE:
device.restore(service.data[ATTR_WITH_GROUP])
entity.restore(service.data[ATTR_WITH_GROUP])
elif service.service == SERVICE_SET_TIMER:
device.set_sleep_timer(service.data[ATTR_SLEEP_TIME])
entity.set_sleep_timer(service.data[ATTR_SLEEP_TIME])
elif service.service == SERVICE_CLEAR_TIMER:
device.clear_sleep_timer()
entity.clear_sleep_timer()
elif service.service == SERVICE_UPDATE_ALARM:
device.set_alarm(**service.data)
entity.set_alarm(**service.data)
elif service.service == SERVICE_SET_OPTION:
device.set_option(**service.data)
entity.set_option(**service.data)
device.schedule_update_ha_state(True)
entity.schedule_update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_JOIN, service_handle,
@ -270,9 +271,9 @@ class _ProcessSonosEventQueue:
def _get_entity_from_soco_uid(hass, uid):
"""Return SonosDevice from SoCo uid."""
for entity in hass.data[DATA_SONOS].devices:
if uid == entity.soco.uid:
"""Return SonosEntity from SoCo uid."""
for entity in hass.data[DATA_SONOS].entities:
if uid == entity.unique_id:
return entity
return None
@ -303,11 +304,11 @@ def soco_error(errorcodes=None):
def soco_coordinator(funct):
"""Call function on coordinator."""
@ft.wraps(funct)
def wrapper(device, *args, **kwargs):
def wrapper(entity, *args, **kwargs):
"""Wrap for call to coordinator."""
if device.is_coordinator:
return funct(device, *args, **kwargs)
return funct(device.coordinator, *args, **kwargs)
if entity.is_coordinator:
return funct(entity, *args, **kwargs)
return funct(entity.coordinator, *args, **kwargs)
return wrapper
@ -329,11 +330,11 @@ def _is_radio_uri(uri):
return uri.startswith(radio_schemes)
class SonosDevice(MediaPlayerDevice):
"""Representation of a Sonos device."""
class SonosEntity(MediaPlayerDevice):
"""Representation of a Sonos entity."""
def __init__(self, player):
"""Initialize the Sonos device."""
"""Initialize the Sonos entity."""
self._subscriptions = []
self._receives_events = False
self._volume_increment = 2
@ -366,7 +367,7 @@ class SonosDevice(MediaPlayerDevice):
async def async_added_to_hass(self):
"""Subscribe sonos events."""
self.hass.data[DATA_SONOS].devices.append(self)
self.hass.data[DATA_SONOS].entities.append(self)
self.hass.async_add_executor_job(self._subscribe_to_player_events)
@property
@ -376,7 +377,7 @@ class SonosDevice(MediaPlayerDevice):
@property
def name(self):
"""Return the name of the device."""
"""Return the name of the entity."""
return self._name
@property
@ -394,7 +395,7 @@ class SonosDevice(MediaPlayerDevice):
@property
@soco_coordinator
def state(self):
"""Return the state of the device."""
"""Return the state of the entity."""
if self._status in ('PAUSED_PLAYBACK', 'STOPPED'):
return STATE_PAUSED
if self._status in ('PLAYING', 'TRANSITIONING'):
@ -410,7 +411,7 @@ class SonosDevice(MediaPlayerDevice):
@property
def soco(self):
"""Return soco device."""
"""Return soco object."""
return self._player
@property
@ -434,7 +435,7 @@ class SonosDevice(MediaPlayerDevice):
return False
def _set_basic_information(self):
"""Set initial device information."""
"""Set initial entity information."""
speaker_info = self.soco.get_speaker_info(True)
self._name = speaker_info['zone_name']
self._model = speaker_info['model_name']
@ -477,8 +478,8 @@ class SonosDevice(MediaPlayerDevice):
self._receives_events = False
# New player available, build the current group topology
for device in self.hass.data[DATA_SONOS].devices:
device.update_groups()
for entity in self.hass.data[DATA_SONOS].entities:
entity.update_groups()
player = self.soco
@ -554,7 +555,7 @@ class SonosDevice(MediaPlayerDevice):
self.schedule_update_ha_state()
# Also update slaves
for entity in self.hass.data[DATA_SONOS].devices:
for entity in self.hass.data[DATA_SONOS].entities:
coordinator = entity.coordinator
if coordinator and coordinator.unique_id == self.unique_id:
entity.schedule_update_ha_state()
@ -1087,7 +1088,7 @@ class SonosDevice(MediaPlayerDevice):
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
"""Return entity specific state attributes."""
attributes = {ATTR_SONOS_GROUP: self._sonos_group}
if self._night_sound is not None:

View File

@ -1263,7 +1263,7 @@ pysmartthings==0.6.3
pysnmp==4.4.8
# homeassistant.components.sonos
pysonos==0.0.6
pysonos==0.0.7
# homeassistant.components.spc
pyspcwebgw==0.4.0

View File

@ -226,7 +226,7 @@ pysmartapp==0.3.0
pysmartthings==0.6.3
# homeassistant.components.sonos
pysonos==0.0.6
pysonos==0.0.7
# homeassistant.components.spc
pyspcwebgw==0.4.0

View File

@ -21,7 +21,7 @@ ENTITY_ID = 'media_player.kitchen'
class pysonosDiscoverMock():
"""Mock class for the pysonos.discover method."""
def discover(interface_addr):
def discover(interface_addr, all_households=False):
"""Return tuple of pysonos.SoCo objects representing found speakers."""
return {SoCoMock('192.0.2.1')}
@ -123,10 +123,10 @@ class SoCoMock():
def add_entities_factory(hass):
"""Add devices factory."""
def add_entities(devices, update_befor_add=False):
"""Fake add device."""
hass.data[sonos.DATA_SONOS].devices = devices
"""Add entities factory."""
def add_entities(entities, update_befor_add=False):
"""Fake add entity."""
hass.data[sonos.DATA_SONOS].entities = entities
return add_entities
@ -144,14 +144,14 @@ class TestSonosMediaPlayer(unittest.TestCase):
return True
# Monkey patches
self.real_available = sonos.SonosDevice.available
sonos.SonosDevice.available = monkey_available
self.real_available = sonos.SonosEntity.available
sonos.SonosEntity.available = monkey_available
# pylint: disable=invalid-name
def tearDown(self):
"""Stop everything that was started."""
# Monkey patches
sonos.SonosDevice.available = self.real_available
sonos.SonosEntity.available = self.real_available
self.hass.stop()
@mock.patch('pysonos.SoCo', new=SoCoMock)
@ -162,9 +162,9 @@ class TestSonosMediaPlayer(unittest.TestCase):
'host': '192.0.2.1'
})
devices = list(self.hass.data[sonos.DATA_SONOS].devices)
assert len(devices) == 1
assert devices[0].name == 'Kitchen'
entities = list(self.hass.data[sonos.DATA_SONOS].entities)
assert len(entities) == 1
assert entities[0].name == 'Kitchen'
@mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
@ -182,7 +182,7 @@ class TestSonosMediaPlayer(unittest.TestCase):
assert setup_component(self.hass, DOMAIN, config)
assert len(self.hass.data[sonos.DATA_SONOS].devices) == 1
assert len(self.hass.data[sonos.DATA_SONOS].entities) == 1
assert discover_mock.call_count == 1
@mock.patch('pysonos.SoCo', new=SoCoMock)
@ -198,9 +198,9 @@ class TestSonosMediaPlayer(unittest.TestCase):
assert setup_component(self.hass, DOMAIN, config)
devices = self.hass.data[sonos.DATA_SONOS].devices
assert len(devices) == 1
assert devices[0].name == 'Kitchen'
entities = self.hass.data[sonos.DATA_SONOS].entities
assert len(entities) == 1
assert entities[0].name == 'Kitchen'
@mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
@ -215,9 +215,9 @@ class TestSonosMediaPlayer(unittest.TestCase):
assert setup_component(self.hass, DOMAIN, config)
devices = self.hass.data[sonos.DATA_SONOS].devices
assert len(devices) == 2
assert devices[0].name == 'Kitchen'
entities = self.hass.data[sonos.DATA_SONOS].entities
assert len(entities) == 2
assert entities[0].name == 'Kitchen'
@mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
@ -232,9 +232,9 @@ class TestSonosMediaPlayer(unittest.TestCase):
assert setup_component(self.hass, DOMAIN, config)
devices = self.hass.data[sonos.DATA_SONOS].devices
assert len(devices) == 2
assert devices[0].name == 'Kitchen'
entities = self.hass.data[sonos.DATA_SONOS].entities
assert len(entities) == 2
assert entities[0].name == 'Kitchen'
@mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch.object(pysonos, 'discover', new=pysonosDiscoverMock.discover)
@ -242,9 +242,9 @@ class TestSonosMediaPlayer(unittest.TestCase):
def test_ensure_setup_sonos_discovery(self, *args):
"""Test a single device using the autodiscovery provided by Sonos."""
sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass))
devices = list(self.hass.data[sonos.DATA_SONOS].devices)
assert len(devices) == 1
assert devices[0].name == 'Kitchen'
entities = list(self.hass.data[sonos.DATA_SONOS].entities)
assert len(entities) == 1
assert entities[0].name == 'Kitchen'
@mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
@ -254,10 +254,10 @@ class TestSonosMediaPlayer(unittest.TestCase):
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
entity = list(self.hass.data[sonos.DATA_SONOS].entities)[-1]
entity.hass = self.hass
device.set_sleep_timer(30)
entity.set_sleep_timer(30)
set_sleep_timerMock.assert_called_once_with(30)
@mock.patch('pysonos.SoCo', new=SoCoMock)
@ -268,10 +268,10 @@ class TestSonosMediaPlayer(unittest.TestCase):
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
entity = list(self.hass.data[sonos.DATA_SONOS].entities)[-1]
entity.hass = self.hass
device.set_sleep_timer(None)
entity.set_sleep_timer(None)
set_sleep_timerMock.assert_called_once_with(None)
@mock.patch('pysonos.SoCo', new=SoCoMock)
@ -282,8 +282,8 @@ class TestSonosMediaPlayer(unittest.TestCase):
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
entity = list(self.hass.data[sonos.DATA_SONOS].entities)[-1]
entity.hass = self.hass
alarm1 = alarms.Alarm(pysonos_mock)
alarm1.configure_mock(_alarm_id="1", start_time=None, enabled=False,
include_linked_zones=False, volume=100)
@ -294,9 +294,9 @@ class TestSonosMediaPlayer(unittest.TestCase):
'include_linked_zones': True,
'volume': 0.30,
}
device.set_alarm(alarm_id=2)
entity.set_alarm(alarm_id=2)
alarm1.save.assert_not_called()
device.set_alarm(alarm_id=1, **attrs)
entity.set_alarm(alarm_id=1, **attrs)
assert alarm1.enabled == attrs['enabled']
assert alarm1.start_time == attrs['time']
assert alarm1.include_linked_zones == \
@ -312,11 +312,11 @@ class TestSonosMediaPlayer(unittest.TestCase):
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
entity = list(self.hass.data[sonos.DATA_SONOS].entities)[-1]
entity.hass = self.hass
snapshotMock.return_value = True
device.snapshot()
entity.snapshot()
assert snapshotMock.call_count == 1
assert snapshotMock.call_args == mock.call()
@ -330,13 +330,13 @@ class TestSonosMediaPlayer(unittest.TestCase):
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
entity = list(self.hass.data[sonos.DATA_SONOS].entities)[-1]
entity.hass = self.hass
restoreMock.return_value = True
device._snapshot_coordinator = mock.MagicMock()
device._snapshot_coordinator.soco_device = SoCoMock('192.0.2.17')
device._soco_snapshot = Snapshot(device._player)
device.restore()
entity._snapshot_coordinator = mock.MagicMock()
entity._snapshot_coordinator.soco_entity = SoCoMock('192.0.2.17')
entity._soco_snapshot = Snapshot(entity._player)
entity.restore()
assert restoreMock.call_count == 1
assert restoreMock.call_args == mock.call(False)