From dbc4a65d4813c62443a0bb3a3c82ca96aaedeb43 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Tue, 30 Sep 2025 14:25:19 -0400 Subject: [PATCH] Fix Sonos Dialog Select type conversion part II (#152491) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/sonos/select.py | 22 ++++++------- homeassistant/components/sonos/speaker.py | 23 ++++++++++++++ tests/components/sonos/test_select.py | 38 ++++++++++++++++++++--- 3 files changed, 66 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/sonos/select.py b/homeassistant/components/sonos/select.py index 0a56e37e75c..fa38bf20c9f 100644 --- a/homeassistant/components/sonos/select.py +++ b/homeassistant/components/sonos/select.py @@ -59,17 +59,12 @@ async def async_setup_entry( for select_data in SELECT_TYPES: if select_data.speaker_model == speaker.model_name.upper(): if ( - state := getattr(speaker.soco, select_data.soco_attribute, None) - ) is not None: - try: - setattr(speaker, select_data.speaker_attribute, int(state)) - features.append(select_data) - except ValueError: - _LOGGER.error( - "Invalid value for %s %s", - select_data.speaker_attribute, - state, - ) + speaker.update_soco_int_attribute( + select_data.soco_attribute, select_data.speaker_attribute + ) + is not None + ): + features.append(select_data) return features async def _async_create_entities(speaker: SonosSpeaker) -> None: @@ -112,8 +107,9 @@ class SonosSelectEntity(SonosEntity, SelectEntity): @soco_error() def poll_state(self) -> None: """Poll the device for the current state.""" - state = getattr(self.soco, self.soco_attribute) - setattr(self.speaker, self.speaker_attribute, state) + self.speaker.update_soco_int_attribute( + self.soco_attribute, self.speaker_attribute + ) @property def current_option(self) -> str | None: diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index acf1b08cd36..c61f047d3e3 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -275,6 +275,29 @@ class SonosSpeaker: """Write states for associated SonosEntity instances.""" async_dispatcher_send(self.hass, f"{SONOS_STATE_UPDATED}-{self.soco.uid}") + def update_soco_int_attribute( + self, soco_attribute: str, speaker_attribute: str + ) -> int | None: + """Update an integer attribute from SoCo and set it on the speaker. + + Returns the integer value if successful, otherwise None. Do not call from + async context as it is a blocking function. + """ + value: int | None = None + if (state := getattr(self.soco, soco_attribute, None)) is None: + _LOGGER.error("Missing value for %s", speaker_attribute) + else: + try: + value = int(state) + except (TypeError, ValueError): + _LOGGER.error( + "Invalid value for %s %s", + speaker_attribute, + state, + ) + setattr(self, speaker_attribute, value) + return value + # # Properties # diff --git a/tests/components/sonos/test_select.py b/tests/components/sonos/test_select.py index dbbf28a52d7..0a50da9b9a7 100644 --- a/tests/components/sonos/test_select.py +++ b/tests/components/sonos/test_select.py @@ -88,6 +88,36 @@ async def test_select_dialog_invalid_level( assert dialog_level_state.state == STATE_UNKNOWN +@pytest.mark.parametrize( + ("value", "result"), + [ + ("invalid_integer", "Invalid value for dialog_level_enum invalid_integer"), + (None, "Missing value for dialog_level_enum"), + ], +) +async def test_select_dialog_value_error( + hass: HomeAssistant, + async_setup_sonos, + soco, + entity_registry: er.EntityRegistry, + speaker_info: dict[str, str], + caplog: pytest.LogCaptureFixture, + value: str | None, + result: str, +) -> None: + """Test receiving a value from Sonos that is not convertible to an integer.""" + + speaker_info["model_name"] = MODEL_SONOS_ARC_ULTRA.lower() + soco.get_speaker_info.return_value = speaker_info + soco.dialog_level = value + + with caplog.at_level(logging.WARNING): + await async_setup_sonos() + assert result in caplog.text + + assert SELECT_DIALOG_LEVEL_ENTITY not in entity_registry.entities + + @pytest.mark.parametrize( ("result", "option"), [ @@ -149,12 +179,12 @@ async def test_select_dialog_level_event( speaker_info["model_name"] = MODEL_SONOS_ARC_ULTRA.lower() soco.get_speaker_info.return_value = speaker_info - soco.dialog_level = 0 + soco.dialog_level = "0" await async_setup_sonos() event = create_rendering_control_event(soco) - event.variables[ATTR_DIALOG_LEVEL] = 3 + event.variables[ATTR_DIALOG_LEVEL] = "3" soco.renderingControl.subscribe.return_value._callback(event) await hass.async_block_till_done(wait_background_tasks=True) @@ -175,11 +205,11 @@ async def test_select_dialog_level_poll( speaker_info["model_name"] = MODEL_SONOS_ARC_ULTRA.lower() soco.get_speaker_info.return_value = speaker_info - soco.dialog_level = 0 + soco.dialog_level = "0" await async_setup_sonos() - soco.dialog_level = 4 + soco.dialog_level = "4" freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass)