Sonos setup fails with unhandled exceptions on discovery messages (#90648)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
PeteRager 2023-05-30 11:09:13 -04:00 committed by GitHub
parent 11299c4537
commit 6a8d18ab35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 471 additions and 55 deletions

View File

@ -368,7 +368,9 @@ class SonosDiscoveryManager:
self, now: datetime.datetime | None = None self, now: datetime.datetime | None = None
) -> None: ) -> None:
"""Add and maintain Sonos devices from a manual configuration.""" """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) ip_addr = await self.hass.async_add_executor_job(socket.gethostbyname, host)
soco = SoCo(ip_addr) soco = SoCo(ip_addr)
try: try:
@ -376,7 +378,12 @@ class SonosDiscoveryManager:
sync_get_visible_zones, sync_get_visible_zones,
soco, soco,
) )
except (OSError, SoCoException, Timeout) as ex: except (
OSError,
SoCoException,
Timeout,
asyncio.TimeoutError,
) as ex:
if not self.hosts_in_error.get(ip_addr): if not self.hosts_in_error.get(ip_addr):
_LOGGER.warning( _LOGGER.warning(
"Could not get visible Sonos devices from %s: %s", ip_addr, ex "Could not get visible Sonos devices from %s: %s", ip_addr, ex
@ -386,31 +393,30 @@ class SonosDiscoveryManager:
_LOGGER.debug( _LOGGER.debug(
"Could not get visible Sonos devices from %s: %s", ip_addr, ex "Could not get visible Sonos devices from %s: %s", ip_addr, ex
) )
continue
else: if self.hosts_in_error.pop(ip_addr, None):
if self.hosts_in_error.pop(ip_addr, None): _LOGGER.info("Connection reestablished to Sonos device %s", ip_addr)
_LOGGER.info("Connection restablished to Sonos device %s", ip_addr) # Each speaker has the topology for other online speakers, so add them in here if they were not
if new_hosts := { # configured. The metadata is already in Soco for these.
x.ip_address if new_hosts := {
for x in visible_zones x.ip_address for x in visible_zones if x.ip_address not in self.hosts
if x.ip_address not in self.hosts }:
}: _LOGGER.debug("Adding to manual hosts: %s", new_hosts)
_LOGGER.debug("Adding to manual hosts: %s", new_hosts) self.hosts.update(new_hosts)
self.hosts.update(new_hosts)
async_dispatcher_send(
self.hass,
f"{SONOS_SPEAKER_ACTIVITY}-{soco.uid}",
"manual zone scan",
)
break
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): if self.is_device_invisible(ip_addr):
_LOGGER.debug("Discarding %s from manual hosts", ip_addr) _LOGGER.debug("Discarding %s from manual hosts", ip_addr)
self.hosts.discard(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( known_speaker = next(
( (
speaker speaker
@ -420,12 +426,28 @@ class SonosDiscoveryManager:
None, None,
) )
if not known_speaker: if not known_speaker:
await self._async_handle_discovery_message( try:
soco.uid, ip_addr, "manual zone scan" 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: elif not known_speaker.available:
try: try:
await self.hass.async_add_executor_job(known_speaker.ping) 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: except SonosUpdateError:
_LOGGER.debug( _LOGGER.debug(
"Manual poll to %s failed, keeping unavailable", ip_addr "Manual poll to %s failed, keeping unavailable", ip_addr

View File

@ -13,13 +13,33 @@ from homeassistant.const import CONF_HOSTS
from tests.common import MockConfigEntry, load_fixture 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: class SonosMockService:
"""Mock a Sonos Service used in callbacks.""" """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.""" """Initialize the instance."""
self.service_type = service_type self.service_type = service_type
self.subscribe = AsyncMock() self.subscribe = AsyncMock(return_value=SonosMockSubscribe(ip_address))
class SonosMockEvent: class SonosMockEvent:
@ -84,28 +104,59 @@ def config_entry_fixture():
return MockConfigEntry(domain=DOMAIN, title="Sonos") return MockConfigEntry(domain=DOMAIN, title="Sonos")
@pytest.fixture(name="soco") class MockSoCo(MagicMock):
def soco_fixture( """Mock the Soco Object."""
music_library, speaker_info, current_track_info_empty, battery_info, alarm_clock
): @property
"""Create a mock soco SoCo fixture.""" def visible_zones(self):
with patch("homeassistant.components.sonos.SoCo", autospec=True) as mock, patch( """Return visible zones and allow property to be overridden by device classes."""
"socket.gethostbyname", return_value="192.168.42.2" return {self}
):
mock_soco = mock.return_value
mock_soco.ip_address = "192.168.42.2" class SoCoMockFactory:
mock_soco.uid = "RINCON_test" """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.play_mode = "NORMAL"
mock_soco.music_library = music_library mock_soco.music_library = self.music_library
mock_soco.get_current_track_info.return_value = current_track_info_empty 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.music_source_from_uri = SoCo.music_source_from_uri
mock_soco.get_speaker_info.return_value = speaker_info my_speaker_info = self.speaker_info.copy()
mock_soco.avTransport = SonosMockService("AVTransport") my_speaker_info["zone_name"] = name
mock_soco.renderingControl = SonosMockService("RenderingControl") my_speaker_info["uid"] = mock_soco.uid
mock_soco.zoneGroupTopology = SonosMockService("ZoneGroupTopology") mock_soco.get_speaker_info = Mock(return_value=my_speaker_info)
mock_soco.contentDirectory = SonosMockService("ContentDirectory")
mock_soco.deviceProperties = SonosMockService("DeviceProperties") mock_soco.avTransport = SonosMockService("AVTransport", ip_address)
mock_soco.alarmClock = alarm_clock 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.mute = False
mock_soco.night_mode = True mock_soco.night_mode = True
mock_soco.dialog_level = True mock_soco.dialog_level = True
@ -123,11 +174,48 @@ def soco_fixture(
mock_soco.surround_level = 3 mock_soco.surround_level = 3
mock_soco.music_surround_level = 4 mock_soco.music_surround_level = 4
mock_soco.soundbar_audio_input_format = "Dolby 5.1" 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.all_zones = {mock_soco}
mock_soco.visible_zones = {mock_soco}
mock_soco.group.coordinator = 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) @pytest.fixture(autouse=True)
@ -172,7 +260,7 @@ def discover_fixture(soco):
@pytest.fixture(name="config") @pytest.fixture(name="config")
def config_fixture(): def config_fixture():
"""Create hass 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") @pytest.fixture(name="music_library")

View File

@ -1,16 +1,31 @@
"""Tests for the Sonos config flow.""" """Tests for the Sonos config flow."""
import asyncio
import logging 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 import pytest
from homeassistant import config_entries, data_entry_flow from homeassistant import config_entries, data_entry_flow
from homeassistant.components import sonos, zeroconf from homeassistant.components import sonos, zeroconf
from homeassistant.components.sonos import SonosDiscoveryManager from homeassistant.components.sonos import SonosDiscoveryManager
from homeassistant.components.sonos.const import DATA_SONOS_DISCOVERY_MANAGER from homeassistant.components.sonos.const import (
from homeassistant.core import HomeAssistant 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 homeassistant.setup import async_setup_component
from .conftest import MockSoCo, SoCoMockFactory
async def test_creating_entry_sets_up_media_player( async def test_creating_entry_sets_up_media_player(
hass: HomeAssistant, zeroconf_payload: zeroconf.ZeroconfServiceInfo 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") manager.hosts.add("10.10.10.10")
with caplog.at_level(logging.DEBUG), patch.object( with caplog.at_level(logging.DEBUG), patch.object(
manager, "_async_handle_discovery_message" 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" "homeassistant.components.sonos.async_dispatcher_send"
), patch( ), patch(
"homeassistant.components.sonos.sync_get_visible_zones", "homeassistant.components.sonos.sync_get_visible_zones",
@ -103,6 +120,7 @@ async def test_async_poll_manual_hosts_warnings(
record = caplog.records[0] record = caplog.records[0]
assert record.levelname == "WARNING" assert record.levelname == "WARNING"
assert "Could not get visible Sonos devices from" in record.message 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 # Second call fails again, it should be logged as a DEBUG message
caplog.clear() caplog.clear()
@ -111,6 +129,7 @@ async def test_async_poll_manual_hosts_warnings(
record = caplog.records[0] record = caplog.records[0]
assert record.levelname == "DEBUG" assert record.levelname == "DEBUG"
assert "Could not get visible Sonos devices from" in record.message 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 # Third call succeeds, it should log an info message
caplog.clear() caplog.clear()
@ -118,12 +137,14 @@ async def test_async_poll_manual_hosts_warnings(
assert len(caplog.messages) == 1 assert len(caplog.messages) == 1
record = caplog.records[0] record = caplog.records[0]
assert record.levelname == "INFO" 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 # Fourth call succeeds again, no need to log
caplog.clear() caplog.clear()
await manager.async_poll_manual_hosts() await manager.async_poll_manual_hosts()
assert len(caplog.messages) == 0 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 # Fifth call fail again again, should be logged as a WARNING message
caplog.clear() caplog.clear()
@ -132,3 +153,288 @@ async def test_async_poll_manual_hosts_warnings(
record = caplog.records[0] record = caplog.records[0]
assert record.levelname == "WARNING" assert record.levelname == "WARNING"
assert "Could not get visible Sonos devices from" in record.message 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