Skip to content

Commit 268f0d9

Browse files
authored
Add Tests for Sonos Alarms (#150014)
1 parent f8d3bc1 commit 268f0d9

File tree

2 files changed

+108
-3
lines changed

2 files changed

+108
-3
lines changed

tests/components/sonos/conftest.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,12 +214,26 @@ class MockSoCo(MagicMock):
214214
surround_level = 3
215215
music_surround_level = 4
216216
soundbar_audio_input_format = "Dolby 5.1"
217+
factory: SoCoMockFactory | None = None
217218

218219
@property
219220
def visible_zones(self):
220221
"""Return visible zones and allow property to be overridden by device classes."""
221222
return {self}
222223

224+
@property
225+
def all_zones(self) -> set[MockSoCo]:
226+
"""Return all mock zones if a factory is set and enabled, else just self."""
227+
return (
228+
self.factory.mock_all_zones
229+
if self.factory and self.factory.mock_all_zones
230+
else {self}
231+
)
232+
233+
def set_factory(self, factory: SoCoMockFactory) -> None:
234+
"""Set the factory for this mock."""
235+
self.factory = factory
236+
223237

224238
class SoCoMockFactory:
225239
"""Factory for creating SoCo Mocks."""
@@ -243,12 +257,19 @@ def __init__(
243257
self.alarm_clock = alarm_clock
244258
self.sonos_playlists = sonos_playlists
245259
self.sonos_queue = sonos_queue
260+
self.mock_zones: bool = False
261+
262+
@property
263+
def mock_all_zones(self) -> set[MockSoCo] | None:
264+
"""Return a set of all mock zones, or None if not enabled."""
265+
return set(self.mock_list.values()) if self.mock_zones else None
246266

247267
def cache_mock(
248268
self, mock_soco: MockSoCo, ip_address: str, name: str = "Zone A"
249269
) -> MockSoCo:
250270
"""Put a user created mock into the cache."""
251271
mock_soco.mock_add_spec(SoCo)
272+
mock_soco.set_factory(self)
252273
mock_soco.ip_address = ip_address
253274
if ip_address != "192.168.42.2":
254275
mock_soco.uid += f"_{ip_address}"
@@ -260,6 +281,11 @@ def cache_mock(
260281
my_speaker_info = self.speaker_info.copy()
261282
my_speaker_info["zone_name"] = name
262283
my_speaker_info["uid"] = mock_soco.uid
284+
# Generate a different MAC for the non-default speakers.
285+
# otherwise new devices will not be created.
286+
if ip_address != "192.168.42.2":
287+
last_octet = ip_address.split(".")[-1]
288+
my_speaker_info["mac_address"] = f"00-00-00-00-00-{last_octet.zfill(2)}"
263289
mock_soco.get_speaker_info = Mock(return_value=my_speaker_info)
264290
mock_soco.add_to_queue = Mock(return_value=10)
265291
mock_soco.add_uri_to_queue = Mock(return_value=10)
@@ -278,7 +304,6 @@ def cache_mock(
278304

279305
mock_soco.alarmClock = self.alarm_clock
280306
mock_soco.get_battery_info.return_value = self.battery_info
281-
mock_soco.all_zones = {mock_soco}
282307
mock_soco.group.coordinator = mock_soco
283308
mock_soco.household_id = "test_household_id"
284309
self.mock_list[ip_address] = mock_soco

tests/components/sonos/test_switch.py

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import pytest
88

9+
from homeassistant.components.sonos import DOMAIN
910
from homeassistant.components.sonos.const import (
1011
DATA_SONOS_DISCOVERY_MANAGER,
1112
MODEL_SONOS_ARC_ULTRA,
@@ -31,10 +32,17 @@
3132
STATE_ON,
3233
)
3334
from homeassistant.core import HomeAssistant
34-
from homeassistant.helpers import entity_registry as er
35+
from homeassistant.helpers import device_registry as dr, entity_registry as er
36+
from homeassistant.setup import async_setup_component
3537
from homeassistant.util import dt as dt_util
3638

37-
from .conftest import MockSoCo, SonosMockEvent, create_rendering_control_event
39+
from .conftest import (
40+
MockSoCo,
41+
SoCoMockFactory,
42+
SonosMockEvent,
43+
SonosMockService,
44+
create_rendering_control_event,
45+
)
3846

3947
from tests.common import async_fire_time_changed
4048

@@ -259,3 +267,75 @@ async def test_alarm_create_delete(
259267

260268
assert "switch.sonos_alarm_14" in entity_registry.entities
261269
assert "switch.sonos_alarm_15" not in entity_registry.entities
270+
271+
272+
async def test_alarm_change_device(
273+
hass: HomeAssistant,
274+
async_setup_sonos,
275+
soco: MockSoCo,
276+
alarm_clock: SonosMockService,
277+
alarm_clock_extended: SonosMockService,
278+
alarm_event: SonosMockEvent,
279+
entity_registry: er.EntityRegistry,
280+
device_registry: dr.DeviceRegistry,
281+
soco_factory: SoCoMockFactory,
282+
) -> None:
283+
"""Test Sonos Alarm being moved to a different speaker.
284+
285+
This test simulates a scenario where an alarm is created on one speaker
286+
and then moved to another speaker. It checks that the entity is correctly
287+
created on the new speaker and removed from the old one.
288+
"""
289+
290+
# Create the alarm on the soco_lr speaker
291+
soco_factory.mock_zones = True
292+
soco_lr = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Living Room")
293+
alarm_dict = copy(alarm_clock.ListAlarms.return_value)
294+
alarm_dict["CurrentAlarmList"] = alarm_dict["CurrentAlarmList"].replace(
295+
"RINCON_test", f"{soco_lr.uid}"
296+
)
297+
alarm_dict["CurrentAlarmListVersion"] = f"{soco_lr.uid}:900"
298+
soco_lr.alarmClock.ListAlarms.return_value = alarm_dict
299+
soco_br = soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom")
300+
await async_setup_component(
301+
hass,
302+
DOMAIN,
303+
{
304+
DOMAIN: {
305+
"media_player": {
306+
"interface_addr": "127.0.0.1",
307+
"hosts": ["10.10.10.1", "10.10.10.2"],
308+
}
309+
}
310+
},
311+
)
312+
await hass.async_block_till_done()
313+
314+
entity_id = "switch.sonos_alarm_14"
315+
316+
# Verify the alarm is created on the soco_lr speaker
317+
assert entity_id in entity_registry.entities
318+
entity = entity_registry.async_get(entity_id)
319+
device = device_registry.async_get(entity.device_id)
320+
assert device.name == soco_lr.get_speaker_info()["zone_name"]
321+
322+
# Simulate the alarm being moved to the soco_br speaker
323+
alarm_update = copy(alarm_clock_extended.ListAlarms.return_value)
324+
alarm_update["CurrentAlarmList"] = alarm_update["CurrentAlarmList"].replace(
325+
"RINCON_test", f"{soco_br.uid}"
326+
)
327+
alarm_clock.ListAlarms.return_value = alarm_update
328+
329+
# Update the alarm_list_version so it gets processed.
330+
alarm_event.variables["alarm_list_version"] = f"{soco_br.uid}:1000"
331+
alarm_update["CurrentAlarmListVersion"] = alarm_event.increment_variable(
332+
"alarm_list_version"
333+
)
334+
335+
alarm_clock.subscribe.return_value.callback(event=alarm_event)
336+
await hass.async_block_till_done(wait_background_tasks=True)
337+
338+
assert entity_id in entity_registry.entities
339+
alarm_14 = entity_registry.async_get(entity_id)
340+
device = device_registry.async_get(alarm_14.device_id)
341+
assert device.name == soco_br.get_speaker_info()["zone_name"]

0 commit comments

Comments
 (0)