Add Sonos favorites sensor (#70235)

This commit is contained in:
jjlawren 2022-04-21 12:37:16 -05:00 committed by GitHub
parent 9bec649323
commit ac88d0be14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 111 additions and 18 deletions

View File

@ -152,6 +152,7 @@ SONOS_CHECK_ACTIVITY = "sonos_check_activity"
SONOS_CREATE_ALARM = "sonos_create_alarm" SONOS_CREATE_ALARM = "sonos_create_alarm"
SONOS_CREATE_AUDIO_FORMAT_SENSOR = "sonos_create_audio_format_sensor" SONOS_CREATE_AUDIO_FORMAT_SENSOR = "sonos_create_audio_format_sensor"
SONOS_CREATE_BATTERY = "sonos_create_battery" SONOS_CREATE_BATTERY = "sonos_create_battery"
SONOS_CREATE_FAVORITES_SENSOR = "sonos_create_favorites_sensor"
SONOS_CREATE_MIC_SENSOR = "sonos_create_mic_sensor" SONOS_CREATE_MIC_SENSOR = "sonos_create_mic_sensor"
SONOS_CREATE_SWITCHES = "sonos_create_switches" SONOS_CREATE_SWITCHES = "sonos_create_switches"
SONOS_CREATE_LEVELS = "sonos_create_levels" SONOS_CREATE_LEVELS = "sonos_create_levels"

View File

@ -13,13 +13,7 @@ import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity import DeviceInfo, Entity
from .const import ( from .const import DATA_SONOS, DOMAIN, SONOS_FALLBACK_POLL, SONOS_STATE_UPDATED
DATA_SONOS,
DOMAIN,
SONOS_FALLBACK_POLL,
SONOS_FAVORITES_UPDATED,
SONOS_STATE_UPDATED,
)
from .exception import SonosUpdateError from .exception import SonosUpdateError
from .speaker import SonosSpeaker from .speaker import SonosSpeaker
@ -54,13 +48,6 @@ class SonosEntity(Entity):
self.async_write_ha_state, self.async_write_ha_state,
) )
) )
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{SONOS_FAVORITES_UPDATED}-{self.soco.household_id}",
self.async_write_ha_state,
)
)
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Clean up when entity is removed.""" """Clean up when entity is removed."""

View File

@ -11,9 +11,9 @@ from soco.data_structures import DidlFavorite
from soco.events_base import Event as SonosEvent from soco.events_base import Event as SonosEvent
from soco.exceptions import SoCoException from soco.exceptions import SoCoException
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send
from .const import SONOS_FAVORITES_UPDATED from .const import SONOS_CREATE_FAVORITES_SENSOR, SONOS_FAVORITES_UPDATED
from .helpers import soco_error from .helpers import soco_error
from .household_coordinator import SonosHouseholdCoordinator from .household_coordinator import SonosHouseholdCoordinator
@ -37,6 +37,16 @@ class SonosFavorites(SonosHouseholdCoordinator):
favorites = self._favorites.copy() favorites = self._favorites.copy()
return iter(favorites) return iter(favorites)
def setup(self, soco: SoCo) -> None:
"""Override to send a signal on base class setup completion."""
super().setup(soco)
dispatcher_send(self.hass, SONOS_CREATE_FAVORITES_SENSOR, self)
@property
def count(self) -> int:
"""Return the number of favorites."""
return len(self._favorites)
def lookup_by_item_id(self, item_id: str) -> DidlFavorite | None: def lookup_by_item_id(self, item_id: str) -> DidlFavorite | None:
"""Return the favorite object with the provided item_id.""" """Return the favorite object with the provided item_id."""
return next((fav for fav in self._favorites if fav.item_id == item_id), None) return next((fav for fav in self._favorites if fav.item_id == item_id), None)

View File

@ -11,8 +11,15 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SONOS_CREATE_AUDIO_FORMAT_SENSOR, SONOS_CREATE_BATTERY, SOURCE_TV from .const import (
SONOS_CREATE_AUDIO_FORMAT_SENSOR,
SONOS_CREATE_BATTERY,
SONOS_CREATE_FAVORITES_SENSOR,
SONOS_FAVORITES_UPDATED,
SOURCE_TV,
)
from .entity import SonosEntity, SonosPollingEntity from .entity import SonosEntity, SonosPollingEntity
from .favorites import SonosFavorites
from .helpers import soco_error from .helpers import soco_error
from .speaker import SonosSpeaker from .speaker import SonosSpeaker
@ -40,6 +47,16 @@ async def async_setup_entry(
entity = SonosBatteryEntity(speaker) entity = SonosBatteryEntity(speaker)
async_add_entities([entity]) async_add_entities([entity])
@callback
def _async_create_favorites_sensor(favorites: SonosFavorites) -> None:
_LOGGER.debug(
"Creating favorites sensor (%s items) for household %s",
favorites.count,
favorites.household_id,
)
entity = SonosFavoritesEntity(favorites)
async_add_entities([entity])
config_entry.async_on_unload( config_entry.async_on_unload(
async_dispatcher_connect( async_dispatcher_connect(
hass, SONOS_CREATE_AUDIO_FORMAT_SENSOR, _async_create_audio_format_entity hass, SONOS_CREATE_AUDIO_FORMAT_SENSOR, _async_create_audio_format_entity
@ -51,6 +68,12 @@ async def async_setup_entry(
) )
) )
config_entry.async_on_unload(
async_dispatcher_connect(
hass, SONOS_CREATE_FAVORITES_SENSOR, _async_create_favorites_sensor
)
)
class SonosBatteryEntity(SonosEntity, SensorEntity): class SonosBatteryEntity(SonosEntity, SensorEntity):
"""Representation of a Sonos Battery entity.""" """Representation of a Sonos Battery entity."""
@ -107,3 +130,36 @@ class SonosAudioInputFormatSensorEntity(SonosPollingEntity, SensorEntity):
async def _async_fallback_poll(self) -> None: async def _async_fallback_poll(self) -> None:
"""Provide a stub for required ABC method.""" """Provide a stub for required ABC method."""
class SonosFavoritesEntity(SensorEntity):
"""Representation of a Sonos favorites info entity."""
_attr_entity_registry_enabled_default = False
_attr_icon = "mdi:star"
_attr_name = "Sonos Favorites"
_attr_native_unit_of_measurement = "items"
_attr_should_poll = False
def __init__(self, favorites: SonosFavorites) -> None:
"""Initialize the favorites sensor."""
self.favorites = favorites
self._attr_unique_id = f"{favorites.household_id}-favorites"
async def async_added_to_hass(self) -> None:
"""Handle common setup when added to hass."""
await self._async_update_state()
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{SONOS_FAVORITES_UPDATED}-{self.favorites.household_id}",
self._async_update_state,
)
)
async def _async_update_state(self) -> None:
self._attr_native_value = self.favorites.count
self._attr_extra_state_attributes = {
"items": {fav.item_id: fav.title for fav in self.favorites}
}
self.async_write_ha_state()

View File

@ -1,14 +1,18 @@
"""Tests for the Sonos battery sensor platform.""" """Tests for the Sonos battery sensor platform."""
from unittest.mock import PropertyMock from datetime import timedelta
from unittest.mock import PropertyMock, patch
from soco.exceptions import NotSupportedException from soco.exceptions import NotSupportedException
from homeassistant.components.sensor import SCAN_INTERVAL from homeassistant.components.sensor import SCAN_INTERVAL
from homeassistant.components.sonos.binary_sensor import ATTR_BATTERY_POWER_SOURCE from homeassistant.components.sonos.binary_sensor import ATTR_BATTERY_POWER_SOURCE
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.helpers import entity_registry as ent_reg from homeassistant.helpers import entity_registry as ent_reg
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .conftest import SonosMockEvent
from tests.common import async_fire_time_changed from tests.common import async_fire_time_changed
@ -178,3 +182,38 @@ async def test_microphone_binary_sensor(
mic_binary_sensor_state = hass.states.get(mic_binary_sensor.entity_id) mic_binary_sensor_state = hass.states.get(mic_binary_sensor.entity_id)
assert mic_binary_sensor_state.state == STATE_ON assert mic_binary_sensor_state.state == STATE_ON
async def test_favorites_sensor(hass, async_autosetup_sonos, soco):
"""Test Sonos favorites sensor."""
entity_registry = ent_reg.async_get(hass)
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()
# 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()
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()