Add tests for Sonos Alarms (#146308)

This commit is contained in:
Pete Sage 2025-07-05 11:39:52 -04:00 committed by GitHub
parent 736865c130
commit d997efc500
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 84 additions and 3 deletions

View File

@ -214,12 +214,25 @@ class MockSoCo(MagicMock):
surround_level = 3 surround_level = 3
music_surround_level = 4 music_surround_level = 4
soundbar_audio_input_format = "Dolby 5.1" soundbar_audio_input_format = "Dolby 5.1"
factory: SoCoMockFactory | None = None
@property @property
def visible_zones(self): def visible_zones(self):
"""Return visible zones and allow property to be overridden by device classes.""" """Return visible zones and allow property to be overridden by device classes."""
return {self} return {self}
@property
def all_zones(self) -> set[MockSoCo]:
"""Return a set of all mock zones, or just self if no factory or zones."""
if self.factory is not None:
if zones := self.factory.mock_all_zones:
return zones
return {self}
def set_factory(self, factory: SoCoMockFactory) -> None:
"""Set the factory for this mock."""
self.factory = factory
class SoCoMockFactory: class SoCoMockFactory:
"""Factory for creating SoCo Mocks.""" """Factory for creating SoCo Mocks."""
@ -244,11 +257,19 @@ class SoCoMockFactory:
self.sonos_playlists = sonos_playlists self.sonos_playlists = sonos_playlists
self.sonos_queue = sonos_queue self.sonos_queue = sonos_queue
@property
def mock_all_zones(self) -> set[MockSoCo]:
"""Return a set of all mock zones."""
return {
mock for mock in self.mock_list.values() if mock.mock_include_in_all_zones
}
def cache_mock( def cache_mock(
self, mock_soco: MockSoCo, ip_address: str, name: str = "Zone A" self, mock_soco: MockSoCo, ip_address: str, name: str = "Zone A"
) -> MockSoCo: ) -> MockSoCo:
"""Put a user created mock into the cache.""" """Put a user created mock into the cache."""
mock_soco.mock_add_spec(SoCo) mock_soco.mock_add_spec(SoCo)
mock_soco.set_factory(self)
mock_soco.ip_address = ip_address mock_soco.ip_address = ip_address
if ip_address != "192.168.42.2": if ip_address != "192.168.42.2":
mock_soco.uid += f"_{ip_address}" mock_soco.uid += f"_{ip_address}"
@ -260,6 +281,11 @@ class SoCoMockFactory:
my_speaker_info = self.speaker_info.copy() my_speaker_info = self.speaker_info.copy()
my_speaker_info["zone_name"] = name my_speaker_info["zone_name"] = name
my_speaker_info["uid"] = mock_soco.uid my_speaker_info["uid"] = mock_soco.uid
# Generate a different MAC for the non-default speakers.
# otherwise new devices will not be created.
if ip_address != "192.168.42.2":
last_octet = ip_address.split(".")[-1]
my_speaker_info["mac_address"] = f"00-00-00-00-00-{last_octet.zfill(2)}"
mock_soco.get_speaker_info = Mock(return_value=my_speaker_info) mock_soco.get_speaker_info = Mock(return_value=my_speaker_info)
mock_soco.add_to_queue = Mock(return_value=10) mock_soco.add_to_queue = Mock(return_value=10)
mock_soco.add_uri_to_queue = Mock(return_value=10) mock_soco.add_uri_to_queue = Mock(return_value=10)
@ -278,7 +304,7 @@ class SoCoMockFactory:
mock_soco.alarmClock = self.alarm_clock mock_soco.alarmClock = self.alarm_clock
mock_soco.get_battery_info.return_value = self.battery_info mock_soco.get_battery_info.return_value = self.battery_info
mock_soco.all_zones = {mock_soco} mock_soco.mock_include_in_all_zones = True
mock_soco.group.coordinator = mock_soco mock_soco.group.coordinator = mock_soco
mock_soco.household_id = "test_household_id" mock_soco.household_id = "test_household_id"
self.mock_list[ip_address] = mock_soco self.mock_list[ip_address] = mock_soco

View File

@ -324,10 +324,15 @@ async def test_async_poll_manual_hosts_5(
soco_1 = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Living Room") soco_1 = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Living Room")
soco_1.renderingControl = Mock() soco_1.renderingControl = Mock()
soco_1.renderingControl.GetVolume = Mock() soco_1.renderingControl.GetVolume = Mock()
# Unavailable speakers should not be included in all zones
soco_1.mock_include_in_all_zones = False
speaker_1_activity = SpeakerActivity(hass, soco_1) speaker_1_activity = SpeakerActivity(hass, soco_1)
soco_2 = soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom") soco_2 = soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom")
soco_2.renderingControl = Mock() soco_2.renderingControl = Mock()
soco_2.renderingControl.GetVolume = Mock() soco_2.renderingControl.GetVolume = Mock()
soco_2.mock_include_in_all_zones = False
speaker_2_activity = SpeakerActivity(hass, soco_2) speaker_2_activity = SpeakerActivity(hass, soco_2)
with caplog.at_level(logging.DEBUG): with caplog.at_level(logging.DEBUG):

View File

@ -26,10 +26,10 @@ from homeassistant.const import (
STATE_ON, STATE_ON,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .conftest import MockSoCo, SonosMockEvent from .conftest import MockSoCo, SonosMockEvent, SonosMockService
from tests.common import async_fire_time_changed from tests.common import async_fire_time_changed
@ -211,3 +211,53 @@ async def test_alarm_create_delete(
assert "switch.sonos_alarm_14" in entity_registry.entities assert "switch.sonos_alarm_14" in entity_registry.entities
assert "switch.sonos_alarm_15" not in entity_registry.entities assert "switch.sonos_alarm_15" not in entity_registry.entities
async def test_alarm_change_device(
hass: HomeAssistant,
async_setup_sonos,
soco: MockSoCo,
alarm_clock: SonosMockService,
alarm_clock_extended: SonosMockService,
alarm_event: SonosMockEvent,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
sonos_setup_two_speakers: list[MockSoCo],
) -> None:
"""Test Sonos Alarm being moved to a different speaker.
This test simulates a scenario where an alarm is created on one speaker
and then moved to another speaker. It checks that the entity is correctly
created on the new speaker and removed from the old one.
"""
entity_id = "switch.sonos_alarm_14"
soco_lr = sonos_setup_two_speakers[0]
await async_setup_sonos()
# Initially, the alarm is created on the soco mock
assert entity_id in entity_registry.entities
entity = entity_registry.async_get(entity_id)
device = device_registry.async_get(entity.device_id)
assert device.name == soco.get_speaker_info()["zone_name"]
# Simulate the alarm being moved to the soco_lr speaker
alarm_update = copy(alarm_clock_extended.ListAlarms.return_value)
alarm_update["CurrentAlarmList"] = alarm_update["CurrentAlarmList"].replace(
"RINCON_test", f"{soco_lr.uid}"
)
alarm_clock.ListAlarms.return_value = alarm_update
# Update the alarm_list_version so it gets processed.
alarm_event.variables["alarm_list_version"] = f"{soco_lr.uid}:1000"
alarm_update["CurrentAlarmListVersion"] = alarm_event.increment_variable(
"alarm_list_version"
)
alarm_clock.subscribe.return_value.callback(event=alarm_event)
await hass.async_block_till_done(wait_background_tasks=True)
assert entity_id in entity_registry.entities
alarm_14 = entity_registry.async_get(entity_id)
device = device_registry.async_get(alarm_14.device_id)
assert device.name == soco_lr.get_speaker_info()["zone_name"]