"""Tests for the Sonos battery sensor platform.""" from collections.abc import Callable, Coroutine from datetime import timedelta from typing import Any from unittest.mock import PropertyMock, patch import pytest from soco.exceptions import NotSupportedException from homeassistant.components.sensor import SCAN_INTERVAL from homeassistant.components.sonos import DOMAIN from homeassistant.components.sonos.binary_sensor import ATTR_BATTERY_POWER_SOURCE from homeassistant.components.sonos.sensor import ( HA_POWER_SOURCE_BATTERY, HA_POWER_SOURCE_CHARGING_BASE, HA_POWER_SOURCE_USB, SensorDeviceClass, ) from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er, translation from homeassistant.util import dt as dt_util from .conftest import MockSoCo, SonosMockEvent from tests.common import async_fire_time_changed async def test_entity_registry_unsupported( hass: HomeAssistant, async_setup_sonos, soco, entity_registry: er.EntityRegistry ) -> None: """Test sonos device without battery registered in the device registry.""" soco.get_battery_info.side_effect = NotSupportedException await async_setup_sonos() await hass.async_block_till_done(wait_background_tasks=True) assert "media_player.zone_a" in entity_registry.entities assert "sensor.zone_a_battery" not in entity_registry.entities assert "binary_sensor.zone_a_charging" not in entity_registry.entities async def test_entity_registry_supported( hass: HomeAssistant, async_autosetup_sonos, soco, entity_registry: er.EntityRegistry ) -> None: """Test sonos device with battery registered in the device registry.""" await hass.async_block_till_done(wait_background_tasks=True) assert "media_player.zone_a" in entity_registry.entities assert "sensor.zone_a_battery" in entity_registry.entities assert "binary_sensor.zone_a_charging" in entity_registry.entities assert "sensor.zone_a_power_source" in entity_registry.entities @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_battery_attributes( hass: HomeAssistant, async_autosetup_sonos, soco, entity_registry: er.EntityRegistry ) -> None: """Test sonos device with battery state.""" battery = entity_registry.entities["sensor.zone_a_battery"] battery_state = hass.states.get(battery.entity_id) assert battery_state.state == "100" assert battery_state.attributes.get("unit_of_measurement") == "%" power = entity_registry.entities["binary_sensor.zone_a_charging"] power_state = hass.states.get(power.entity_id) assert power_state.state == STATE_ON assert ( power_state.attributes.get(ATTR_BATTERY_POWER_SOURCE) == "SONOS_CHARGING_RING" ) power_source = entity_registry.entities["sensor.zone_a_power_source"] power_source_state = hass.states.get(power_source.entity_id) assert power_source_state.state == HA_POWER_SOURCE_CHARGING_BASE assert power_source_state.attributes.get("device_class") == SensorDeviceClass.ENUM assert power_source_state.attributes.get("options") == [ HA_POWER_SOURCE_BATTERY, HA_POWER_SOURCE_CHARGING_BASE, HA_POWER_SOURCE_USB, ] result = translation.async_translate_state( hass, power_source_state.state, Platform.SENSOR, DOMAIN, power_source.translation_key, None, ) assert result == "Charging base" @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_power_source_unknown_state( hass: HomeAssistant, async_setup_sonos: Callable[[], Coroutine[Any, Any, None]], soco: MockSoCo, entity_registry: er.EntityRegistry, caplog: pytest.LogCaptureFixture, ) -> None: """Test bad value for power source.""" soco.get_battery_info.return_value = { "Level": 100, "PowerSource": "BAD_POWER_SOURCE", } with caplog.at_level("WARNING"): await async_setup_sonos() assert "Unknown power source" in caplog.text assert "BAD_POWER_SOURCE" in caplog.text assert "Zone A" in caplog.text power_source = entity_registry.entities["sensor.zone_a_power_source"] power_source_state = hass.states.get(power_source.entity_id) assert power_source_state.state == STATE_UNKNOWN @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_power_source_none( hass: HomeAssistant, async_setup_sonos: Callable[[], Coroutine[Any, Any, None]], soco: MockSoCo, entity_registry: er.EntityRegistry, caplog: pytest.LogCaptureFixture, ) -> None: """Test none value for power source.""" soco.get_battery_info.return_value = { "Level": 100, "PowerSource": None, } await async_setup_sonos() power_source = entity_registry.entities["sensor.zone_a_power_source"] power_source_state = hass.states.get(power_source.entity_id) assert power_source_state.state == STATE_UNAVAILABLE async def test_battery_on_s1( hass: HomeAssistant, async_setup_sonos, soco, device_properties_event, entity_registry: er.EntityRegistry, ) -> None: """Test battery state updates on a Sonos S1 device.""" soco.get_battery_info.return_value = {} await async_setup_sonos() await hass.async_block_till_done(wait_background_tasks=True) subscription = soco.deviceProperties.subscribe.return_value sub_callback = subscription.callback assert "sensor.zone_a_battery" not in entity_registry.entities assert "binary_sensor.zone_a_charging" not in entity_registry.entities # Update the speaker with a callback event sub_callback(device_properties_event) await hass.async_block_till_done(wait_background_tasks=True) battery = entity_registry.entities["sensor.zone_a_battery"] battery_state = hass.states.get(battery.entity_id) assert battery_state.state == "100" power = entity_registry.entities["binary_sensor.zone_a_charging"] power_state = hass.states.get(power.entity_id) assert power_state.state == STATE_OFF assert power_state.attributes.get(ATTR_BATTERY_POWER_SOURCE) == "BATTERY" async def test_device_payload_without_battery( hass: HomeAssistant, async_setup_sonos, soco, device_properties_event, caplog: pytest.LogCaptureFixture, ) -> None: """Test device properties event update without battery info.""" soco.get_battery_info.return_value = None await async_setup_sonos() await hass.async_block_till_done(wait_background_tasks=True) subscription = soco.deviceProperties.subscribe.return_value sub_callback = subscription.callback bad_payload = "BadKey:BadValue" device_properties_event.variables["more_info"] = bad_payload sub_callback(device_properties_event) await hass.async_block_till_done(wait_background_tasks=True) assert bad_payload in caplog.text async def test_device_payload_without_battery_and_ignored_keys( hass: HomeAssistant, async_setup_sonos, soco, device_properties_event, caplog: pytest.LogCaptureFixture, ) -> None: """Test device properties event update without battery info and ignored keys.""" soco.get_battery_info.return_value = None await async_setup_sonos() await hass.async_block_till_done(wait_background_tasks=True) subscription = soco.deviceProperties.subscribe.return_value sub_callback = subscription.callback ignored_payload = "SPID:InCeiling,TargetRoomName:Bouncy House" device_properties_event.variables["more_info"] = ignored_payload sub_callback(device_properties_event) await hass.async_block_till_done(wait_background_tasks=True) assert ignored_payload not in caplog.text async def test_audio_input_sensor( hass: HomeAssistant, async_autosetup_sonos, soco, tv_event, no_media_event, entity_registry: er.EntityRegistry, ) -> None: """Test audio input sensor.""" subscription = soco.avTransport.subscribe.return_value sub_callback = subscription.callback sub_callback(tv_event) await hass.async_block_till_done(wait_background_tasks=True) 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(wait_background_tasks=True) 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(wait_background_tasks=True) 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(wait_background_tasks=True) 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: HomeAssistant, async_autosetup_sonos, soco, device_properties_event, entity_registry: er.EntityRegistry, ) -> None: """Test microphone binary sensor.""" assert "binary_sensor.zone_a_microphone" in entity_registry.entities mic_binary_sensor = entity_registry.entities["binary_sensor.zone_a_microphone"] mic_binary_sensor_state = hass.states.get(mic_binary_sensor.entity_id) assert mic_binary_sensor_state.state == STATE_OFF # Update the speaker with a callback event subscription = soco.deviceProperties.subscribe.return_value subscription.callback(device_properties_event) await hass.async_block_till_done(wait_background_tasks=True) mic_binary_sensor_state = hass.states.get(mic_binary_sensor.entity_id) assert mic_binary_sensor_state.state == STATE_ON async def test_favorites_sensor( hass: HomeAssistant, async_autosetup_sonos, soco, fire_zgs_event, entity_registry: er.EntityRegistry, ) -> None: """Test Sonos favorites sensor.""" favorites = entity_registry.entities["sensor.sonos_favorites"] assert hass.states.get(favorites.entity_id) is None # Enable disabled sensor entity_registry.async_update_entity(entity_id=favorites.entity_id, disabled_by=None) await hass.async_block_till_done() # Fire event to cancel poll timer and avoid triggering errors during time jump service = soco.contentDirectory empty_event = SonosMockEvent(soco, service, {}) subscription = service.subscribe.return_value subscription.callback(event=empty_event) await hass.async_block_till_done(wait_background_tasks=True) # Reload the integration to enable the sensor async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), ) await hass.async_block_till_done(wait_background_tasks=True) # Trigger subscription callback for speaker discovery await fire_zgs_event() favorites_updated_event = SonosMockEvent( soco, service, {"favorites_update_id": "2", "container_update_i_ds": "FV:2,2"} ) with patch( "homeassistant.components.sonos.favorites.SonosFavorites.update_cache", return_value=True, ): subscription.callback(event=favorites_updated_event) await hass.async_block_till_done(wait_background_tasks=True)