mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 09:17:53 +00:00
Sonos setup fails with unhandled exceptions on discovery messages (#90648)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
11299c4537
commit
6a8d18ab35
@ -368,7 +368,9 @@ class SonosDiscoveryManager:
|
||||
self, now: datetime.datetime | None = None
|
||||
) -> None:
|
||||
"""Add and maintain Sonos devices from a manual configuration."""
|
||||
for host in self.hosts:
|
||||
|
||||
# Loop through each configured host and verify that Soco attributes are available for it.
|
||||
for host in self.hosts.copy():
|
||||
ip_addr = await self.hass.async_add_executor_job(socket.gethostbyname, host)
|
||||
soco = SoCo(ip_addr)
|
||||
try:
|
||||
@ -376,7 +378,12 @@ class SonosDiscoveryManager:
|
||||
sync_get_visible_zones,
|
||||
soco,
|
||||
)
|
||||
except (OSError, SoCoException, Timeout) as ex:
|
||||
except (
|
||||
OSError,
|
||||
SoCoException,
|
||||
Timeout,
|
||||
asyncio.TimeoutError,
|
||||
) as ex:
|
||||
if not self.hosts_in_error.get(ip_addr):
|
||||
_LOGGER.warning(
|
||||
"Could not get visible Sonos devices from %s: %s", ip_addr, ex
|
||||
@ -386,31 +393,30 @@ class SonosDiscoveryManager:
|
||||
_LOGGER.debug(
|
||||
"Could not get visible Sonos devices from %s: %s", ip_addr, ex
|
||||
)
|
||||
continue
|
||||
|
||||
else:
|
||||
if self.hosts_in_error.pop(ip_addr, None):
|
||||
_LOGGER.info("Connection restablished to Sonos device %s", ip_addr)
|
||||
if new_hosts := {
|
||||
x.ip_address
|
||||
for x in visible_zones
|
||||
if x.ip_address not in self.hosts
|
||||
}:
|
||||
_LOGGER.debug("Adding to manual hosts: %s", new_hosts)
|
||||
self.hosts.update(new_hosts)
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
f"{SONOS_SPEAKER_ACTIVITY}-{soco.uid}",
|
||||
"manual zone scan",
|
||||
)
|
||||
break
|
||||
if self.hosts_in_error.pop(ip_addr, None):
|
||||
_LOGGER.info("Connection reestablished to Sonos device %s", ip_addr)
|
||||
# Each speaker has the topology for other online speakers, so add them in here if they were not
|
||||
# configured. The metadata is already in Soco for these.
|
||||
if new_hosts := {
|
||||
x.ip_address for x in visible_zones if x.ip_address not in self.hosts
|
||||
}:
|
||||
_LOGGER.debug("Adding to manual hosts: %s", new_hosts)
|
||||
self.hosts.update(new_hosts)
|
||||
|
||||
for host in self.hosts.copy():
|
||||
ip_addr = await self.hass.async_add_executor_job(socket.gethostbyname, host)
|
||||
if self.is_device_invisible(ip_addr):
|
||||
_LOGGER.debug("Discarding %s from manual hosts", ip_addr)
|
||||
self.hosts.discard(ip_addr)
|
||||
continue
|
||||
|
||||
# Loop through each configured host that is not in error. Send a discovery message
|
||||
# if a speaker does not already exist, or ping the speaker if it is unavailable.
|
||||
for host in self.hosts.copy():
|
||||
ip_addr = await self.hass.async_add_executor_job(socket.gethostbyname, host)
|
||||
soco = SoCo(ip_addr)
|
||||
# Skip hosts that are in error to avoid blocking call on soco.uuid in event loop
|
||||
if self.hosts_in_error.get(ip_addr):
|
||||
continue
|
||||
known_speaker = next(
|
||||
(
|
||||
speaker
|
||||
@ -420,12 +426,28 @@ class SonosDiscoveryManager:
|
||||
None,
|
||||
)
|
||||
if not known_speaker:
|
||||
await self._async_handle_discovery_message(
|
||||
soco.uid, ip_addr, "manual zone scan"
|
||||
)
|
||||
try:
|
||||
await self._async_handle_discovery_message(
|
||||
soco.uid,
|
||||
ip_addr,
|
||||
"manual zone scan",
|
||||
)
|
||||
except (
|
||||
OSError,
|
||||
SoCoException,
|
||||
Timeout,
|
||||
asyncio.TimeoutError,
|
||||
) as ex:
|
||||
_LOGGER.warning("Discovery message failed to %s : %s", ip_addr, ex)
|
||||
elif not known_speaker.available:
|
||||
try:
|
||||
await self.hass.async_add_executor_job(known_speaker.ping)
|
||||
# Only send the message if the ping was successful.
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
f"{SONOS_SPEAKER_ACTIVITY}-{soco.uid}",
|
||||
"manual zone scan",
|
||||
)
|
||||
except SonosUpdateError:
|
||||
_LOGGER.debug(
|
||||
"Manual poll to %s failed, keeping unavailable", ip_addr
|
||||
|
@ -13,13 +13,33 @@ from homeassistant.const import CONF_HOSTS
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
|
||||
class SonosMockEventListener:
|
||||
"""Mock the event listener."""
|
||||
|
||||
def __init__(self, ip_address: str) -> None:
|
||||
"""Initialize the mock event listener."""
|
||||
self.address = [ip_address, "8080"]
|
||||
|
||||
|
||||
class SonosMockSubscribe:
|
||||
"""Mock the subscription."""
|
||||
|
||||
def __init__(self, ip_address: str, *args, **kwargs) -> None:
|
||||
"""Initialize the mock subscriber."""
|
||||
self.event_listener = SonosMockEventListener(ip_address)
|
||||
self.service = Mock()
|
||||
|
||||
async def unsubscribe(self) -> None:
|
||||
"""Unsubscribe mock."""
|
||||
|
||||
|
||||
class SonosMockService:
|
||||
"""Mock a Sonos Service used in callbacks."""
|
||||
|
||||
def __init__(self, service_type):
|
||||
def __init__(self, service_type, ip_address="192.168.42.2") -> None:
|
||||
"""Initialize the instance."""
|
||||
self.service_type = service_type
|
||||
self.subscribe = AsyncMock()
|
||||
self.subscribe = AsyncMock(return_value=SonosMockSubscribe(ip_address))
|
||||
|
||||
|
||||
class SonosMockEvent:
|
||||
@ -84,28 +104,59 @@ def config_entry_fixture():
|
||||
return MockConfigEntry(domain=DOMAIN, title="Sonos")
|
||||
|
||||
|
||||
@pytest.fixture(name="soco")
|
||||
def soco_fixture(
|
||||
music_library, speaker_info, current_track_info_empty, battery_info, alarm_clock
|
||||
):
|
||||
"""Create a mock soco SoCo fixture."""
|
||||
with patch("homeassistant.components.sonos.SoCo", autospec=True) as mock, patch(
|
||||
"socket.gethostbyname", return_value="192.168.42.2"
|
||||
):
|
||||
mock_soco = mock.return_value
|
||||
mock_soco.ip_address = "192.168.42.2"
|
||||
mock_soco.uid = "RINCON_test"
|
||||
class MockSoCo(MagicMock):
|
||||
"""Mock the Soco Object."""
|
||||
|
||||
@property
|
||||
def visible_zones(self):
|
||||
"""Return visible zones and allow property to be overridden by device classes."""
|
||||
return {self}
|
||||
|
||||
|
||||
class SoCoMockFactory:
|
||||
"""Factory for creating SoCo Mocks."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
music_library,
|
||||
speaker_info,
|
||||
current_track_info_empty,
|
||||
battery_info,
|
||||
alarm_clock,
|
||||
) -> None:
|
||||
"""Initialize the mock factory."""
|
||||
self.mock_list: dict[str, MockSoCo] = {}
|
||||
self.music_library = music_library
|
||||
self.speaker_info = speaker_info
|
||||
self.current_track_info = current_track_info_empty
|
||||
self.battery_info = battery_info
|
||||
self.alarm_clock = alarm_clock
|
||||
|
||||
def cache_mock(
|
||||
self, mock_soco: MockSoCo, ip_address: str, name: str = "Zone A"
|
||||
) -> MockSoCo:
|
||||
"""Put a user created mock into the cache."""
|
||||
mock_soco.mock_add_spec(SoCo)
|
||||
mock_soco.ip_address = ip_address
|
||||
if ip_address != "192.168.42.2":
|
||||
mock_soco.uid = f"RINCON_test_{ip_address}"
|
||||
else:
|
||||
mock_soco.uid = "RINCON_test"
|
||||
mock_soco.play_mode = "NORMAL"
|
||||
mock_soco.music_library = music_library
|
||||
mock_soco.get_current_track_info.return_value = current_track_info_empty
|
||||
mock_soco.music_library = self.music_library
|
||||
mock_soco.get_current_track_info.return_value = self.current_track_info
|
||||
mock_soco.music_source_from_uri = SoCo.music_source_from_uri
|
||||
mock_soco.get_speaker_info.return_value = speaker_info
|
||||
mock_soco.avTransport = SonosMockService("AVTransport")
|
||||
mock_soco.renderingControl = SonosMockService("RenderingControl")
|
||||
mock_soco.zoneGroupTopology = SonosMockService("ZoneGroupTopology")
|
||||
mock_soco.contentDirectory = SonosMockService("ContentDirectory")
|
||||
mock_soco.deviceProperties = SonosMockService("DeviceProperties")
|
||||
mock_soco.alarmClock = alarm_clock
|
||||
my_speaker_info = self.speaker_info.copy()
|
||||
my_speaker_info["zone_name"] = name
|
||||
my_speaker_info["uid"] = mock_soco.uid
|
||||
mock_soco.get_speaker_info = Mock(return_value=my_speaker_info)
|
||||
|
||||
mock_soco.avTransport = SonosMockService("AVTransport", ip_address)
|
||||
mock_soco.renderingControl = SonosMockService("RenderingControl", ip_address)
|
||||
mock_soco.zoneGroupTopology = SonosMockService("ZoneGroupTopology", ip_address)
|
||||
mock_soco.contentDirectory = SonosMockService("ContentDirectory", ip_address)
|
||||
mock_soco.deviceProperties = SonosMockService("DeviceProperties", ip_address)
|
||||
mock_soco.alarmClock = self.alarm_clock
|
||||
mock_soco.mute = False
|
||||
mock_soco.night_mode = True
|
||||
mock_soco.dialog_level = True
|
||||
@ -123,11 +174,48 @@ def soco_fixture(
|
||||
mock_soco.surround_level = 3
|
||||
mock_soco.music_surround_level = 4
|
||||
mock_soco.soundbar_audio_input_format = "Dolby 5.1"
|
||||
mock_soco.get_battery_info.return_value = battery_info
|
||||
mock_soco.get_battery_info.return_value = self.battery_info
|
||||
mock_soco.all_zones = {mock_soco}
|
||||
mock_soco.visible_zones = {mock_soco}
|
||||
mock_soco.group.coordinator = mock_soco
|
||||
yield mock_soco
|
||||
self.mock_list[ip_address] = mock_soco
|
||||
return mock_soco
|
||||
|
||||
def get_mock(self, *args) -> SoCo:
|
||||
"""Return a mock."""
|
||||
if len(args) > 0:
|
||||
ip_address = args[0]
|
||||
else:
|
||||
ip_address = "192.168.42.2"
|
||||
if ip_address in self.mock_list:
|
||||
return self.mock_list[ip_address]
|
||||
mock_soco = MockSoCo(name=f"Soco Mock {ip_address}")
|
||||
self.cache_mock(mock_soco, ip_address)
|
||||
return mock_soco
|
||||
|
||||
|
||||
def patch_gethostbyname(host: str) -> str:
|
||||
"""Mock to return host name as ip address for testing."""
|
||||
return host
|
||||
|
||||
|
||||
@pytest.fixture(name="soco_factory")
|
||||
def soco_factory(
|
||||
music_library, speaker_info, current_track_info_empty, battery_info, alarm_clock
|
||||
):
|
||||
"""Create factory for instantiating SoCo mocks."""
|
||||
factory = SoCoMockFactory(
|
||||
music_library, speaker_info, current_track_info_empty, battery_info, alarm_clock
|
||||
)
|
||||
with patch("homeassistant.components.sonos.SoCo", new=factory.get_mock), patch(
|
||||
"socket.gethostbyname", side_effect=patch_gethostbyname
|
||||
), patch("homeassistant.components.sonos.ZGS_SUBSCRIPTION_TIMEOUT", 0):
|
||||
yield factory
|
||||
|
||||
|
||||
@pytest.fixture(name="soco")
|
||||
def soco_fixture(soco_factory):
|
||||
"""Create a default mock soco SoCo fixture."""
|
||||
return soco_factory.get_mock()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@ -172,7 +260,7 @@ def discover_fixture(soco):
|
||||
@pytest.fixture(name="config")
|
||||
def config_fixture():
|
||||
"""Create hass config fixture."""
|
||||
return {DOMAIN: {MP_DOMAIN: {CONF_HOSTS: ["192.168.42.1"]}}}
|
||||
return {DOMAIN: {MP_DOMAIN: {CONF_HOSTS: ["192.168.42.2"]}}}
|
||||
|
||||
|
||||
@pytest.fixture(name="music_library")
|
||||
|
@ -1,16 +1,31 @@
|
||||
"""Tests for the Sonos config flow."""
|
||||
import asyncio
|
||||
import logging
|
||||
from unittest.mock import patch
|
||||
import sys
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
from async_timeout import timeout as asyncio_timeout
|
||||
else:
|
||||
from asyncio import timeout as asyncio_timeout
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components import sonos, zeroconf
|
||||
from homeassistant.components.sonos import SonosDiscoveryManager
|
||||
from homeassistant.components.sonos.const import DATA_SONOS_DISCOVERY_MANAGER
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.sonos.const import (
|
||||
DATA_SONOS_DISCOVERY_MANAGER,
|
||||
SONOS_SPEAKER_ACTIVITY,
|
||||
)
|
||||
from homeassistant.components.sonos.exception import SonosUpdateError
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .conftest import MockSoCo, SoCoMockFactory
|
||||
|
||||
|
||||
async def test_creating_entry_sets_up_media_player(
|
||||
hass: HomeAssistant, zeroconf_payload: zeroconf.ZeroconfServiceInfo
|
||||
@ -84,7 +99,9 @@ async def test_async_poll_manual_hosts_warnings(
|
||||
manager.hosts.add("10.10.10.10")
|
||||
with caplog.at_level(logging.DEBUG), patch.object(
|
||||
manager, "_async_handle_discovery_message"
|
||||
), patch("homeassistant.components.sonos.async_call_later"), patch(
|
||||
), patch(
|
||||
"homeassistant.components.sonos.async_call_later"
|
||||
) as mock_async_call_later, patch(
|
||||
"homeassistant.components.sonos.async_dispatcher_send"
|
||||
), patch(
|
||||
"homeassistant.components.sonos.sync_get_visible_zones",
|
||||
@ -103,6 +120,7 @@ async def test_async_poll_manual_hosts_warnings(
|
||||
record = caplog.records[0]
|
||||
assert record.levelname == "WARNING"
|
||||
assert "Could not get visible Sonos devices from" in record.message
|
||||
assert mock_async_call_later.call_count == 1
|
||||
|
||||
# Second call fails again, it should be logged as a DEBUG message
|
||||
caplog.clear()
|
||||
@ -111,6 +129,7 @@ async def test_async_poll_manual_hosts_warnings(
|
||||
record = caplog.records[0]
|
||||
assert record.levelname == "DEBUG"
|
||||
assert "Could not get visible Sonos devices from" in record.message
|
||||
assert mock_async_call_later.call_count == 2
|
||||
|
||||
# Third call succeeds, it should log an info message
|
||||
caplog.clear()
|
||||
@ -118,12 +137,14 @@ async def test_async_poll_manual_hosts_warnings(
|
||||
assert len(caplog.messages) == 1
|
||||
record = caplog.records[0]
|
||||
assert record.levelname == "INFO"
|
||||
assert "Connection restablished to Sonos device" in record.message
|
||||
assert "Connection reestablished to Sonos device" in record.message
|
||||
assert mock_async_call_later.call_count == 3
|
||||
|
||||
# Fourth call succeeds again, no need to log
|
||||
caplog.clear()
|
||||
await manager.async_poll_manual_hosts()
|
||||
assert len(caplog.messages) == 0
|
||||
assert mock_async_call_later.call_count == 4
|
||||
|
||||
# Fifth call fail again again, should be logged as a WARNING message
|
||||
caplog.clear()
|
||||
@ -132,3 +153,288 @@ async def test_async_poll_manual_hosts_warnings(
|
||||
record = caplog.records[0]
|
||||
assert record.levelname == "WARNING"
|
||||
assert "Could not get visible Sonos devices from" in record.message
|
||||
assert mock_async_call_later.call_count == 5
|
||||
|
||||
|
||||
class _MockSoCoOsError(MockSoCo):
|
||||
@property
|
||||
def visible_zones(self):
|
||||
raise OSError()
|
||||
|
||||
|
||||
class _MockSoCoVisibleZones(MockSoCo):
|
||||
def set_visible_zones(self, visible_zones) -> None:
|
||||
"""Set visible zones."""
|
||||
self.vz_return = visible_zones # pylint: disable=attribute-defined-outside-init
|
||||
|
||||
@property
|
||||
def visible_zones(self):
|
||||
return self.vz_return
|
||||
|
||||
|
||||
async def _setup_hass(hass: HomeAssistant):
|
||||
await async_setup_component(
|
||||
hass,
|
||||
sonos.DOMAIN,
|
||||
{
|
||||
"sonos": {
|
||||
"media_player": {
|
||||
"interface_addr": "127.0.0.1",
|
||||
"hosts": ["10.10.10.1", "10.10.10.2"],
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_async_poll_manual_hosts_1(
|
||||
hass: HomeAssistant,
|
||||
soco_factory: SoCoMockFactory,
|
||||
entity_registry: er.EntityRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Tests first device fails, second device successful, speakers do not exist."""
|
||||
soco_1 = soco_factory.cache_mock(_MockSoCoOsError(), "10.10.10.1", "Living Room")
|
||||
soco_2 = soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom")
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
await _setup_hass(hass)
|
||||
assert "media_player.bedroom" in entity_registry.entities
|
||||
assert "media_player.living_room" not in entity_registry.entities
|
||||
assert (
|
||||
f"Could not get visible Sonos devices from {soco_1.ip_address}"
|
||||
in caplog.text
|
||||
)
|
||||
assert (
|
||||
f"Could not get visible Sonos devices from {soco_2.ip_address}"
|
||||
not in caplog.text
|
||||
)
|
||||
|
||||
|
||||
async def test_async_poll_manual_hosts_2(
|
||||
hass: HomeAssistant,
|
||||
soco_factory: SoCoMockFactory,
|
||||
entity_registry: er.EntityRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test first device success, second device fails, speakers do not exist."""
|
||||
soco_1 = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Living Room")
|
||||
soco_2 = soco_factory.cache_mock(_MockSoCoOsError(), "10.10.10.2", "Bedroom")
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
await _setup_hass(hass)
|
||||
assert "media_player.bedroom" not in entity_registry.entities
|
||||
assert "media_player.living_room" in entity_registry.entities
|
||||
assert (
|
||||
f"Could not get visible Sonos devices from {soco_1.ip_address}"
|
||||
not in caplog.text
|
||||
)
|
||||
assert (
|
||||
f"Could not get visible Sonos devices from {soco_2.ip_address}"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
|
||||
async def test_async_poll_manual_hosts_3(
|
||||
hass: HomeAssistant,
|
||||
soco_factory: SoCoMockFactory,
|
||||
entity_registry: er.EntityRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test both devices fail, speakers do not exist."""
|
||||
soco_1 = soco_factory.cache_mock(_MockSoCoOsError(), "10.10.10.1", "Living Room")
|
||||
soco_2 = soco_factory.cache_mock(_MockSoCoOsError(), "10.10.10.2", "Bedroom")
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
await _setup_hass(hass)
|
||||
assert "media_player.bedroom" not in entity_registry.entities
|
||||
assert "media_player.living_room" not in entity_registry.entities
|
||||
assert (
|
||||
f"Could not get visible Sonos devices from {soco_1.ip_address}"
|
||||
in caplog.text
|
||||
)
|
||||
assert (
|
||||
f"Could not get visible Sonos devices from {soco_2.ip_address}"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
|
||||
async def test_async_poll_manual_hosts_4(
|
||||
hass: HomeAssistant,
|
||||
soco_factory: SoCoMockFactory,
|
||||
entity_registry: er.EntityRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test both devices are successful, speakers do not exist."""
|
||||
soco_1 = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Living Room")
|
||||
soco_2 = soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom")
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
await _setup_hass(hass)
|
||||
assert "media_player.bedroom" in entity_registry.entities
|
||||
assert "media_player.living_room" in entity_registry.entities
|
||||
assert (
|
||||
f"Could not get visible Sonos devices from {soco_1.ip_address}"
|
||||
not in caplog.text
|
||||
)
|
||||
assert (
|
||||
f"Could not get visible Sonos devices from {soco_2.ip_address}"
|
||||
not in caplog.text
|
||||
)
|
||||
|
||||
|
||||
class SpeakerActivity:
|
||||
"""Unit test class to track speaker activity messages."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, soco: MockSoCo) -> None:
|
||||
"""Create the object from soco."""
|
||||
self.soco = soco
|
||||
self.hass = hass
|
||||
self.call_count: int = 0
|
||||
self.event = asyncio.Event()
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{SONOS_SPEAKER_ACTIVITY}-{self.soco.uid}",
|
||||
self.speaker_activity,
|
||||
)
|
||||
|
||||
@callback
|
||||
def speaker_activity(self, source: str) -> None:
|
||||
"""Track the last activity on this speaker, set availability and resubscribe."""
|
||||
if source == "manual zone scan":
|
||||
self.event.set()
|
||||
self.call_count += 1
|
||||
|
||||
|
||||
async def test_async_poll_manual_hosts_5(
|
||||
hass: HomeAssistant,
|
||||
soco_factory: SoCoMockFactory,
|
||||
entity_registry: er.EntityRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test both succeed, speakers exist and unavailable, ping succeeds."""
|
||||
soco_1 = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Living Room")
|
||||
soco_1.renderingControl = Mock()
|
||||
soco_1.renderingControl.GetVolume = Mock()
|
||||
speaker_1_activity = SpeakerActivity(hass, soco_1)
|
||||
soco_2 = soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom")
|
||||
soco_2.renderingControl = Mock()
|
||||
soco_2.renderingControl.GetVolume = Mock()
|
||||
speaker_2_activity = SpeakerActivity(hass, soco_2)
|
||||
with patch(
|
||||
"homeassistant.components.sonos.DISCOVERY_INTERVAL"
|
||||
) as mock_discovery_interval:
|
||||
# Speed up manual discovery interval so second iteration runs sooner
|
||||
mock_discovery_interval.total_seconds = Mock(side_effect=[0.5, 60])
|
||||
|
||||
await _setup_hass(hass)
|
||||
|
||||
assert "media_player.bedroom" in entity_registry.entities
|
||||
assert "media_player.living_room" in entity_registry.entities
|
||||
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
caplog.clear()
|
||||
await speaker_1_activity.event.wait()
|
||||
await speaker_2_activity.event.wait()
|
||||
await hass.async_block_till_done()
|
||||
assert speaker_1_activity.call_count == 1
|
||||
assert speaker_2_activity.call_count == 1
|
||||
assert "Activity on Living Room" in caplog.text
|
||||
assert "Activity on Bedroom" in caplog.text
|
||||
|
||||
|
||||
async def test_async_poll_manual_hosts_6(
|
||||
hass: HomeAssistant,
|
||||
soco_factory: SoCoMockFactory,
|
||||
entity_registry: er.EntityRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test both succeed, speakers exist and unavailable, pings fail."""
|
||||
soco_1 = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Living Room")
|
||||
# Rendering Control Get Volume is what speaker ping calls.
|
||||
soco_1.renderingControl = Mock()
|
||||
soco_1.renderingControl.GetVolume = Mock()
|
||||
soco_1.renderingControl.GetVolume.side_effect = SonosUpdateError()
|
||||
speaker_1_activity = SpeakerActivity(hass, soco_1)
|
||||
soco_2 = soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom")
|
||||
soco_2.renderingControl = Mock()
|
||||
soco_2.renderingControl.GetVolume = Mock()
|
||||
soco_2.renderingControl.GetVolume.side_effect = SonosUpdateError()
|
||||
speaker_2_activity = SpeakerActivity(hass, soco_2)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.sonos.DISCOVERY_INTERVAL"
|
||||
) as mock_discovery_interval:
|
||||
# Speed up manual discovery interval so second iteration runs sooner
|
||||
mock_discovery_interval.total_seconds = Mock(side_effect=[0.5, 60])
|
||||
await _setup_hass(hass)
|
||||
|
||||
assert "media_player.bedroom" in entity_registry.entities
|
||||
assert "media_player.living_room" in entity_registry.entities
|
||||
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
caplog.clear()
|
||||
# The discovery events should not fire, wait with a timeout.
|
||||
with pytest.raises(asyncio.TimeoutError):
|
||||
async with asyncio_timeout(1.0):
|
||||
await speaker_1_activity.event.wait()
|
||||
await hass.async_block_till_done()
|
||||
assert "Activity on Living Room" not in caplog.text
|
||||
assert "Activity on Bedroom" not in caplog.text
|
||||
assert speaker_1_activity.call_count == 0
|
||||
assert speaker_2_activity.call_count == 0
|
||||
|
||||
|
||||
async def test_async_poll_manual_hosts_7(
|
||||
hass: HomeAssistant,
|
||||
soco_factory: SoCoMockFactory,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test both succeed, speaker do not exist, new hosts found in visible zones."""
|
||||
soco_1 = soco_factory.cache_mock(
|
||||
_MockSoCoVisibleZones(), "10.10.10.1", "Living Room"
|
||||
)
|
||||
soco_2 = soco_factory.cache_mock(_MockSoCoVisibleZones(), "10.10.10.2", "Bedroom")
|
||||
soco_3 = soco_factory.cache_mock(MockSoCo(), "10.10.10.3", "Basement")
|
||||
soco_4 = soco_factory.cache_mock(MockSoCo(), "10.10.10.4", "Garage")
|
||||
soco_5 = soco_factory.cache_mock(MockSoCo(), "10.10.10.5", "Studio")
|
||||
|
||||
soco_1.set_visible_zones({soco_1, soco_2, soco_3, soco_4, soco_5})
|
||||
soco_2.set_visible_zones({soco_1, soco_2, soco_3, soco_4, soco_5})
|
||||
|
||||
await _setup_hass(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert "media_player.bedroom" in entity_registry.entities
|
||||
assert "media_player.living_room" in entity_registry.entities
|
||||
assert "media_player.basement" in entity_registry.entities
|
||||
assert "media_player.garage" in entity_registry.entities
|
||||
assert "media_player.studio" in entity_registry.entities
|
||||
|
||||
|
||||
async def test_async_poll_manual_hosts_8(
|
||||
hass: HomeAssistant,
|
||||
soco_factory: SoCoMockFactory,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test both succeed, speaker do not exist, invisible zone."""
|
||||
soco_1 = soco_factory.cache_mock(
|
||||
_MockSoCoVisibleZones(), "10.10.10.1", "Living Room"
|
||||
)
|
||||
soco_2 = soco_factory.cache_mock(_MockSoCoVisibleZones(), "10.10.10.2", "Bedroom")
|
||||
soco_3 = soco_factory.cache_mock(MockSoCo(), "10.10.10.3", "Basement")
|
||||
soco_4 = soco_factory.cache_mock(MockSoCo(), "10.10.10.4", "Garage")
|
||||
soco_5 = soco_factory.cache_mock(MockSoCo(), "10.10.10.5", "Studio")
|
||||
|
||||
soco_1.set_visible_zones({soco_2, soco_3, soco_4, soco_5})
|
||||
soco_2.set_visible_zones({soco_2, soco_3, soco_4, soco_5})
|
||||
|
||||
await _setup_hass(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert "media_player.bedroom" in entity_registry.entities
|
||||
assert "media_player.living_room" not in entity_registry.entities
|
||||
assert "media_player.basement" in entity_registry.entities
|
||||
assert "media_player.garage" in entity_registry.entities
|
||||
assert "media_player.studio" in entity_registry.entities
|
||||
|
Loading…
x
Reference in New Issue
Block a user