Add Sonos favorites sensor (#70235)

pull/70382/head
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_AUDIO_FORMAT_SENSOR = "sonos_create_audio_format_sensor"
SONOS_CREATE_BATTERY = "sonos_create_battery"
SONOS_CREATE_FAVORITES_SENSOR = "sonos_create_favorites_sensor"
SONOS_CREATE_MIC_SENSOR = "sonos_create_mic_sensor"
SONOS_CREATE_SWITCHES = "sonos_create_switches"
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.entity import DeviceInfo, Entity
from .const import (
DATA_SONOS,
DOMAIN,
SONOS_FALLBACK_POLL,
SONOS_FAVORITES_UPDATED,
SONOS_STATE_UPDATED,
)
from .const import DATA_SONOS, DOMAIN, SONOS_FALLBACK_POLL, SONOS_STATE_UPDATED
from .exception import SonosUpdateError
from .speaker import SonosSpeaker
@ -54,13 +48,6 @@ class SonosEntity(Entity):
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:
"""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.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 .household_coordinator import SonosHouseholdCoordinator
@ -37,6 +37,16 @@ class SonosFavorites(SonosHouseholdCoordinator):
favorites = self._favorites.copy()
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:
"""Return the favorite object with the provided item_id."""
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_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 .favorites import SonosFavorites
from .helpers import soco_error
from .speaker import SonosSpeaker
@ -40,6 +47,16 @@ async def async_setup_entry(
entity = SonosBatteryEntity(speaker)
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(
async_dispatcher_connect(
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):
"""Representation of a Sonos Battery entity."""
@ -107,3 +130,36 @@ class SonosAudioInputFormatSensorEntity(SonosPollingEntity, SensorEntity):
async def _async_fallback_poll(self) -> None:
"""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."""
from unittest.mock import PropertyMock
from datetime import timedelta
from unittest.mock import PropertyMock, patch
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.config_entries import RELOAD_AFTER_UPDATE_DELAY
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 .conftest import SonosMockEvent
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)
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()