Co-authored-by: Walter Huf <hufman@gmail.com> Co-authored-by: J. Nick Koston <nick@koston.org>
204 lines
6.4 KiB
Python
204 lines
6.4 KiB
Python
"""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 DOMAIN as SENSOR_DOMAIN
|
|
from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE, STATE_UNKNOWN
|
|
from homeassistant.helpers.dispatcher import (
|
|
async_dispatcher_connect,
|
|
async_dispatcher_send,
|
|
)
|
|
from homeassistant.helpers.entity import Entity
|
|
from homeassistant.helpers.icon import icon_for_battery_level
|
|
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
|
|
|
|
_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()
|
|
|
|
|
|
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) -> 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)
|
|
|
|
|
|
class SonosBatteryEntity(SonosEntity, Entity):
|
|
"""Representation of a Sonos Battery entity."""
|
|
|
|
def __init__(
|
|
self, speaker: SonosSpeaker, sonos_data: SonosData, battery_info: dict[str, Any]
|
|
):
|
|
"""Initialize a SonosBatteryEntity."""
|
|
super().__init__(speaker, sonos_data)
|
|
self._battery_info: dict[str, Any] = battery_info
|
|
self._last_event: datetime.datetime = 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."""
|
|
return f"{self.soco.uid}-battery"
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
"""Return the name of the sensor."""
|
|
return f"{self.speaker.zone_name} Battery"
|
|
|
|
@property
|
|
def device_class(self) -> str:
|
|
"""Return the entity's device class."""
|
|
return DEVICE_CLASS_BATTERY
|
|
|
|
@property
|
|
def unit_of_measurement(self) -> str:
|
|
"""Get the unit of measurement."""
|
|
return PERCENTAGE
|
|
|
|
async def async_update(self, event=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)
|
|
|
|
@property
|
|
def icon(self) -> str:
|
|
"""Return the icon of the sensor."""
|
|
return icon_for_battery_level(self.battery_level, self.charging)
|
|
|
|
@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,
|
|
}
|