mirror of
https://github.com/home-assistant/core.git
synced 2025-09-11 15:51:47 +02:00
Fix dialog enhancement switch for Sonos Arc Ultra (#150116)
This commit is contained in:
@@ -186,6 +186,9 @@ MODELS_TV_ONLY = (
|
||||
"ULTRA",
|
||||
)
|
||||
MODELS_LINEIN_AND_TV = ("AMP",)
|
||||
MODEL_SONOS_ARC_ULTRA = "SONOS ARC ULTRA"
|
||||
|
||||
ATTR_SPEECH_ENHANCEMENT_ENABLED = "speech_enhance_enabled"
|
||||
|
||||
AVAILABILITY_CHECK_INTERVAL = datetime.timedelta(minutes=1)
|
||||
AVAILABILITY_TIMEOUT = AVAILABILITY_CHECK_INTERVAL.total_seconds() * 4.5
|
||||
|
@@ -35,6 +35,7 @@ from homeassistant.util import dt as dt_util
|
||||
|
||||
from .alarms import SonosAlarms
|
||||
from .const import (
|
||||
ATTR_SPEECH_ENHANCEMENT_ENABLED,
|
||||
AVAILABILITY_TIMEOUT,
|
||||
BATTERY_SCAN_INTERVAL,
|
||||
DOMAIN,
|
||||
@@ -157,6 +158,7 @@ class SonosSpeaker:
|
||||
# Home theater
|
||||
self.audio_delay: int | None = None
|
||||
self.dialog_level: bool | None = None
|
||||
self.speech_enhance_enabled: bool | None = None
|
||||
self.night_mode: bool | None = None
|
||||
self.sub_enabled: bool | None = None
|
||||
self.sub_crossover: int | None = None
|
||||
@@ -548,6 +550,11 @@ class SonosSpeaker:
|
||||
@callback
|
||||
def async_update_volume(self, event: SonosEvent) -> None:
|
||||
"""Update information about currently volume settings."""
|
||||
_LOGGER.debug(
|
||||
"Updating volume for %s with event variables: %s",
|
||||
self.zone_name,
|
||||
event.variables,
|
||||
)
|
||||
self.event_stats.process(event)
|
||||
variables = event.variables
|
||||
|
||||
@@ -565,6 +572,7 @@ class SonosSpeaker:
|
||||
|
||||
for bool_var in (
|
||||
"dialog_level",
|
||||
ATTR_SPEECH_ENHANCEMENT_ENABLED,
|
||||
"night_mode",
|
||||
"sub_enabled",
|
||||
"surround_enabled",
|
||||
|
@@ -19,7 +19,9 @@ from homeassistant.helpers.event import async_track_time_change
|
||||
|
||||
from .alarms import SonosAlarms
|
||||
from .const import (
|
||||
ATTR_SPEECH_ENHANCEMENT_ENABLED,
|
||||
DOMAIN,
|
||||
MODEL_SONOS_ARC_ULTRA,
|
||||
SONOS_ALARMS_UPDATED,
|
||||
SONOS_CREATE_ALARM,
|
||||
SONOS_CREATE_SWITCHES,
|
||||
@@ -59,6 +61,7 @@ ALL_FEATURES = (
|
||||
ATTR_SURROUND_ENABLED,
|
||||
ATTR_STATUS_LIGHT,
|
||||
)
|
||||
ALL_SUBST_FEATURES = (ATTR_SPEECH_ENHANCEMENT_ENABLED,)
|
||||
|
||||
COORDINATOR_FEATURES = ATTR_CROSSFADE
|
||||
|
||||
@@ -69,6 +72,14 @@ POLL_REQUIRED = (
|
||||
|
||||
WEEKEND_DAYS = (0, 6)
|
||||
|
||||
# Mapping of model names to feature attributes that need to be substituted.
|
||||
# This is used to handle differences in attributes across Sonos models.
|
||||
MODEL_FEATURE_SUBSTITUTIONS: dict[str, dict[str, str]] = {
|
||||
MODEL_SONOS_ARC_ULTRA: {
|
||||
ATTR_SPEECH_ENHANCEMENT: ATTR_SPEECH_ENHANCEMENT_ENABLED,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -92,6 +103,13 @@ async def async_setup_entry(
|
||||
|
||||
def available_soco_attributes(speaker: SonosSpeaker) -> list[str]:
|
||||
features = []
|
||||
for feature_type in ALL_SUBST_FEATURES:
|
||||
try:
|
||||
if (state := getattr(speaker.soco, feature_type, None)) is not None:
|
||||
setattr(speaker, feature_type, state)
|
||||
except SoCoSlaveException:
|
||||
pass
|
||||
|
||||
for feature_type in ALL_FEATURES:
|
||||
try:
|
||||
if (state := getattr(speaker.soco, feature_type, None)) is not None:
|
||||
@@ -107,12 +125,23 @@ async def async_setup_entry(
|
||||
available_soco_attributes, speaker
|
||||
)
|
||||
for feature_type in available_features:
|
||||
attribute_key = MODEL_FEATURE_SUBSTITUTIONS.get(
|
||||
speaker.model_name.upper(), {}
|
||||
).get(feature_type, feature_type)
|
||||
_LOGGER.debug(
|
||||
"Creating %s switch on %s",
|
||||
"Creating %s switch on %s attribute %s",
|
||||
feature_type,
|
||||
speaker.zone_name,
|
||||
attribute_key,
|
||||
)
|
||||
entities.append(
|
||||
SonosSwitchEntity(
|
||||
feature_type=feature_type,
|
||||
attribute_key=attribute_key,
|
||||
speaker=speaker,
|
||||
config_entry=config_entry,
|
||||
)
|
||||
)
|
||||
entities.append(SonosSwitchEntity(feature_type, speaker, config_entry))
|
||||
async_add_entities(entities)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
@@ -127,11 +156,15 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity):
|
||||
"""Representation of a Sonos feature switch."""
|
||||
|
||||
def __init__(
|
||||
self, feature_type: str, speaker: SonosSpeaker, config_entry: SonosConfigEntry
|
||||
self,
|
||||
feature_type: str,
|
||||
attribute_key: str,
|
||||
speaker: SonosSpeaker,
|
||||
config_entry: SonosConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the switch."""
|
||||
super().__init__(speaker, config_entry)
|
||||
self.feature_type = feature_type
|
||||
self.attribute_key = attribute_key
|
||||
self.needs_coordinator = feature_type in COORDINATOR_FEATURES
|
||||
self._attr_entity_category = EntityCategory.CONFIG
|
||||
self._attr_translation_key = feature_type
|
||||
@@ -149,15 +182,15 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity):
|
||||
@soco_error()
|
||||
def poll_state(self) -> None:
|
||||
"""Poll the current state of the switch."""
|
||||
state = getattr(self.soco, self.feature_type)
|
||||
setattr(self.speaker, self.feature_type, state)
|
||||
state = getattr(self.soco, self.attribute_key)
|
||||
setattr(self.speaker, self.attribute_key, state)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if entity is on."""
|
||||
if self.needs_coordinator and not self.speaker.is_coordinator:
|
||||
return cast(bool, getattr(self.speaker.coordinator, self.feature_type))
|
||||
return cast(bool, getattr(self.speaker, self.feature_type))
|
||||
return cast(bool, getattr(self.speaker.coordinator, self.attribute_key))
|
||||
return cast(bool, getattr(self.speaker, self.attribute_key))
|
||||
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
@@ -175,7 +208,7 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity):
|
||||
else:
|
||||
soco = self.soco
|
||||
try:
|
||||
setattr(soco, self.feature_type, enable)
|
||||
setattr(soco, self.attribute_key, enable)
|
||||
except SoCoUPnPException as exc:
|
||||
_LOGGER.warning("Could not toggle %s: %s", self.entity_id, exc)
|
||||
|
||||
|
@@ -882,3 +882,23 @@ def ungroup_speakers(coordinator: MockSoCo, group_member: MockSoCo) -> None:
|
||||
)
|
||||
coordinator.zoneGroupTopology.subscribe.return_value._callback(event)
|
||||
group_member.zoneGroupTopology.subscribe.return_value._callback(event)
|
||||
|
||||
|
||||
def create_rendering_control_event(
|
||||
soco: MockSoCo,
|
||||
) -> SonosMockEvent:
|
||||
"""Create a Sonos Event for speaker rendering control."""
|
||||
variables = {
|
||||
"dialog_level": 1,
|
||||
"speech_enhance_enable": 1,
|
||||
"surround_level": 6,
|
||||
"music_surround_level": 4,
|
||||
"audio_delay": 0,
|
||||
"audio_delay_left_rear": 0,
|
||||
"audio_delay_right_rear": 0,
|
||||
"night_mode": 0,
|
||||
"surround_enabled": 1,
|
||||
"surround_mode": 1,
|
||||
"height_channel_level": 1,
|
||||
}
|
||||
return SonosMockEvent(soco, soco.renderingControl, variables)
|
||||
|
@@ -6,13 +6,18 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.sonos.const import DATA_SONOS_DISCOVERY_MANAGER
|
||||
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
|
||||
@@ -29,7 +34,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .conftest import MockSoCo, SonosMockEvent
|
||||
from .conftest import MockSoCo, SonosMockEvent, create_rendering_control_event
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
@@ -142,6 +147,49 @@ async def test_switch_attributes(
|
||||
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"),
|
||||
[
|
||||
|
Reference in New Issue
Block a user