mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
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:
parent
9742bfdf46
commit
d4ed65e0f5
67
homeassistant/components/sonos/binary_sensor.py
Normal file
67
homeassistant/components/sonos/binary_sensor.py
Normal 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,
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
"""Const for Sonos."""
|
||||
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.const import (
|
||||
MEDIA_CLASS_ALBUM,
|
||||
@ -22,7 +23,7 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
|
||||
DOMAIN = "sonos"
|
||||
DATA_SONOS = "sonos_media_player"
|
||||
PLATFORMS = {MP_DOMAIN, SENSOR_DOMAIN}
|
||||
PLATFORMS = {BINARY_SENSOR_DOMAIN, MP_DOMAIN, SENSOR_DOMAIN}
|
||||
|
||||
SONOS_ARTIST = "artists"
|
||||
SONOS_ALBUM = "albums"
|
||||
@ -128,12 +129,12 @@ PLAYABLE_MEDIA_TYPES = [
|
||||
]
|
||||
|
||||
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_UPDATE = "sonos_entity_update"
|
||||
SONOS_GROUP_UPDATE = "sonos_group_update"
|
||||
SONOS_MEDIA_UPDATE = "sonos_media_update"
|
||||
SONOS_PROPERTIES_UPDATE = "sonos_properties_update"
|
||||
SONOS_PLAYER_RECONNECTED = "sonos_player_reconnected"
|
||||
SONOS_STATE_UPDATED = "sonos_state_updated"
|
||||
SONOS_VOLUME_UPDATE = "sonos_properties_update"
|
||||
|
@ -7,11 +7,19 @@ from typing import Any
|
||||
from pysonos.core import SoCo
|
||||
|
||||
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 . 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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -71,3 +79,14 @@ class SonosEntity(Entity):
|
||||
def should_poll(self) -> bool:
|
||||
"""Return that we should not be polled (we handle that internally)."""
|
||||
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
|
||||
)
|
||||
|
@ -30,7 +30,6 @@ import voluptuous as vol
|
||||
from homeassistant.components.media_player import MediaPlayerEntity
|
||||
from homeassistant.components.media_player.const import (
|
||||
ATTR_MEDIA_ENQUEUE,
|
||||
DOMAIN as MP_DOMAIN,
|
||||
MEDIA_TYPE_ALBUM,
|
||||
MEDIA_TYPE_ARTIST,
|
||||
MEDIA_TYPE_MUSIC,
|
||||
@ -75,7 +74,7 @@ from .const import (
|
||||
MEDIA_TYPES_TO_SONOS,
|
||||
PLAYABLE_MEDIA_TYPES,
|
||||
SONOS_CONTENT_UPDATE,
|
||||
SONOS_DISCOVERY_UPDATE,
|
||||
SONOS_CREATE_MEDIA_PLAYER,
|
||||
SONOS_ENTITY_CREATED,
|
||||
SONOS_GROUP_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]
|
||||
)
|
||||
|
||||
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(
|
||||
SONOS_DOMAIN,
|
||||
@ -390,7 +391,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
async_dispatcher_send(self.hass, SONOS_GROUP_UPDATE)
|
||||
|
||||
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
|
||||
|
@ -1,133 +1,35 @@
|
||||
"""Entity representing a Sonos battery level."""
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import datetime
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pysonos.core import SoCo
|
||||
from pysonos.events_base import Event as SonosEvent
|
||||
from pysonos.exceptions import SoCoException
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorEntity
|
||||
from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE, STATE_UNKNOWN
|
||||
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 .const import DATA_SONOS, SONOS_CREATE_BATTERY
|
||||
from .entity import SonosSensorEntity
|
||||
from .speaker import SonosSpeaker
|
||||
|
||||
_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):
|
||||
"""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_add_entities([entity])
|
||||
|
||||
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])
|
||||
else:
|
||||
async_dispatcher_send(
|
||||
hass, f"{SONOS_ENTITY_CREATED}-{speaker.soco.uid}", SENSOR_DOMAIN
|
||||
)
|
||||
|
||||
async_dispatcher_connect(hass, SONOS_DISCOVERY_UPDATE, _async_create_entities)
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, SONOS_CREATE_BATTERY, _async_create_entity)
|
||||
)
|
||||
|
||||
|
||||
class SonosBatteryEntity(SonosEntity, SensorEntity):
|
||||
class SonosBatteryEntity(SonosSensorEntity, SensorEntity):
|
||||
"""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
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique ID of the sensor."""
|
||||
@ -148,51 +50,11 @@ class SonosBatteryEntity(SonosEntity, SensorEntity):
|
||||
"""Get the unit of measurement."""
|
||||
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."""
|
||||
if not self.available:
|
||||
# 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)
|
||||
await self.speaker.async_poll_battery()
|
||||
|
||||
@property
|
||||
def state(self) -> int | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self._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,
|
||||
}
|
||||
return self.speaker.battery_info.get("Level")
|
||||
|
@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import gather
|
||||
import contextlib
|
||||
import datetime
|
||||
import logging
|
||||
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.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.helpers.dispatcher import (
|
||||
async_dispatcher_send,
|
||||
dispatcher_connect,
|
||||
dispatcher_send,
|
||||
)
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
BATTERY_SCAN_INTERVAL,
|
||||
PLATFORMS,
|
||||
SCAN_INTERVAL,
|
||||
SEEN_EXPIRE_TIME,
|
||||
SONOS_CONTENT_UPDATE,
|
||||
SONOS_DISCOVERY_UPDATE,
|
||||
SONOS_CREATE_BATTERY,
|
||||
SONOS_CREATE_MEDIA_PLAYER,
|
||||
SONOS_ENTITY_CREATED,
|
||||
SONOS_ENTITY_UPDATE,
|
||||
SONOS_GROUP_UPDATE,
|
||||
SONOS_MEDIA_UPDATE,
|
||||
SONOS_PLAYER_RECONNECTED,
|
||||
SONOS_PROPERTIES_UPDATE,
|
||||
SONOS_SEEN,
|
||||
SONOS_STATE_UPDATED,
|
||||
SONOS_VOLUME_UPDATE,
|
||||
)
|
||||
|
||||
EVENT_CHARGING = {
|
||||
"CHARGING": True,
|
||||
"NOT_CHARGING": False,
|
||||
}
|
||||
|
||||
_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:
|
||||
"""Representation of a Sonos speaker."""
|
||||
|
||||
@ -60,6 +80,10 @@ class SonosSpeaker:
|
||||
self.version = speaker_info["software_version"]
|
||||
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:
|
||||
"""Run initial setup of the speaker."""
|
||||
self._entity_creation_dispatcher = dispatcher_connect(
|
||||
@ -70,7 +94,18 @@ class SonosSpeaker:
|
||||
self._seen_dispatcher = dispatcher_connect(
|
||||
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:
|
||||
"""Listen to new entities to trigger first subscription."""
|
||||
@ -149,9 +184,7 @@ class SonosSpeaker:
|
||||
@callback
|
||||
def async_dispatch_properties(self, event: SonosEvent | None = None) -> None:
|
||||
"""Update properties from event."""
|
||||
async_dispatcher_send(
|
||||
self.hass, f"{SONOS_PROPERTIES_UPDATE}-{self.soco.uid}", event
|
||||
)
|
||||
self.hass.async_create_task(self.async_update_device_properties(event))
|
||||
|
||||
@callback
|
||||
def async_dispatch_groups(self, event: SonosEvent | None = None) -> None:
|
||||
@ -217,3 +250,57 @@ class SonosSpeaker:
|
||||
await subscription.unsubscribe()
|
||||
|
||||
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()
|
||||
|
@ -2,6 +2,8 @@
|
||||
from pysonos.exceptions import NotSupportedException
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -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 "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):
|
||||
@ -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 "sensor.zone_a_battery" 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
|
||||
assert "binary_sensor.zone_a_power" in entity_registry.entities
|
||||
|
||||
|
||||
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_state = hass.states.get(battery.entity_id)
|
||||
|
||||
# confirm initial state from conftest
|
||||
assert battery_state.state == "100"
|
||||
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"
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user