Add power binary_sensor support to Sonos (#49730)

* Add power binary_sensor support to Sonos

* Prepare for future unloading of config entries

* Remove unnecessary calls to super() inits

* Add binary_sensor to tests, remove invalid test for empty battery payload

* Move sensor added_to_hass to common sensor class

* Avoid dispatching sensors if no battery

* Use proper attributes property

* Remove power source fallback

* Update homeassistant/components/sonos/speaker.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
jjlawren 2021-04-27 09:52:05 -05:00 committed by GitHub
parent 9742bfdf46
commit d4ed65e0f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 216 additions and 183 deletions

View File

@ -0,0 +1,67 @@
"""Entity representing a Sonos power sensor."""
from __future__ import annotations
import datetime
import logging
from typing import Any
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_BATTERY_CHARGING,
BinarySensorEntity,
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import DATA_SONOS, SONOS_CREATE_BATTERY
from .entity import SonosSensorEntity
from .speaker import SonosSpeaker
_LOGGER = logging.getLogger(__name__)
ATTR_BATTERY_POWER_SOURCE = "power_source"
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Sonos from a config entry."""
async def _async_create_entity(speaker: SonosSpeaker) -> None:
entity = SonosPowerEntity(speaker, hass.data[DATA_SONOS])
async_add_entities([entity])
config_entry.async_on_unload(
async_dispatcher_connect(hass, SONOS_CREATE_BATTERY, _async_create_entity)
)
class SonosPowerEntity(SonosSensorEntity, BinarySensorEntity):
"""Representation of a Sonos power entity."""
@property
def unique_id(self) -> str:
"""Return the unique ID of the sensor."""
return f"{self.soco.uid}-power"
@property
def name(self) -> str:
"""Return the name of the sensor."""
return f"{self.speaker.zone_name} Power"
@property
def device_class(self) -> str:
"""Return the entity's device class."""
return DEVICE_CLASS_BATTERY_CHARGING
async def async_update(self, now: datetime.datetime | None = None) -> None:
"""Poll the device for the current state."""
await self.speaker.async_poll_battery()
@property
def is_on(self) -> bool | None:
"""Return the state of the binary sensor."""
return self.speaker.charging
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return entity specific state attributes."""
return {
ATTR_BATTERY_POWER_SOURCE: self.speaker.power_source,
}

View File

@ -1,6 +1,7 @@
"""Const for Sonos.""" """Const for Sonos."""
import datetime import datetime
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
MEDIA_CLASS_ALBUM, MEDIA_CLASS_ALBUM,
@ -22,7 +23,7 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
DOMAIN = "sonos" DOMAIN = "sonos"
DATA_SONOS = "sonos_media_player" DATA_SONOS = "sonos_media_player"
PLATFORMS = {MP_DOMAIN, SENSOR_DOMAIN} PLATFORMS = {BINARY_SENSOR_DOMAIN, MP_DOMAIN, SENSOR_DOMAIN}
SONOS_ARTIST = "artists" SONOS_ARTIST = "artists"
SONOS_ALBUM = "albums" SONOS_ALBUM = "albums"
@ -128,12 +129,12 @@ PLAYABLE_MEDIA_TYPES = [
] ]
SONOS_CONTENT_UPDATE = "sonos_content_update" SONOS_CONTENT_UPDATE = "sonos_content_update"
SONOS_DISCOVERY_UPDATE = "sonos_discovery_update" SONOS_CREATE_BATTERY = "sonos_create_battery"
SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player"
SONOS_ENTITY_CREATED = "sonos_entity_created" SONOS_ENTITY_CREATED = "sonos_entity_created"
SONOS_ENTITY_UPDATE = "sonos_entity_update" SONOS_ENTITY_UPDATE = "sonos_entity_update"
SONOS_GROUP_UPDATE = "sonos_group_update" SONOS_GROUP_UPDATE = "sonos_group_update"
SONOS_MEDIA_UPDATE = "sonos_media_update" SONOS_MEDIA_UPDATE = "sonos_media_update"
SONOS_PROPERTIES_UPDATE = "sonos_properties_update"
SONOS_PLAYER_RECONNECTED = "sonos_player_reconnected" SONOS_PLAYER_RECONNECTED = "sonos_player_reconnected"
SONOS_STATE_UPDATED = "sonos_state_updated" SONOS_STATE_UPDATED = "sonos_state_updated"
SONOS_VOLUME_UPDATE = "sonos_properties_update" SONOS_VOLUME_UPDATE = "sonos_properties_update"

View File

@ -7,11 +7,19 @@ from typing import Any
from pysonos.core import SoCo from pysonos.core import SoCo
import homeassistant.helpers.device_registry as dr import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from . import SonosData from . import SonosData
from .const import DOMAIN, SONOS_ENTITY_UPDATE, SONOS_STATE_UPDATED from .const import (
DOMAIN,
SONOS_ENTITY_CREATED,
SONOS_ENTITY_UPDATE,
SONOS_STATE_UPDATED,
)
from .speaker import SonosSpeaker from .speaker import SonosSpeaker
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -71,3 +79,14 @@ class SonosEntity(Entity):
def should_poll(self) -> bool: def should_poll(self) -> bool:
"""Return that we should not be polled (we handle that internally).""" """Return that we should not be polled (we handle that internally)."""
return False return False
class SonosSensorEntity(SonosEntity):
"""Representation of a Sonos sensor entity."""
async def async_added_to_hass(self) -> None:
"""Handle common setup when added to hass."""
await super().async_added_to_hass()
async_dispatcher_send(
self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.platform.domain
)

View File

@ -30,7 +30,6 @@ import voluptuous as vol
from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player import MediaPlayerEntity
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_ENQUEUE,
DOMAIN as MP_DOMAIN,
MEDIA_TYPE_ALBUM, MEDIA_TYPE_ALBUM,
MEDIA_TYPE_ARTIST, MEDIA_TYPE_ARTIST,
MEDIA_TYPE_MUSIC, MEDIA_TYPE_MUSIC,
@ -75,7 +74,7 @@ from .const import (
MEDIA_TYPES_TO_SONOS, MEDIA_TYPES_TO_SONOS,
PLAYABLE_MEDIA_TYPES, PLAYABLE_MEDIA_TYPES,
SONOS_CONTENT_UPDATE, SONOS_CONTENT_UPDATE,
SONOS_DISCOVERY_UPDATE, SONOS_CREATE_MEDIA_PLAYER,
SONOS_ENTITY_CREATED, SONOS_ENTITY_CREATED,
SONOS_GROUP_UPDATE, SONOS_GROUP_UPDATE,
SONOS_MEDIA_UPDATE, SONOS_MEDIA_UPDATE,
@ -189,7 +188,9 @@ async def async_setup_entry(
hass, entities, service_call.data[ATTR_WITH_GROUP] # type: ignore[arg-type] hass, entities, service_call.data[ATTR_WITH_GROUP] # type: ignore[arg-type]
) )
async_dispatcher_connect(hass, SONOS_DISCOVERY_UPDATE, async_create_entities) config_entry.async_on_unload(
async_dispatcher_connect(hass, SONOS_CREATE_MEDIA_PLAYER, async_create_entities)
)
hass.services.async_register( hass.services.async_register(
SONOS_DOMAIN, SONOS_DOMAIN,
@ -390,7 +391,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
async_dispatcher_send(self.hass, SONOS_GROUP_UPDATE) async_dispatcher_send(self.hass, SONOS_GROUP_UPDATE)
async_dispatcher_send( async_dispatcher_send(
self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", MP_DOMAIN self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.platform.domain
) )
@property @property

View File

@ -1,133 +1,35 @@
"""Entity representing a Sonos battery level.""" """Entity representing a Sonos battery level."""
from __future__ import annotations from __future__ import annotations
import contextlib
import datetime import datetime
import logging import logging
from typing import Any
from pysonos.core import SoCo from homeassistant.components.sensor import SensorEntity
from pysonos.events_base import Event as SonosEvent from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE
from pysonos.exceptions import SoCoException from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorEntity from .const import DATA_SONOS, SONOS_CREATE_BATTERY
from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE, STATE_UNKNOWN from .entity import SonosSensorEntity
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.util import dt as dt_util
from . import SonosData
from .const import (
BATTERY_SCAN_INTERVAL,
DATA_SONOS,
SONOS_DISCOVERY_UPDATE,
SONOS_ENTITY_CREATED,
SONOS_PROPERTIES_UPDATE,
)
from .entity import SonosEntity
from .speaker import SonosSpeaker from .speaker import SonosSpeaker
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_BATTERY_LEVEL = "battery_level"
ATTR_BATTERY_CHARGING = "charging"
ATTR_BATTERY_POWERSOURCE = "power_source"
EVENT_CHARGING = {
"CHARGING": True,
"NOT_CHARGING": False,
}
def fetch_battery_info_or_none(soco: SoCo) -> dict[str, Any] | None:
"""Fetch battery_info from the given SoCo object.
Returns None if the device doesn't support battery info
or if the device is offline.
"""
with contextlib.suppress(ConnectionError, TimeoutError, SoCoException):
return soco.get_battery_info()
return None
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Sonos from a config entry.""" """Set up Sonos from a config entry."""
sonos_data = hass.data[DATA_SONOS] async def _async_create_entity(speaker: SonosSpeaker) -> None:
entity = SonosBatteryEntity(speaker, hass.data[DATA_SONOS])
async def _async_create_entity(speaker: SonosSpeaker) -> SonosBatteryEntity | None:
if battery_info := await hass.async_add_executor_job(
fetch_battery_info_or_none, speaker.soco
):
return SonosBatteryEntity(speaker, sonos_data, battery_info)
return None
async def _async_create_entities(speaker: SonosSpeaker):
if entity := await _async_create_entity(speaker):
async_add_entities([entity]) async_add_entities([entity])
else:
async_dispatcher_send( config_entry.async_on_unload(
hass, f"{SONOS_ENTITY_CREATED}-{speaker.soco.uid}", SENSOR_DOMAIN async_dispatcher_connect(hass, SONOS_CREATE_BATTERY, _async_create_entity)
) )
async_dispatcher_connect(hass, SONOS_DISCOVERY_UPDATE, _async_create_entities)
class SonosBatteryEntity(SonosSensorEntity, SensorEntity):
class SonosBatteryEntity(SonosEntity, SensorEntity):
"""Representation of a Sonos Battery entity.""" """Representation of a Sonos Battery entity."""
def __init__(
self, speaker: SonosSpeaker, sonos_data: SonosData, battery_info: dict[str, Any]
) -> None:
"""Initialize a SonosBatteryEntity."""
super().__init__(speaker, sonos_data)
self._battery_info: dict[str, Any] = battery_info
self._last_event: datetime.datetime | None = None
async def async_added_to_hass(self) -> None:
"""Register polling callback when added to hass."""
await super().async_added_to_hass()
self.async_on_remove(
self.hass.helpers.event.async_track_time_interval(
self.async_update, BATTERY_SCAN_INTERVAL
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{SONOS_PROPERTIES_UPDATE}-{self.soco.uid}",
self.async_update_battery_info,
)
)
async_dispatcher_send(
self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", SENSOR_DOMAIN
)
async def async_update_battery_info(self, event: SonosEvent = None) -> None:
"""Update battery info using the provided SonosEvent."""
if event is None:
return
if (more_info := event.variables.get("more_info")) is None:
return
more_info_dict = dict(x.split(":") for x in more_info.split(","))
self._last_event = dt_util.utcnow()
is_charging = EVENT_CHARGING[more_info_dict["BattChg"]]
if is_charging == self.charging:
self._battery_info.update({"Level": int(more_info_dict["BattPct"])})
else:
if battery_info := await self.hass.async_add_executor_job(
fetch_battery_info_or_none, self.soco
):
self._battery_info = battery_info
self.async_write_ha_state()
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Return the unique ID of the sensor.""" """Return the unique ID of the sensor."""
@ -148,51 +50,11 @@ class SonosBatteryEntity(SonosEntity, SensorEntity):
"""Get the unit of measurement.""" """Get the unit of measurement."""
return PERCENTAGE return PERCENTAGE
async def async_update(self, event=None) -> None: async def async_update(self, now: datetime.datetime | None = None) -> None:
"""Poll the device for the current state.""" """Poll the device for the current state."""
if not self.available: await self.speaker.async_poll_battery()
# wait for the Sonos device to come back online
return
if (
self._last_event
and dt_util.utcnow() - self._last_event < BATTERY_SCAN_INTERVAL
):
return
if battery_info := await self.hass.async_add_executor_job(
fetch_battery_info_or_none, self.soco
):
self._battery_info = battery_info
self.async_write_ha_state()
@property
def battery_level(self) -> int:
"""Return the battery level."""
return self._battery_info.get("Level", 0)
@property
def power_source(self) -> str:
"""Return the name of the power source.
Observed to be either BATTERY or SONOS_CHARGING_RING or USB_POWER.
"""
return self._battery_info.get("PowerSource", STATE_UNKNOWN)
@property
def charging(self) -> bool:
"""Return the charging status of this battery."""
return self.power_source not in ("BATTERY", STATE_UNKNOWN)
@property @property
def state(self) -> int | None: def state(self) -> int | None:
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self._battery_info.get("Level") return self.speaker.battery_info.get("Level")
@property
def device_state_attributes(self) -> dict[str, Any]:
"""Return entity specific state attributes."""
return {
ATTR_BATTERY_CHARGING: self.charging,
ATTR_BATTERY_POWERSOURCE: self.power_source,
}

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from asyncio import gather from asyncio import gather
import contextlib
import datetime import datetime
import logging import logging
from typing import Any, Callable from typing import Any, Callable
@ -10,33 +11,52 @@ from pysonos.core import SoCo
from pysonos.events_base import Event as SonosEvent, SubscriptionBase from pysonos.events_base import Event as SonosEvent, SubscriptionBase
from pysonos.exceptions import SoCoException from pysonos.exceptions import SoCoException
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
async_dispatcher_send, async_dispatcher_send,
dispatcher_connect, dispatcher_connect,
dispatcher_send, dispatcher_send,
) )
from homeassistant.util import dt as dt_util
from .const import ( from .const import (
BATTERY_SCAN_INTERVAL,
PLATFORMS, PLATFORMS,
SCAN_INTERVAL, SCAN_INTERVAL,
SEEN_EXPIRE_TIME, SEEN_EXPIRE_TIME,
SONOS_CONTENT_UPDATE, SONOS_CONTENT_UPDATE,
SONOS_DISCOVERY_UPDATE, SONOS_CREATE_BATTERY,
SONOS_CREATE_MEDIA_PLAYER,
SONOS_ENTITY_CREATED, SONOS_ENTITY_CREATED,
SONOS_ENTITY_UPDATE, SONOS_ENTITY_UPDATE,
SONOS_GROUP_UPDATE, SONOS_GROUP_UPDATE,
SONOS_MEDIA_UPDATE, SONOS_MEDIA_UPDATE,
SONOS_PLAYER_RECONNECTED, SONOS_PLAYER_RECONNECTED,
SONOS_PROPERTIES_UPDATE,
SONOS_SEEN, SONOS_SEEN,
SONOS_STATE_UPDATED, SONOS_STATE_UPDATED,
SONOS_VOLUME_UPDATE, SONOS_VOLUME_UPDATE,
) )
EVENT_CHARGING = {
"CHARGING": True,
"NOT_CHARGING": False,
}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def fetch_battery_info_or_none(soco: SoCo) -> dict[str, Any] | None:
"""Fetch battery_info from the given SoCo object.
Returns None if the device doesn't support battery info
or if the device is offline.
"""
with contextlib.suppress(ConnectionError, TimeoutError, SoCoException):
return soco.get_battery_info()
class SonosSpeaker: class SonosSpeaker:
"""Representation of a Sonos speaker.""" """Representation of a Sonos speaker."""
@ -60,6 +80,10 @@ class SonosSpeaker:
self.version = speaker_info["software_version"] self.version = speaker_info["software_version"]
self.zone_name = speaker_info["zone_name"] self.zone_name = speaker_info["zone_name"]
self.battery_info: dict[str, Any] | None = None
self._last_battery_event: datetime.datetime | None = None
self._battery_poll_timer: Callable | None = None
def setup(self) -> None: def setup(self) -> None:
"""Run initial setup of the speaker.""" """Run initial setup of the speaker."""
self._entity_creation_dispatcher = dispatcher_connect( self._entity_creation_dispatcher = dispatcher_connect(
@ -70,7 +94,18 @@ class SonosSpeaker:
self._seen_dispatcher = dispatcher_connect( self._seen_dispatcher = dispatcher_connect(
self.hass, f"{SONOS_SEEN}-{self.soco.uid}", self.async_seen self.hass, f"{SONOS_SEEN}-{self.soco.uid}", self.async_seen
) )
dispatcher_send(self.hass, SONOS_DISCOVERY_UPDATE, self)
if (battery_info := fetch_battery_info_or_none(self.soco)) is not None:
# Battery events can be infrequent, polling is still necessary
self.battery_info = battery_info
self._battery_poll_timer = self.hass.helpers.event.track_time_interval(
self.async_poll_battery, BATTERY_SCAN_INTERVAL
)
dispatcher_send(self.hass, SONOS_CREATE_BATTERY, self)
else:
self._platforms_ready.update({BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN})
dispatcher_send(self.hass, SONOS_CREATE_MEDIA_PLAYER, self)
async def async_handle_new_entity(self, entity_type: str) -> None: async def async_handle_new_entity(self, entity_type: str) -> None:
"""Listen to new entities to trigger first subscription.""" """Listen to new entities to trigger first subscription."""
@ -149,9 +184,7 @@ class SonosSpeaker:
@callback @callback
def async_dispatch_properties(self, event: SonosEvent | None = None) -> None: def async_dispatch_properties(self, event: SonosEvent | None = None) -> None:
"""Update properties from event.""" """Update properties from event."""
async_dispatcher_send( self.hass.async_create_task(self.async_update_device_properties(event))
self.hass, f"{SONOS_PROPERTIES_UPDATE}-{self.soco.uid}", event
)
@callback @callback
def async_dispatch_groups(self, event: SonosEvent | None = None) -> None: def async_dispatch_groups(self, event: SonosEvent | None = None) -> None:
@ -217,3 +250,57 @@ class SonosSpeaker:
await subscription.unsubscribe() await subscription.unsubscribe()
self._subscriptions = [] self._subscriptions = []
async def async_update_device_properties(self, event: SonosEvent = None) -> None:
"""Update device properties using the provided SonosEvent."""
if event is None:
return
if (more_info := event.variables.get("more_info")) is not None:
battery_dict = dict(x.split(":") for x in more_info.split(","))
await self.async_update_battery_info(battery_dict)
self.async_write_entity_states()
async def async_update_battery_info(self, battery_dict: dict[str, Any]) -> None:
"""Update battery info using the decoded SonosEvent."""
self._last_battery_event = dt_util.utcnow()
is_charging = EVENT_CHARGING[battery_dict["BattChg"]]
if is_charging == self.charging:
self.battery_info.update({"Level": int(battery_dict["BattPct"])})
else:
if battery_info := await self.hass.async_add_executor_job(
fetch_battery_info_or_none, self.soco
):
self.battery_info = battery_info
@property
def power_source(self) -> str:
"""Return the name of the current power source.
Observed to be either BATTERY or SONOS_CHARGING_RING or USB_POWER.
"""
return self.battery_info["PowerSource"]
@property
def charging(self) -> bool:
"""Return the charging status of the speaker."""
return self.power_source != "BATTERY"
async def async_poll_battery(self, now: datetime.datetime | None = None) -> None:
"""Poll the device for the current battery state."""
if not self.available:
return
if (
self._last_battery_event
and dt_util.utcnow() - self._last_battery_event < BATTERY_SCAN_INTERVAL
):
return
if battery_info := await self.hass.async_add_executor_job(
fetch_battery_info_or_none, self.soco
):
self.battery_info = battery_info
self.async_write_entity_states()

View File

@ -2,6 +2,8 @@
from pysonos.exceptions import NotSupportedException from pysonos.exceptions import NotSupportedException
from homeassistant.components.sonos import DOMAIN from homeassistant.components.sonos import DOMAIN
from homeassistant.components.sonos.binary_sensor import ATTR_BATTERY_POWER_SOURCE
from homeassistant.const import STATE_ON
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -22,6 +24,7 @@ async def test_entity_registry_unsupported(hass, config_entry, config, soco):
assert "media_player.zone_a" in entity_registry.entities assert "media_player.zone_a" in entity_registry.entities
assert "sensor.zone_a_battery" not in entity_registry.entities assert "sensor.zone_a_battery" not in entity_registry.entities
assert "binary_sensor.zone_a_power" not in entity_registry.entities
async def test_entity_registry_supported(hass, config_entry, config, soco): async def test_entity_registry_supported(hass, config_entry, config, soco):
@ -32,17 +35,7 @@ async def test_entity_registry_supported(hass, config_entry, config, soco):
assert "media_player.zone_a" in entity_registry.entities assert "media_player.zone_a" in entity_registry.entities
assert "sensor.zone_a_battery" in entity_registry.entities assert "sensor.zone_a_battery" in entity_registry.entities
assert "binary_sensor.zone_a_power" in entity_registry.entities
async def test_battery_missing_attributes(hass, config_entry, config, soco):
"""Test sonos device with unknown battery state."""
soco.get_battery_info.return_value = {}
await setup_platform(hass, config_entry, config)
entity_registry = await hass.helpers.entity_registry.async_get_registry()
assert entity_registry.entities.get("sensor.zone_a_battery") is None
async def test_battery_attributes(hass, config_entry, config, soco): async def test_battery_attributes(hass, config_entry, config, soco):
@ -53,9 +46,12 @@ async def test_battery_attributes(hass, config_entry, config, soco):
battery = entity_registry.entities["sensor.zone_a_battery"] battery = entity_registry.entities["sensor.zone_a_battery"]
battery_state = hass.states.get(battery.entity_id) battery_state = hass.states.get(battery.entity_id)
# confirm initial state from conftest
assert battery_state.state == "100" assert battery_state.state == "100"
assert battery_state.attributes.get("unit_of_measurement") == "%" assert battery_state.attributes.get("unit_of_measurement") == "%"
assert battery_state.attributes.get("charging")
assert battery_state.attributes.get("power_source") == "SONOS_CHARGING_RING" power = entity_registry.entities["binary_sensor.zone_a_power"]
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"
)