mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 09:47:52 +00:00
Add audio_delay
number entity to Sonos (#63566)
This commit is contained in:
parent
e2f9284c55
commit
740a8c33ee
@ -5,17 +5,22 @@ import logging
|
||||
|
||||
from homeassistant.components.number import NumberEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import SONOS_CREATE_LEVELS
|
||||
from .entity import SonosEntity
|
||||
from .exception import SpeakerUnavailable
|
||||
from .helpers import soco_error
|
||||
from .speaker import SonosSpeaker
|
||||
|
||||
LEVEL_TYPES = ("bass", "treble")
|
||||
LEVEL_TYPES = {
|
||||
"audio_delay": (0, 5),
|
||||
"bass": (-10, 10),
|
||||
"treble": (-10, 10),
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -27,14 +32,26 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Sonos number platform from a config entry."""
|
||||
|
||||
@callback
|
||||
def _async_create_entities(speaker: SonosSpeaker) -> None:
|
||||
def available_soco_attributes(speaker: SonosSpeaker) -> list[str]:
|
||||
features = []
|
||||
for level_type, valid_range in LEVEL_TYPES.items():
|
||||
if (state := getattr(speaker.soco, level_type, None)) is not None:
|
||||
setattr(speaker, level_type, state)
|
||||
features.append((level_type, valid_range))
|
||||
return features
|
||||
|
||||
async def _async_create_entities(speaker: SonosSpeaker) -> None:
|
||||
entities = []
|
||||
for level_type in LEVEL_TYPES:
|
||||
|
||||
available_features = await hass.async_add_executor_job(
|
||||
available_soco_attributes, speaker
|
||||
)
|
||||
|
||||
for level_type, valid_range in available_features:
|
||||
_LOGGER.debug(
|
||||
"Creating %s number control on %s", level_type, speaker.zone_name
|
||||
)
|
||||
entities.append(SonosLevelEntity(speaker, level_type))
|
||||
entities.append(SonosLevelEntity(speaker, level_type, valid_range))
|
||||
async_add_entities(entities)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
@ -46,19 +63,30 @@ class SonosLevelEntity(SonosEntity, NumberEntity):
|
||||
"""Representation of a Sonos level entity."""
|
||||
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_attr_min_value = -10
|
||||
_attr_max_value = 10
|
||||
|
||||
def __init__(self, speaker: SonosSpeaker, level_type: str) -> None:
|
||||
def __init__(
|
||||
self, speaker: SonosSpeaker, level_type: str, valid_range: tuple[int]
|
||||
) -> None:
|
||||
"""Initialize the level entity."""
|
||||
super().__init__(speaker)
|
||||
self._attr_unique_id = f"{self.soco.uid}-{level_type}"
|
||||
self._attr_name = f"{self.speaker.zone_name} {level_type.capitalize()}"
|
||||
name_suffix = level_type.replace("_", " ").title()
|
||||
self._attr_name = f"{self.speaker.zone_name} {name_suffix}"
|
||||
self.level_type = level_type
|
||||
self._attr_min_value, self._attr_max_value = valid_range
|
||||
|
||||
async def _async_poll(self) -> None:
|
||||
"""Poll the value if subscriptions are not working."""
|
||||
# Handled by SonosSpeaker
|
||||
await self.hass.async_add_executor_job(self.update)
|
||||
|
||||
@soco_error(raise_on_err=False)
|
||||
def update(self) -> None:
|
||||
"""Fetch number state if necessary."""
|
||||
if not self.available:
|
||||
raise SpeakerUnavailable
|
||||
|
||||
state = getattr(self.soco, self.level_type)
|
||||
setattr(self.speaker, self.level_type, state)
|
||||
|
||||
@soco_error()
|
||||
def set_value(self, value: float) -> None:
|
||||
|
@ -185,11 +185,14 @@ class SonosSpeaker:
|
||||
# Volume / Sound
|
||||
self.volume: int | None = None
|
||||
self.muted: bool | None = None
|
||||
self.night_mode: bool | None = None
|
||||
self.dialog_level: bool | None = None
|
||||
self.cross_fade: bool | None = None
|
||||
self.bass: int | None = None
|
||||
self.treble: int | None = None
|
||||
|
||||
# Home theater
|
||||
self.audio_delay: int | None = None
|
||||
self.dialog_level: bool | None = None
|
||||
self.night_mode: bool | None = None
|
||||
self.sub_enabled: bool | None = None
|
||||
self.surround_enabled: bool | None = None
|
||||
|
||||
@ -485,7 +488,7 @@ class SonosSpeaker:
|
||||
if bool_var in variables:
|
||||
setattr(self, bool_var, variables[bool_var] == "1")
|
||||
|
||||
for int_var in ("bass", "treble"):
|
||||
for int_var in ("audio_delay", "bass", "treble"):
|
||||
if int_var in variables:
|
||||
setattr(self, int_var, variables[int_var])
|
||||
|
||||
@ -969,15 +972,13 @@ class SonosSpeaker:
|
||||
#
|
||||
# Media and playback state handlers
|
||||
#
|
||||
@soco_error()
|
||||
@soco_error(raise_on_err=False)
|
||||
def update_volume(self) -> None:
|
||||
"""Update information about current volume settings."""
|
||||
self.volume = self.soco.volume
|
||||
self.muted = self.soco.mute
|
||||
self.night_mode = self.soco.night_mode
|
||||
self.dialog_level = self.soco.dialog_level
|
||||
self.bass = self.soco.bass
|
||||
self.treble = self.soco.treble
|
||||
|
||||
try:
|
||||
self.cross_fade = self.soco.cross_fade
|
||||
|
@ -106,7 +106,7 @@ async def async_setup_entry(
|
||||
entities.append(SonosAlarmEntity(alarm_id, speaker))
|
||||
async_add_entities(entities)
|
||||
|
||||
def available_soco_attributes(speaker: SonosSpeaker) -> list[tuple[str, bool]]:
|
||||
def available_soco_attributes(speaker: SonosSpeaker) -> list[str]:
|
||||
features = []
|
||||
for feature_type in ALL_FEATURES:
|
||||
try:
|
||||
|
@ -101,6 +101,7 @@ def soco_fixture(music_library, speaker_info, battery_info, alarm_clock):
|
||||
mock_soco.night_mode = True
|
||||
mock_soco.dialog_level = True
|
||||
mock_soco.volume = 19
|
||||
mock_soco.audio_delay = 2
|
||||
mock_soco.bass = 1
|
||||
mock_soco.treble = -1
|
||||
mock_soco.sub_enabled = False
|
||||
|
32
tests/components/sonos/test_number.py
Normal file
32
tests/components/sonos/test_number.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""Tests for the Sonos number platform."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.helpers import entity_registry as ent_reg
|
||||
|
||||
|
||||
async def test_audio_input_sensor(hass, async_autosetup_sonos, soco):
|
||||
"""Test audio input sensor."""
|
||||
entity_registry = ent_reg.async_get(hass)
|
||||
|
||||
bass_number = entity_registry.entities["number.zone_a_bass"]
|
||||
bass_state = hass.states.get(bass_number.entity_id)
|
||||
assert bass_state.state == "1"
|
||||
|
||||
treble_number = entity_registry.entities["number.zone_a_treble"]
|
||||
treble_state = hass.states.get(treble_number.entity_id)
|
||||
assert treble_state.state == "-1"
|
||||
|
||||
audio_delay_number = entity_registry.entities["number.zone_a_audio_delay"]
|
||||
audio_delay_state = hass.states.get(audio_delay_number.entity_id)
|
||||
assert audio_delay_state.state == "2"
|
||||
|
||||
with patch("soco.SoCo.audio_delay") as mock_audio_delay:
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{ATTR_ENTITY_ID: audio_delay_number.entity_id, "value": 3},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_audio_delay.called_with(3)
|
33
tests/components/sonos/test_speaker.py
Normal file
33
tests/components/sonos/test_speaker.py
Normal file
@ -0,0 +1,33 @@
|
||||
"""Tests for common SonosSpeaker behavior."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.sonos.const import DATA_SONOS, SCAN_INTERVAL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
|
||||
async def test_fallback_to_polling(
|
||||
hass: HomeAssistant, async_autosetup_sonos, soco, caplog
|
||||
):
|
||||
"""Test that polling fallback works."""
|
||||
speaker = list(hass.data[DATA_SONOS].discovered.values())[0]
|
||||
assert speaker.soco is soco
|
||||
assert speaker._subscriptions
|
||||
|
||||
caplog.clear()
|
||||
|
||||
# Ensure subscriptions are cancelled and polling methods are called when subscriptions time out
|
||||
with patch(
|
||||
"homeassistant.components.sonos.speaker.SonosSpeaker.update_media"
|
||||
), patch(
|
||||
"homeassistant.components.sonos.speaker.SonosSpeaker.subscription_address"
|
||||
):
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert not speaker._subscriptions
|
||||
assert speaker.subscriptions_failed
|
||||
assert "falling back to polling" in caplog.text
|
||||
assert "Activity on Zone A from SonosSpeaker.update_volume" in caplog.text
|
Loading…
x
Reference in New Issue
Block a user