mirror of https://github.com/home-assistant/core
457 lines
17 KiB
Python
457 lines
17 KiB
Python
"""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
|
|
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.data_entry_flow import FlowResultType
|
|
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,
|
|
)
|
|
|
|
with patch(
|
|
"homeassistant.components.sonos.media_player.async_setup_entry",
|
|
) as mock_setup:
|
|
result = await hass.config_entries.flow.async_init(
|
|
sonos.DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
|
|
# Confirmation form
|
|
assert result["type"] is FlowResultType.FORM
|
|
|
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
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."""
|
|
with patch(
|
|
"homeassistant.components.sonos.async_setup_entry",
|
|
return_value=True,
|
|
) as mock_setup:
|
|
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."""
|
|
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 == "WARNING"
|
|
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
|
|
)
|
|
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
|
|
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
|
|
)
|
|
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
|
|
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
|
|
)
|
|
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
|
|
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
|
|
)
|
|
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
|
|
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
|
|
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
|
|
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.0, 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 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
|
|
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
|
|
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
|
|
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
|
|
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
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|