core/tests/components/sonos/test_init.py

442 lines
16 KiB
Python
Raw Normal View History

"""Tests for the Sonos config flow."""
import asyncio
from datetime import timedelta
import logging
from unittest.mock import Mock, patch
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,
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
import homeassistant.util.dt as dt_util
from .conftest import MockSoCo, SoCoMockFactory
from tests.common import async_fire_time_changed
async def test_creating_entry_sets_up_media_player(
hass: HomeAssistant, zeroconf_payload: zeroconf.ZeroconfServiceInfo
) -> None:
"""Test setting up Sonos loads the media player."""
# Initiate a discovery to allow a user config flow
await hass.config_entries.flow.async_init(
sonos.DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=zeroconf_payload,
)
2019-07-31 19:25:30 +00:00
with patch(
"homeassistant.components.sonos.media_player.async_setup_entry",
) as mock_setup:
result = await hass.config_entries.flow.async_init(
2019-07-31 19:25:30 +00:00
sonos.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
# Confirmation form
assert result["type"] == data_entry_flow.FlowResultType.FORM
2019-07-31 19:25:30 +00:00
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
async def test_configuring_sonos_creates_entry(hass: HomeAssistant) -> None:
"""Test that specifying config will create an entry."""
2019-07-31 19:25:30 +00:00
with patch(
"homeassistant.components.sonos.async_setup_entry",
return_value=True,
) as mock_setup:
2019-07-31 19:25:30 +00:00
await async_setup_component(
hass,
sonos.DOMAIN,
{"sonos": {"media_player": {"interface_addr": "127.0.0.1"}}},
)
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
async def test_not_configuring_sonos_not_creates_entry(hass: HomeAssistant) -> None:
"""Test that no config will not create an entry."""
2019-07-31 19:25:30 +00:00
with patch(
"homeassistant.components.sonos.async_setup_entry",
return_value=True,
) as mock_setup:
await async_setup_component(hass, sonos.DOMAIN, {})
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 0
async def test_async_poll_manual_hosts_warnings(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test that host warnings are not logged repeatedly."""
await async_setup_component(
hass,
sonos.DOMAIN,
{"sonos": {"media_player": {"interface_addr": "127.0.0.1"}}},
)
await hass.async_block_till_done()
manager: SonosDiscoveryManager = hass.data[DATA_SONOS_DISCOVERY_MANAGER]
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"
) as mock_async_call_later, patch(
"homeassistant.components.sonos.async_dispatcher_send"
), patch(
"homeassistant.components.sonos.sync_get_visible_zones",
side_effect=[
OSError(),
OSError(),
[],
[],
OSError(),
],
):
# First call fails, it should be logged as a WARNING message
caplog.clear()
await manager.async_poll_manual_hosts()
assert len(caplog.messages) == 1
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()
await manager.async_poll_manual_hosts()
assert len(caplog.messages) == 1
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()
await manager.async_poll_manual_hosts()
assert len(caplog.messages) == 1
record = caplog.records[0]
assert record.levelname == "INFO"
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()
await manager.async_poll_manual_hosts()
assert len(caplog.messages) == 1
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])
with caplog.at_level(logging.DEBUG):
caplog.clear()
await _setup_hass(hass)
assert "media_player.bedroom" in entity_registry.entities
assert "media_player.living_room" in entity_registry.entities
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=0.5))
await hass.async_block_till_done()
await asyncio.gather(
*[speaker_1_activity.event.wait(), speaker_2_activity.event.wait()]
)
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):
2023-08-15 12:34:18 +00:00
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