diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index 5cccc40ac5f..f011cb2d754 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -11,7 +11,7 @@ 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_AUDIO_FORMAT_SENSOR, SONOS_CREATE_BATTERY +from .const import SONOS_CREATE_AUDIO_FORMAT_SENSOR, SONOS_CREATE_BATTERY, SOURCE_TV from .entity import SonosEntity, SonosPollingEntity from .helpers import soco_error from .speaker import SonosSpeaker @@ -94,8 +94,14 @@ class SonosAudioInputFormatSensorEntity(SonosPollingEntity, SensorEntity): self._attr_name = f"{self.speaker.zone_name} Audio Input Format" self._attr_native_value = audio_format - @soco_error() def poll_state(self) -> None: + """Poll the state if TV source is active and state has settled.""" + if self.speaker.media.source_name != SOURCE_TV and self.state == "No input": + return + self._poll_state() + + @soco_error() + def _poll_state(self) -> None: """Poll the device for the current state.""" self._attr_native_value = self.soco.soundbar_audio_input_format diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 14ad17bec8b..d7791c6ce72 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -3,6 +3,7 @@ from copy import copy from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest +from soco import SoCo from homeassistant.components import ssdp, zeroconf from homeassistant.components.media_player import DOMAIN as MP_DOMAIN @@ -82,7 +83,9 @@ def config_entry_fixture(): @pytest.fixture(name="soco") -def soco_fixture(music_library, speaker_info, battery_info, alarm_clock): +def soco_fixture( + music_library, speaker_info, current_track_info_empty, battery_info, alarm_clock +): """Create a mock soco SoCo fixture.""" with patch("homeassistant.components.sonos.SoCo", autospec=True) as mock, patch( "socket.gethostbyname", return_value="192.168.42.2" @@ -92,6 +95,8 @@ def soco_fixture(music_library, speaker_info, battery_info, alarm_clock): mock_soco.uid = "RINCON_test" mock_soco.play_mode = "NORMAL" mock_soco.music_library = music_library + mock_soco.get_current_track_info.return_value = current_track_info_empty + mock_soco.music_source_from_uri = SoCo.music_source_from_uri mock_soco.get_speaker_info.return_value = speaker_info mock_soco.avTransport = SonosMockService("AVTransport") mock_soco.renderingControl = SonosMockService("RenderingControl") @@ -216,6 +221,22 @@ def speaker_info_fixture(): } +@pytest.fixture(name="current_track_info_empty") +def current_track_info_empty_fixture(): + """Create current_track_info_empty fixture.""" + return { + "title": "", + "artist": "", + "album": "", + "album_art": "", + "position": "NOT_IMPLEMENTED", + "playlist_position": "1", + "duration": "NOT_IMPLEMENTED", + "uri": "", + "metadata": "NOT_IMPLEMENTED", + } + + @pytest.fixture(name="battery_info") def battery_info_fixture(): """Create battery_info fixture.""" @@ -254,6 +275,61 @@ def alarm_event_fixture(soco): return SonosMockEvent(soco, soco.alarmClock, variables) +@pytest.fixture(name="no_media_event") +def no_media_event_fixture(soco): + """Create no_media_event_fixture.""" + variables = { + "current_crossfade_mode": "0", + "current_play_mode": "NORMAL", + "current_section": "0", + "current_track_uri": "", + "enqueued_transport_uri": "", + "enqueued_transport_uri_meta_data": "", + "transport_state": "STOPPED", + } + return SonosMockEvent(soco, soco.avTransport, variables) + + +@pytest.fixture(name="tv_event") +def tv_event_fixture(soco): + """Create alarm_event fixture.""" + variables = { + "transport_state": "PLAYING", + "current_play_mode": "NORMAL", + "current_crossfade_mode": "0", + "number_of_tracks": "1", + "current_track": "1", + "current_section": "0", + "current_track_uri": f"x-sonos-htastream:{soco.uid}:spdif", + "current_track_duration": "", + "current_track_meta_data": { + "title": " ", + "parent_id": "-1", + "item_id": "-1", + "restricted": True, + "resources": [], + "desc": None, + }, + "next_track_uri": "", + "next_track_meta_data": "", + "enqueued_transport_uri": "", + "enqueued_transport_uri_meta_data": "", + "playback_storage_medium": "NETWORK", + "av_transport_uri": f"x-sonos-htastream:{soco.uid}:spdif", + "av_transport_uri_meta_data": { + "title": soco.uid, + "parent_id": "0", + "item_id": "spdif-input", + "restricted": False, + "resources": [], + "desc": None, + }, + "current_transport_actions": "Set, Play", + "current_valid_play_modes": "", + } + return SonosMockEvent(soco, soco.avTransport, variables) + + @pytest.fixture(autouse=True) def mock_get_source_ip(mock_get_source_ip): """Mock network util's async_get_source_ip in all sonos tests.""" diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index 8fb75789149..bda4e08cd25 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -1,9 +1,15 @@ """Tests for the Sonos battery sensor platform.""" +from unittest.mock import PropertyMock + from soco.exceptions import NotSupportedException +from homeassistant.components.sensor import SCAN_INTERVAL from homeassistant.components.sonos.binary_sensor import ATTR_BATTERY_POWER_SOURCE from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import entity_registry as ent_reg +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed async def test_entity_registry_unsupported(hass, async_setup_sonos, soco): @@ -113,14 +119,46 @@ async def test_device_payload_without_battery_and_ignored_keys( assert ignored_payload not in caplog.text -async def test_audio_input_sensor(hass, async_autosetup_sonos, soco): +async def test_audio_input_sensor( + hass, async_autosetup_sonos, soco, tv_event, no_media_event +): """Test audio input sensor.""" entity_registry = ent_reg.async_get(hass) + subscription = soco.avTransport.subscribe.return_value + sub_callback = subscription.callback + sub_callback(tv_event) + await hass.async_block_till_done() + audio_input_sensor = entity_registry.entities["sensor.zone_a_audio_input_format"] audio_input_state = hass.states.get(audio_input_sensor.entity_id) assert audio_input_state.state == "Dolby 5.1" + # Set mocked input format to new value and ensure poll success + no_input_mock = PropertyMock(return_value="No input") + type(soco).soundbar_audio_input_format = no_input_mock + + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + no_input_mock.assert_called_once() + audio_input_state = hass.states.get(audio_input_sensor.entity_id) + assert audio_input_state.state == "No input" + + # Ensure state is not polled when source is not TV and state is already "No input" + sub_callback(no_media_event) + await hass.async_block_till_done() + + unpolled_mock = PropertyMock(return_value="Will not be polled") + type(soco).soundbar_audio_input_format = unpolled_mock + + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + unpolled_mock.assert_not_called() + audio_input_state = hass.states.get(audio_input_sensor.entity_id) + assert audio_input_state.state == "No input" + async def test_microphone_binary_sensor( hass, async_autosetup_sonos, soco, device_properties_event