Files
core/tests/components/sonos/test_switch.py
Pete Sage 51fbccd125 Fix Sonos CI issue part 2 (#150529)
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2025-08-13 12:26:24 +02:00

340 lines
12 KiB
Python

"""Tests for the Sonos Alarm switch platform."""
from copy import copy
from datetime import timedelta
from unittest.mock import patch
import pytest
from homeassistant.components.sonos import DOMAIN
from homeassistant.components.sonos.const import (
DATA_SONOS_DISCOVERY_MANAGER,
MODEL_SONOS_ARC_ULTRA,
)
from homeassistant.components.sonos.switch import (
ATTR_DURATION,
ATTR_ID,
ATTR_INCLUDE_LINKED_ZONES,
ATTR_PLAY_MODE,
ATTR_RECURRENCE,
ATTR_SPEECH_ENHANCEMENT,
ATTR_SPEECH_ENHANCEMENT_ENABLED,
ATTR_VOLUME,
)
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_TIME,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from .conftest import (
MockSoCo,
SoCoMockFactory,
SonosMockEvent,
SonosMockService,
create_rendering_control_event,
)
from tests.common import async_fire_time_changed
async def test_entity_registry(
hass: HomeAssistant, async_autosetup_sonos, entity_registry: er.EntityRegistry
) -> None:
"""Test sonos device with alarm registered in the device registry."""
assert "media_player.zone_a" in entity_registry.entities
assert "switch.sonos_alarm_14" in entity_registry.entities
assert "switch.zone_a_status_light" in entity_registry.entities
assert "switch.zone_a_loudness" in entity_registry.entities
assert "switch.zone_a_night_sound" in entity_registry.entities
assert "switch.zone_a_speech_enhancement" in entity_registry.entities
assert "switch.zone_a_subwoofer_enabled" in entity_registry.entities
assert "switch.zone_a_surround_enabled" in entity_registry.entities
assert "switch.zone_a_touch_controls" in entity_registry.entities
async def test_switch_attributes(
hass: HomeAssistant,
async_autosetup_sonos,
soco,
fire_zgs_event,
entity_registry: er.EntityRegistry,
) -> None:
"""Test for correct Sonos switch states."""
alarm = entity_registry.entities["switch.sonos_alarm_14"]
alarm_state = hass.states.get(alarm.entity_id)
assert alarm_state.state == STATE_ON
assert alarm_state.attributes.get(ATTR_TIME) == "07:00:00"
assert alarm_state.attributes.get(ATTR_ID) == "14"
assert alarm_state.attributes.get(ATTR_DURATION) == "02:00:00"
assert alarm_state.attributes.get(ATTR_RECURRENCE) == "DAILY"
assert alarm_state.attributes.get(ATTR_VOLUME) == 0.25
assert alarm_state.attributes.get(ATTR_PLAY_MODE) == "SHUFFLE_NOREPEAT"
assert not alarm_state.attributes.get(ATTR_INCLUDE_LINKED_ZONES)
surround_music_full_volume = entity_registry.entities[
"switch.zone_a_surround_music_full_volume"
]
surround_music_full_volume_state = hass.states.get(
surround_music_full_volume.entity_id
)
assert surround_music_full_volume_state.state == STATE_ON
night_sound = entity_registry.entities["switch.zone_a_night_sound"]
night_sound_state = hass.states.get(night_sound.entity_id)
assert night_sound_state.state == STATE_ON
loudness = entity_registry.entities["switch.zone_a_loudness"]
loudness_state = hass.states.get(loudness.entity_id)
assert loudness_state.state == STATE_ON
speech_enhancement = entity_registry.entities["switch.zone_a_speech_enhancement"]
speech_enhancement_state = hass.states.get(speech_enhancement.entity_id)
assert speech_enhancement_state.state == STATE_ON
crossfade = entity_registry.entities["switch.zone_a_crossfade"]
crossfade_state = hass.states.get(crossfade.entity_id)
assert crossfade_state.state == STATE_ON
# Ensure switches are disabled
status_light = entity_registry.entities["switch.zone_a_status_light"]
assert hass.states.get(status_light.entity_id) is None
touch_controls = entity_registry.entities["switch.zone_a_touch_controls"]
assert hass.states.get(touch_controls.entity_id) is None
sub_switch = entity_registry.entities["switch.zone_a_subwoofer_enabled"]
sub_switch_state = hass.states.get(sub_switch.entity_id)
assert sub_switch_state.state == STATE_OFF
surround_switch = entity_registry.entities["switch.zone_a_surround_enabled"]
surround_switch_state = hass.states.get(surround_switch.entity_id)
assert surround_switch_state.state == STATE_ON
# Enable disabled switches
for entity in (status_light, touch_controls):
entity_registry.async_update_entity(
entity_id=entity.entity_id, disabled_by=None
)
await hass.async_block_till_done()
# Fire event to cancel poll timer and avoid triggering errors during time jump
service = soco.contentDirectory
empty_event = SonosMockEvent(soco, service, {})
subscription = service.subscribe.return_value
subscription.callback(event=empty_event)
await hass.async_block_till_done()
# Mock shutdown calls during config entry reload
with patch.object(hass.data[DATA_SONOS_DISCOVERY_MANAGER], "async_shutdown") as m:
async_fire_time_changed(
hass,
dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
)
await hass.async_block_till_done(wait_background_tasks=True)
assert m.called
# Trigger subscription callback for speaker discovery
await fire_zgs_event()
await hass.async_block_till_done(wait_background_tasks=True)
status_light_state = hass.states.get(status_light.entity_id)
assert status_light_state.state == STATE_ON
touch_controls = entity_registry.entities["switch.zone_a_touch_controls"]
touch_controls_state = hass.states.get(touch_controls.entity_id)
assert touch_controls_state.state == STATE_ON
@pytest.mark.parametrize(
("model", "attribute"),
[
("Sonos One SL", ATTR_SPEECH_ENHANCEMENT),
(MODEL_SONOS_ARC_ULTRA.lower(), ATTR_SPEECH_ENHANCEMENT_ENABLED),
],
)
async def test_switch_speech_enhancement(
hass: HomeAssistant,
async_setup_sonos,
soco: MockSoCo,
speaker_info: dict[str, str],
entity_registry: er.EntityRegistry,
model: str,
attribute: str,
) -> None:
"""Tests the speech enhancement switch and attribute substitution for different models."""
entity_id = "switch.zone_a_speech_enhancement"
speaker_info["model_name"] = model
soco.get_speaker_info.return_value = speaker_info
setattr(soco, attribute, True)
await async_setup_sonos()
switch = entity_registry.entities[entity_id]
state = hass.states.get(switch.entity_id)
assert state.state == STATE_ON
event = create_rendering_control_event(soco)
event.variables[attribute] = False
soco.renderingControl.subscribe.return_value._callback(event)
await hass.async_block_till_done(wait_background_tasks=True)
state = hass.states.get(switch.entity_id)
assert state.state == STATE_OFF
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
assert getattr(soco, attribute) is True
@pytest.mark.parametrize(
("service", "expected_result"),
[
(SERVICE_TURN_OFF, "0"),
(SERVICE_TURN_ON, "1"),
],
)
async def test_switch_alarm_turn_on(
hass: HomeAssistant,
async_setup_sonos,
soco: MockSoCo,
service: str,
expected_result: str,
) -> None:
"""Test enabling and disabling of alarm."""
await async_setup_sonos()
await hass.services.async_call(
SWITCH_DOMAIN, service, {ATTR_ENTITY_ID: "switch.sonos_alarm_14"}, blocking=True
)
assert soco.alarmClock.UpdateAlarm.call_count == 1
call_args = soco.alarmClock.UpdateAlarm.call_args[0]
assert call_args[0][0] == ("ID", "14")
assert call_args[0][4] == ("Enabled", expected_result)
async def test_alarm_create_delete(
hass: HomeAssistant,
async_setup_sonos,
soco,
alarm_clock,
alarm_clock_extended,
alarm_event,
entity_registry: er.EntityRegistry,
) -> None:
"""Test for correct creation and deletion of alarms during runtime."""
one_alarm = copy(alarm_clock.ListAlarms.return_value)
two_alarms = copy(alarm_clock_extended.ListAlarms.return_value)
await async_setup_sonos()
assert "switch.sonos_alarm_14" in entity_registry.entities
assert "switch.sonos_alarm_15" not in entity_registry.entities
subscription = alarm_clock.subscribe.return_value
sub_callback = subscription.callback
alarm_clock.ListAlarms.return_value = two_alarms
alarm_event.variables["alarm_list_version"] = two_alarms["CurrentAlarmListVersion"]
sub_callback(event=alarm_event)
await hass.async_block_till_done(wait_background_tasks=True)
assert "switch.sonos_alarm_14" in entity_registry.entities
assert "switch.sonos_alarm_15" in entity_registry.entities
one_alarm["CurrentAlarmListVersion"] = alarm_event.increment_variable(
"alarm_list_version"
)
alarm_clock.ListAlarms.return_value = one_alarm
sub_callback(event=alarm_event)
await hass.async_block_till_done(wait_background_tasks=True)
assert "switch.sonos_alarm_14" in entity_registry.entities
assert "switch.sonos_alarm_15" not in entity_registry.entities
async def test_alarm_change_device(
hass: HomeAssistant,
alarm_clock: SonosMockService,
alarm_clock_extended: SonosMockService,
alarm_event: SonosMockEvent,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
soco_factory: SoCoMockFactory,
) -> 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.
"""
# Create the alarm on the soco_lr speaker
soco_factory.mock_zones = True
soco_lr = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Living Room")
alarm_dict = copy(alarm_clock.ListAlarms.return_value)
alarm_dict["CurrentAlarmList"] = alarm_dict["CurrentAlarmList"].replace(
"RINCON_test", f"{soco_lr.uid}"
)
alarm_dict["CurrentAlarmListVersion"] = "RINCON_test:900"
soco_lr.alarmClock.ListAlarms.return_value = alarm_dict
soco_br = soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom")
await async_setup_component(
hass,
DOMAIN,
{
DOMAIN: {
"media_player": {
"interface_addr": "127.0.0.1",
"hosts": ["10.10.10.1", "10.10.10.2"],
}
}
},
)
await hass.async_block_till_done()
entity_id = "switch.sonos_alarm_14"
# Verify the alarm is created on the soco_lr speaker
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_lr.get_speaker_info()["zone_name"]
# Simulate the alarm being moved to the soco_br speaker
alarm_update = copy(alarm_clock_extended.ListAlarms.return_value)
alarm_update["CurrentAlarmList"] = alarm_update["CurrentAlarmList"].replace(
"RINCON_test", f"{soco_br.uid}"
)
alarm_clock.ListAlarms.return_value = alarm_update
# Update the alarm_list_version so it gets processed.
alarm_event.variables["alarm_list_version"] = "RINCON_test: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_br.get_speaker_info()["zone_name"]