diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 05abc662b48..4b636b3e0f6 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -19,6 +19,7 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_TRACK, ) +from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -27,7 +28,13 @@ UPNP_ST = "urn:schemas-upnp-org:device:ZonePlayer:1" DOMAIN = "sonos" DATA_SONOS = "sonos_media_player" DATA_SONOS_DISCOVERY_MANAGER = "sonos_discovery_manager" -PLATFORMS = {BINARY_SENSOR_DOMAIN, MP_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN} +PLATFORMS = { + BINARY_SENSOR_DOMAIN, + MP_DOMAIN, + NUMBER_DOMAIN, + SENSOR_DOMAIN, + SWITCH_DOMAIN, +} SONOS_ARTIST = "artists" SONOS_ALBUM = "albums" @@ -139,6 +146,7 @@ SONOS_CHECK_ACTIVITY = "sonos_check_activity" SONOS_CREATE_ALARM = "sonos_create_alarm" SONOS_CREATE_BATTERY = "sonos_create_battery" SONOS_CREATE_SWITCHES = "sonos_create_switches" +SONOS_CREATE_LEVELS = "sonos_create_levels" SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player" SONOS_ENTITY_CREATED = "sonos_entity_created" SONOS_POLL_UPDATE = "sonos_poll_update" diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index f5389c7966a..664245a1c99 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -108,7 +108,6 @@ SERVICE_RESTORE = "restore" SERVICE_SET_TIMER = "set_sleep_timer" SERVICE_CLEAR_TIMER = "clear_sleep_timer" SERVICE_UPDATE_ALARM = "update_alarm" -SERVICE_SET_OPTION = "set_option" SERVICE_PLAY_QUEUE = "play_queue" SERVICE_REMOVE_FROM_QUEUE = "remove_from_queue" @@ -120,8 +119,6 @@ ATTR_INCLUDE_LINKED_ZONES = "include_linked_zones" ATTR_MASTER = "master" ATTR_WITH_GROUP = "with_group" ATTR_QUEUE_POSITION = "queue_position" -ATTR_EQ_BASS = "bass_level" -ATTR_EQ_TREBLE = "treble_level" async def async_setup_entry( @@ -225,19 +222,6 @@ async def async_setup_entry( "set_alarm", ) - platform.async_register_entity_service( # type: ignore - SERVICE_SET_OPTION, - { - vol.Optional(ATTR_EQ_BASS): vol.All( - vol.Coerce(int), vol.Range(min=-10, max=10) - ), - vol.Optional(ATTR_EQ_TREBLE): vol.All( - vol.Coerce(int), vol.Range(min=-10, max=10) - ), - }, - "set_option", - ) - platform.async_register_entity_service( # type: ignore SERVICE_PLAY_QUEUE, {vol.Optional(ATTR_QUEUE_POSITION): cv.positive_int}, @@ -605,19 +589,6 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): alarm.include_linked_zones = include_linked_zones alarm.save() - @soco_error() - def set_option( - self, - bass_level: int | None = None, - treble_level: int | None = None, - ) -> None: - """Modify playback options.""" - if bass_level is not None: - self.soco.bass = bass_level - - if treble_level is not None: - self.soco.treble = treble_level - @soco_error() def play_queue(self, queue_position: int = 0) -> None: """Start playing the queue.""" @@ -635,12 +606,6 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): ATTR_SONOS_GROUP: self.speaker.sonos_group_entities } - if self.speaker.bass_level is not None: - attributes[ATTR_EQ_BASS] = self.speaker.bass_level - - if self.speaker.treble_level is not None: - attributes[ATTR_EQ_TREBLE] = self.speaker.treble_level - if self.media.queue_position is not None: attributes[ATTR_QUEUE_POSITION] = self.media.queue_position diff --git a/homeassistant/components/sonos/number.py b/homeassistant/components/sonos/number.py new file mode 100644 index 00000000000..372a89edda0 --- /dev/null +++ b/homeassistant/components/sonos/number.py @@ -0,0 +1,64 @@ +"""Entity representing a Sonos number control.""" +from __future__ import annotations + +from homeassistant.components.number import NumberEntity +from homeassistant.const import ENTITY_CATEGORY_CONFIG +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import SONOS_CREATE_LEVELS +from .entity import SonosEntity +from .helpers import soco_error +from .speaker import SonosSpeaker + +LEVEL_TYPES = ("bass", "treble") + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Sonos number platform from a config entry.""" + + async def _async_create_entities(speaker: SonosSpeaker) -> None: + entities = [] + for level_type in LEVEL_TYPES: + entities.append(SonosLevelEntity(speaker, level_type)) + async_add_entities(entities) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, SONOS_CREATE_LEVELS, _async_create_entities) + ) + + +class SonosLevelEntity(SonosEntity, NumberEntity): + """Representation of a Sonos level entity.""" + + _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_min_value = -10 + _attr_max_value = 10 + + def __init__(self, speaker: SonosSpeaker, level_type: str) -> None: + """Initialize the level entity.""" + super().__init__(speaker) + self.level_type = level_type + + @property + def unique_id(self) -> str: + """Return the unique ID.""" + return f"{self.soco.uid}-{self.level_type}" + + @property + def name(self) -> str: + """Return the name.""" + return f"{self.speaker.zone_name} {self.level_type.capitalize()}" + + async def _async_poll(self) -> None: + """Poll the value if subscriptions are not working.""" + # Handled by SonosSpeaker + + @soco_error() + def set_value(self, value: float) -> None: + """Set a new value.""" + setattr(self.soco, self.level_type, value) + + @property + def value(self) -> float: + """Return the current value.""" + return getattr(self.speaker, self.level_type) diff --git a/homeassistant/components/sonos/services.yaml b/homeassistant/components/sonos/services.yaml index af664f0b367..4f04b2407ff 100644 --- a/homeassistant/components/sonos/services.yaml +++ b/homeassistant/components/sonos/services.yaml @@ -87,30 +87,6 @@ clear_sleep_timer: device: integration: sonos -set_option: - name: Set option - description: Set Sonos sound options. - target: - device: - integration: sonos - fields: - bass_level: - name: Bass Level - description: Bass level for EQ. - selector: - number: - min: -10 - max: 10 - mode: box - treble_level: - name: Treble Level - description: Treble level for EQ. - selector: - number: - min: -10 - max: 10 - mode: box - play_queue: name: Play queue description: Start playing the queue from the first item. diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 1acb814ee17..e8cd729bf6c 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -46,6 +46,7 @@ from .const import ( SONOS_CHECK_ACTIVITY, SONOS_CREATE_ALARM, SONOS_CREATE_BATTERY, + SONOS_CREATE_LEVELS, SONOS_CREATE_MEDIA_PLAYER, SONOS_CREATE_SWITCHES, SONOS_ENTITY_CREATED, @@ -192,8 +193,8 @@ class SonosSpeaker: self.night_mode: bool | None = None self.dialog_mode: bool | None = None self.cross_fade: bool | None = None - self.bass_level: int | None = None - self.treble_level: int | None = None + self.bass: int | None = None + self.treble: int | None = None # Misc features self.buttons_enabled: bool | None = None @@ -234,6 +235,8 @@ class SonosSpeaker: ) future.result(timeout=10) + dispatcher_send(self.hass, SONOS_CREATE_LEVELS, self) + if battery_info := fetch_battery_info_or_none(self.soco): self.battery_info = battery_info # Battery events can be infrequent, polling is still necessary @@ -490,11 +493,11 @@ class SonosSpeaker: if "dialog_level" in variables: self.dialog_mode = variables["dialog_level"] == "1" - if "bass_level" in variables: - self.bass_level = variables["bass_level"] + if "bass" in variables: + self.bass = variables["bass"] - if "treble_level" in variables: - self.treble_level = variables["treble_level"] + if "treble" in variables: + self.treble = variables["treble"] self.async_write_entity_states() @@ -968,8 +971,8 @@ class SonosSpeaker: self.muted = self.soco.mute self.night_mode = self.soco.night_mode self.dialog_mode = self.soco.dialog_mode - self.bass_level = self.soco.bass - self.treble_level = self.soco.treble + self.bass = self.soco.bass + self.treble = self.soco.treble try: self.cross_fade = self.soco.cross_fade diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 8a3a6571faa..cb31604081f 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -70,6 +70,8 @@ def soco_fixture(music_library, speaker_info, battery_info, alarm_clock): mock_soco.night_mode = True mock_soco.dialog_mode = True mock_soco.volume = 19 + mock_soco.bass = 1 + mock_soco.treble = -1 mock_soco.get_battery_info.return_value = battery_info mock_soco.all_zones = [mock_soco] yield mock_soco