mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Continuous discovery of Sonos speakers (#23484)
This commit is contained in:
parent
5529bcc114
commit
0ecf152153
@ -4,6 +4,7 @@ import datetime
|
|||||||
import functools as ft
|
import functools as ft
|
||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
|
import time
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
import async_timeout
|
import async_timeout
|
||||||
@ -35,6 +36,8 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
DISCOVERY_INTERVAL = 60
|
||||||
|
|
||||||
# Quiet down pysonos logging to just actual problems.
|
# Quiet down pysonos logging to just actual problems.
|
||||||
logging.getLogger('pysonos').setLevel(logging.WARNING)
|
logging.getLogger('pysonos').setLevel(logging.WARNING)
|
||||||
logging.getLogger('pysonos.data_structures_entry').setLevel(logging.ERROR)
|
logging.getLogger('pysonos.data_structures_entry').setLevel(logging.ERROR)
|
||||||
@ -109,7 +112,6 @@ class SonosData:
|
|||||||
|
|
||||||
def __init__(self, hass):
|
def __init__(self, hass):
|
||||||
"""Initialize the data."""
|
"""Initialize the data."""
|
||||||
self.uids = set()
|
|
||||||
self.entities = []
|
self.entities = []
|
||||||
self.topology_condition = asyncio.Condition(loop=hass.loop)
|
self.topology_condition = asyncio.Condition(loop=hass.loop)
|
||||||
|
|
||||||
@ -134,32 +136,41 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||||||
if advertise_addr:
|
if advertise_addr:
|
||||||
pysonos.config.EVENT_ADVERTISE_IP = advertise_addr
|
pysonos.config.EVENT_ADVERTISE_IP = advertise_addr
|
||||||
|
|
||||||
def _create_sonos_entities():
|
def _discovery(now=None):
|
||||||
"""Discover players and return a list of SonosEntity objects."""
|
"""Discover players from network or configuration."""
|
||||||
players = []
|
|
||||||
hosts = config.get(CONF_HOSTS)
|
hosts = config.get(CONF_HOSTS)
|
||||||
|
|
||||||
|
def _discovered_player(soco):
|
||||||
|
"""Handle a (re)discovered player."""
|
||||||
|
try:
|
||||||
|
# Make sure that the player is available
|
||||||
|
_ = soco.volume
|
||||||
|
|
||||||
|
entity = _get_entity_from_soco_uid(hass, soco.uid)
|
||||||
|
if not entity:
|
||||||
|
hass.add_job(async_add_entities, [SonosEntity(soco)])
|
||||||
|
else:
|
||||||
|
entity.seen()
|
||||||
|
except SoCoException:
|
||||||
|
pass
|
||||||
|
|
||||||
if hosts:
|
if hosts:
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
try:
|
try:
|
||||||
players.append(pysonos.SoCo(socket.gethostbyname(host)))
|
player = pysonos.SoCo(socket.gethostbyname(host))
|
||||||
|
if player.is_visible:
|
||||||
|
_discovered_player(player)
|
||||||
except (OSError, SoCoException):
|
except (OSError, SoCoException):
|
||||||
|
if now is None:
|
||||||
_LOGGER.warning("Failed to initialize '%s'", host)
|
_LOGGER.warning("Failed to initialize '%s'", host)
|
||||||
else:
|
else:
|
||||||
players = pysonos.discover(
|
pysonos.discover_thread(
|
||||||
interface_addr=config.get(CONF_INTERFACE_ADDR),
|
_discovered_player,
|
||||||
all_households=True)
|
interface_addr=config.get(CONF_INTERFACE_ADDR))
|
||||||
|
|
||||||
if not players:
|
hass.helpers.event.call_later(DISCOVERY_INTERVAL, _discovery)
|
||||||
_LOGGER.warning("No Sonos speakers found")
|
|
||||||
|
|
||||||
return [SonosEntity(p) for p in players]
|
hass.async_add_executor_job(_discovery)
|
||||||
|
|
||||||
entities = await hass.async_add_executor_job(_create_sonos_entities)
|
|
||||||
hass.data[DATA_SONOS].uids.update(e.unique_id for e in entities)
|
|
||||||
|
|
||||||
async_add_entities(entities)
|
|
||||||
_LOGGER.debug("Added %s Sonos speakers", len(entities))
|
|
||||||
|
|
||||||
def _service_to_entities(service):
|
def _service_to_entities(service):
|
||||||
"""Extract and return entities from service call."""
|
"""Extract and return entities from service call."""
|
||||||
@ -309,6 +320,7 @@ class SonosEntity(MediaPlayerDevice):
|
|||||||
|
|
||||||
def __init__(self, player):
|
def __init__(self, player):
|
||||||
"""Initialize the Sonos entity."""
|
"""Initialize the Sonos entity."""
|
||||||
|
self._seen = None
|
||||||
self._subscriptions = []
|
self._subscriptions = []
|
||||||
self._receives_events = False
|
self._receives_events = False
|
||||||
self._volume_increment = 2
|
self._volume_increment = 2
|
||||||
@ -338,6 +350,7 @@ class SonosEntity(MediaPlayerDevice):
|
|||||||
self._snapshot_group = None
|
self._snapshot_group = None
|
||||||
|
|
||||||
self._set_basic_information()
|
self._set_basic_information()
|
||||||
|
self.seen()
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Subscribe sonos events."""
|
"""Subscribe sonos events."""
|
||||||
@ -397,20 +410,18 @@ class SonosEntity(MediaPlayerDevice):
|
|||||||
"""Return coordinator of this player."""
|
"""Return coordinator of this player."""
|
||||||
return self._coordinator
|
return self._coordinator
|
||||||
|
|
||||||
|
def seen(self):
|
||||||
|
"""Record that this player was seen right now."""
|
||||||
|
self._seen = time.monotonic()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
return self._available
|
return self._available
|
||||||
|
|
||||||
def _check_available(self):
|
def _check_available(self):
|
||||||
"""Check that we can still connect to the player."""
|
"""Check that we saw the player recently."""
|
||||||
try:
|
return self._seen > time.monotonic() - 2*DISCOVERY_INTERVAL
|
||||||
sock = socket.create_connection(
|
|
||||||
address=(self.soco.ip_address, 1443), timeout=3)
|
|
||||||
sock.close()
|
|
||||||
return True
|
|
||||||
except socket.error:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _set_basic_information(self):
|
def _set_basic_information(self):
|
||||||
"""Set initial entity information."""
|
"""Set initial entity information."""
|
||||||
|
@ -35,8 +35,10 @@ def soco_fixture(music_library, speaker_info, dummy_soco_service):
|
|||||||
@pytest.fixture(name="discover")
|
@pytest.fixture(name="discover")
|
||||||
def discover_fixture(soco):
|
def discover_fixture(soco):
|
||||||
"""Create a mock pysonos discover fixture."""
|
"""Create a mock pysonos discover fixture."""
|
||||||
with patch('pysonos.discover') as mock:
|
def do_callback(callback, **kwargs):
|
||||||
mock.return_value = {soco}
|
callback(soco)
|
||||||
|
|
||||||
|
with patch('pysonos.discover_thread', side_effect=do_callback) as mock:
|
||||||
yield mock
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,10 +13,14 @@ async def setup_platform(hass, config_entry, config):
|
|||||||
async def test_async_setup_entry_hosts(hass, config_entry, config, soco):
|
async def test_async_setup_entry_hosts(hass, config_entry, config, soco):
|
||||||
"""Test static setup."""
|
"""Test static setup."""
|
||||||
await setup_platform(hass, config_entry, config)
|
await setup_platform(hass, config_entry, config)
|
||||||
assert hass.data[media_player.DATA_SONOS].entities[0].soco == soco
|
|
||||||
|
entity = hass.data[media_player.DATA_SONOS].entities[0]
|
||||||
|
assert entity.soco == soco
|
||||||
|
|
||||||
|
|
||||||
async def test_async_setup_entry_discover(hass, config_entry, discover):
|
async def test_async_setup_entry_discover(hass, config_entry, discover):
|
||||||
"""Test discovery setup."""
|
"""Test discovery setup."""
|
||||||
await setup_platform(hass, config_entry, {})
|
await setup_platform(hass, config_entry, {})
|
||||||
assert hass.data[media_player.DATA_SONOS].uids == {'RINCON_test'}
|
|
||||||
|
entity = hass.data[media_player.DATA_SONOS].entities[0]
|
||||||
|
assert entity.unique_id == 'RINCON_test'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user