diff --git a/__init__.py b/__init__.py index fe32bc4..04494fd 100644 --- a/__init__.py +++ b/__init__.py @@ -6,13 +6,14 @@ from random import choice from typing import Any +import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from surepy import Surepy from surepy.enums import LockState from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError @@ -26,7 +27,6 @@ SERVICE_SET_LOCK_STATE, SPC, SURE_API_TIMEOUT, - TOPIC_UPDATE, ) _LOGGER = logging.getLogger(__name__) @@ -84,6 +84,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return False spc = SurePetcareAPI(hass, entry, surepy) + + async def async_update_data(): + + try: + # asyncio.TimeoutError and aiohttp.ClientError already handled + + async with async_timeout.timeout(20): + return await spc.surepy.get_entities(refresh=True) + + except SurePetcareAuthenticationError as err: + raise ConfigEntryAuthFailed from err + except SurePetcareError as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + + spc.coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="sureha_sensors", + update_method=async_update_data, + update_interval=timedelta(seconds=150), + ) + + await spc.coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][SPC] = spc return await spc.async_setup() @@ -97,26 +121,13 @@ def __init__( ) -> None: """Initialize the Sure Petcare object.""" + self.coordinator: DataUpdateCoordinator + self.hass = hass self.config_entry = config_entry self.surepy = surepy - self.states: dict[int, Any] = {} - async def async_update(self, _: Any = None) -> None: - """Get the latest data from Sure Petcare.""" - - try: - self.states = await self.surepy.get_entities(refresh=True) - _LOGGER.info( - "馃惥 \x1b[38;2;0;255;0m路\x1b[0m successfully updated %d entities", - len(self.states), - ) - except SurePetcareError as error: - _LOGGER.error( - "馃惥 \x1b[38;2;255;26;102m路\x1b[0m unable to fetch data: %s", error - ) - - async_dispatcher_send(self.hass, TOPIC_UPDATE) + self.states: dict[int, Any] = {} async def set_lock_state(self, flap_id: int, state: str) -> None: """Update the lock state of a flap.""" @@ -144,10 +155,6 @@ async def async_setup(self) -> bool: _LOGGER.info(" \x1b[38;2;255;26;102m路\x1b[0m" * 30) _LOGGER.info("") - await self.async_update() - - async_track_time_interval(self.hass, self.async_update, SCAN_INTERVAL) - self.hass.async_add_job( self.hass.config_entries.async_forward_entry_setup( # type: ignore self.config_entry, "binary_sensor" @@ -171,7 +178,8 @@ async def handle_set_lock_state(call: Any) -> None: await self.set_lock_state( call.data[ATTR_FLAP_ID], call.data[ATTR_LOCK_STATE] ) - await self.async_update() + + await self.coordinator.async_request_refresh() lock_state_service_schema = vol.Schema( { diff --git a/binary_sensor.py b/binary_sensor.py index 040e0dc..92eb1c0 100644 --- a/binary_sensor.py +++ b/binary_sensor.py @@ -10,15 +10,16 @@ BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from surepy.entities import PetLocation, SurepyEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from surepy.entities import SurepyEntity +from surepy.entities.devices import Hub as SureHub from surepy.entities.pet import Pet as SurePet from surepy.enums import EntityType, Location # pylint: disable=relative-beyond-top-level from . import SurePetcareAPI -from .const import DOMAIN, SPC, SURE_MANUFACTURER, TOPIC_UPDATE +from .const import DOMAIN, SPC, SURE_MANUFACTURER PARALLEL_UPDATES = 2 @@ -45,13 +46,13 @@ async def async_setup_entry( spc: SurePetcareAPI = hass.data[DOMAIN][SPC] - for surepy_entity in spc.states.values(): + for surepy_entity in spc.coordinator.data.values(): if surepy_entity.type == EntityType.PET: - entities.append(Pet(surepy_entity.id, spc)) + entities.append(Pet(spc.coordinator, surepy_entity.id, spc)) elif surepy_entity.type == EntityType.HUB: - entities.append(Hub(surepy_entity.id, spc)) + entities.append(Hub(spc.coordinator, surepy_entity.id, spc)) # connectivity elif surepy_entity.type in [ @@ -60,28 +61,32 @@ async def async_setup_entry( EntityType.FEEDER, EntityType.FELAQUA, ]: - entities.append(DeviceConnectivity(surepy_entity.id, spc)) + entities.append(DeviceConnectivity(spc.coordinator, surepy_entity.id, spc)) async_add_entities(entities, True) -class SurePetcareBinarySensor(BinarySensorEntity): # type: ignore +class SurePetcareBinarySensor(CoordinatorEntity, BinarySensorEntity): """A binary sensor implementation for Sure Petcare Entities.""" _attr_should_poll = False def __init__( self, + coordinator, _id: int, spc: SurePetcareAPI, device_class: str, ): """Initialize a Sure Petcare binary sensor.""" + super().__init__(coordinator) self._id: int = _id self._spc: SurePetcareAPI = spc - self._surepy_entity: SurepyEntity = self._spc.states[self._id] + self._coordinator = coordinator + + self._surepy_entity: SurepyEntity = self._coordinator.data[self._id] self._state: Any = self._surepy_entity.raw_data().get("status", {}) type_name = self._surepy_entity.type.name.replace("_", " ").title() @@ -120,7 +125,7 @@ def device_info(self): device = { "identifiers": {(DOMAIN, self._id)}, - "name": self._surepy_entity.name.capitalize(), # type: ignore + "name": self._surepy_entity.name.capitalize(), "manufacturer": SURE_MANUFACTURER, "model": model, } @@ -145,46 +150,13 @@ def device_info(self): return device - @callback - def _async_update(self) -> None: - """Get the latest data and update the state.""" - - self._surepy_entity = self._spc.states[self._id] - self._state = self._surepy_entity.raw_data()["status"] - - _LOGGER.debug( - "馃惥 \x1b[38;2;0;255;0m路\x1b[0m %s updated", - self._attr_name.replace( - f"{self._surepy_entity.type.name.replace('_', ' ').title()} ", "" - ), - ) - - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - - self.async_on_remove( - async_dispatcher_connect(self.hass, TOPIC_UPDATE, self._async_update) - ) - - @callback - def update() -> None: - """Update the state.""" - self.async_schedule_update_ha_state(True) - - # pylint: disable=attribute-defined-outside-init - self._async_unsub_dispatcher_connect = async_dispatcher_connect( - self.hass, TOPIC_UPDATE, update - ) - - self._async_update() - class Hub(SurePetcareBinarySensor): """Sure Petcare Pet.""" - def __init__(self, _id: int, spc: SurePetcareAPI) -> None: + def __init__(self, coordinator, _id: int, spc: SurePetcareAPI) -> None: """Initialize a Sure Petcare Hub.""" - super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY) + super().__init__(coordinator, _id, spc, DEVICE_CLASS_CONNECTIVITY) if self._attr_device_info: self._attr_device_info["identifiers"] = {(DOMAIN, str(self._id))} @@ -195,30 +167,30 @@ def __init__(self, _id: int, spc: SurePetcareAPI) -> None: def is_on(self) -> bool: """Return True if the hub is on.""" - if self._state: + hub: SureHub + + if hub := self.coordinator.data[self._id]: + self._attr_extra_state_attributes = { - "led_mode": int(self._surepy_entity.raw_data()["status"]["led_mode"]), - "pairing_mode": bool( - self._surepy_entity.raw_data()["status"]["pairing_mode"] - ), + "led_mode": int(hub.raw_data()["status"]["led_mode"]), + "pairing_mode": bool(hub.raw_data()["status"]["pairing_mode"]), } - return bool(self._state["online"]) + return bool(hub.online) class Pet(SurePetcareBinarySensor): """Sure Petcare Pet.""" - def __init__(self, _id: int, spc: SurePetcareAPI) -> None: + def __init__(self, coordinator, _id: int, spc: SurePetcareAPI) -> None: """Initialize a Sure Petcare Pet.""" - super().__init__(_id, spc, DEVICE_CLASS_PRESENCE) + super().__init__(coordinator, _id, spc, DEVICE_CLASS_PRESENCE) self._surepy_entity: SurePet - self._state: PetLocation self._attr_entity_picture = self._surepy_entity.photo_url - if self._state: + if self._surepy_entity: self._attr_extra_state_attributes = { "since": self._surepy_entity.location.since, "where": self._surepy_entity.location.where, @@ -228,49 +200,34 @@ def __init__(self, _id: int, spc: SurePetcareAPI) -> None: @property def is_on(self) -> bool: """Return True if the pet is at home.""" - return self._attr_is_on - - @callback - def _async_update(self) -> None: - """Get the latest data and update the state.""" - - self._surepy_entity = self._spc.states[self._id] - self._state = self._surepy_entity.location - - try: - self._attr_is_on: bool = bool( - Location(self._surepy_entity.location.where) == Location.INSIDE - ) - except (KeyError, TypeError): - self._attr_is_on: bool = False - - _LOGGER.debug( - "馃惥 \x1b[38;2;0;255;0m路\x1b[0m %s updated", - self._attr_name.replace( - f"{self._surepy_entity.type.name.replace('_', ' ').title()} ", "" - ), - ) + pet: SurePet + if pet := self.coordinator.data[self._id]: + return bool(Location(pet.location.where) == Location.INSIDE) class DeviceConnectivity(SurePetcareBinarySensor): """Sure Petcare Pet.""" - def __init__(self, _id: int, spc: SurePetcareAPI) -> None: + def __init__(self, coordinator, _id: int, spc: SurePetcareAPI) -> None: """Initialize a Sure Petcare device connectivity sensor.""" - super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY) + super().__init__(coordinator, _id, spc, DEVICE_CLASS_CONNECTIVITY) self._attr_name = f"{self._name} Connectivity" self._attr_unique_id = ( f"{self._surepy_entity.household_id}-{self._id}-connectivity" ) - if self._state: - self._attr_extra_state_attributes = { - "device_rssi": f'{self._state["signal"]["device_rssi"]:.2f}', - "hub_rssi": f'{self._state["signal"]["hub_rssi"]:.2f}', + @property + def extra_state_attributes(self) -> dict[str, Any]: + if (data := self._surepy_entity.raw_data()) and (state := data.get("status")): + return { + "device_rssi": f'{state["signal"]["device_rssi"]:.2f}', + "hub_rssi": f'{state["signal"]["hub_rssi"]:.2f}', } - @callback - def _async_update(self) -> None: - super()._async_update() - self._attr_is_on = bool(self._attr_extra_state_attributes) + return {} + + @property + def is_on(self) -> bool: + """Return True if the pet is at home.""" + return bool(self.extra_state_attributes) diff --git a/sensor.py b/sensor.py index 4356b7c..488429c 100644 --- a/sensor.py +++ b/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, cast from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry @@ -13,8 +13,8 @@ PERCENTAGE, VOLUME_MILLILITERS, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import CoordinatorEntity from surepy.entities import SurepyEntity from surepy.entities.devices import ( Feeder as SureFeeder, @@ -27,7 +27,7 @@ # pylint: disable=relative-beyond-top-level from . import SurePetcareAPI -from .const import DOMAIN, SPC, SURE_MANUFACTURER, TOPIC_UPDATE +from .const import DOMAIN, SPC, SURE_MANUFACTURER PARALLEL_UPDATES = 2 @@ -50,27 +50,29 @@ async def async_setup_entry( ) -> None: """Set up config entry Sure PetCare Flaps sensors.""" - entities: list[Flap | Felaqua | Feeder | FeederBowl | SureBattery] = [] + entities: list[Flap | Felaqua | Feeder | FeederBowl | Battery] = [] spc: SurePetcareAPI = hass.data[DOMAIN][SPC] - for surepy_entity in spc.states.values(): + for surepy_entity in spc.coordinator.data.values(): if surepy_entity.type in [ EntityType.CAT_FLAP, EntityType.PET_FLAP, ]: - entities.append(Flap(surepy_entity.id, spc)) + entities.append(Flap(spc.coordinator, surepy_entity.id, spc)) elif surepy_entity.type == EntityType.FELAQUA: - entities.append(Felaqua(surepy_entity.id, spc)) + entities.append(Felaqua(spc.coordinator, surepy_entity.id, spc)) elif surepy_entity.type == EntityType.FEEDER: for bowl in surepy_entity.bowls.values(): - entities.append(FeederBowl(surepy_entity.id, spc, bowl.raw_data())) + entities.append( + FeederBowl(spc.coordinator, surepy_entity.id, spc, bowl.raw_data()) + ) - entities.append(Feeder(surepy_entity.id, spc)) + entities.append(Feeder(spc.coordinator, surepy_entity.id, spc)) if surepy_entity.type in [ EntityType.CAT_FLAP, @@ -78,23 +80,26 @@ async def async_setup_entry( EntityType.FEEDER, EntityType.FELAQUA, ]: - entities.append(SureBattery(surepy_entity.id, spc)) + entities.append(Battery(spc.coordinator, surepy_entity.id, spc)) async_add_entities(entities) -class SurePetcareSensor(SensorEntity): # type: ignore +class SurePetcareSensor(CoordinatorEntity, SensorEntity): """A binary sensor implementation for Sure Petcare Entities.""" _attr_should_poll = False - def __init__(self, _id: int, spc: SurePetcareAPI): + def __init__(self, coordinator, _id: int, spc: SurePetcareAPI): """Initialize a Sure Petcare sensor.""" + super().__init__(coordinator) self._id = _id self._spc: SurePetcareAPI = spc - self._surepy_entity: SurepyEntity = self._spc.states[_id] + self._coordinator = coordinator + + self._surepy_entity: SurepyEntity = self._coordinator.data[_id] self._state: dict[str, Any] = self._surepy_entity.raw_data()["status"] self._attr_available = bool(self._state) @@ -106,7 +111,7 @@ def __init__(self, _id: int, spc: SurePetcareAPI): self._attr_name: str = ( f"{self._surepy_entity.type.name.replace('_', ' ').title()} " - f"{self._surepy_entity.name.capitalize()}" # type: ignore + f"{self._surepy_entity.name.capitalize()}" ) @property @@ -122,7 +127,7 @@ def device_info(self): device = { "identifiers": {(DOMAIN, self._id)}, - "name": self._surepy_entity.name.capitalize(), # type: ignore + "name": self._surepy_entity.name.capitalize(), "manufacturer": SURE_MANUFACTURER, "model": model, } @@ -146,45 +151,12 @@ def device_info(self): return device - @callback - def _async_update(self) -> None: - """Get the latest data and update the state.""" - - self._surepy_entity = self._spc.states[self._id] - self._state = self._surepy_entity.raw_data()["status"] - - _LOGGER.debug( - "馃惥 \x1b[38;2;0;255;0m路\x1b[0m %s updated", - self._attr_name.replace( - f"{self._surepy_entity.type.name.replace('_', ' ').title()} ", "" - ), - ) - - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - - self.async_on_remove( - async_dispatcher_connect(self.hass, TOPIC_UPDATE, self._async_update) - ) - - @callback - def update() -> None: - """Update the state.""" - self.async_schedule_update_ha_state(True) - - # pylint: disable=attribute-defined-outside-init - self._async_unsub_dispatcher_connect = async_dispatcher_connect( - self.hass, TOPIC_UPDATE, update - ) - - self._async_update() - class Flap(SurePetcareSensor): """Sure Petcare Flap.""" - def __init__(self, _id: int, spc: SurePetcareAPI) -> None: - super().__init__(_id, spc) + def __init__(self, coordinator, _id: int, spc: SurePetcareAPI) -> None: + super().__init__(coordinator, _id, spc) self._surepy_entity: SureFlap @@ -203,58 +175,71 @@ def __init__(self, _id: int, spc: SurePetcareAPI) -> None: @property def state(self) -> str: """Return battery level in percent.""" - return LockState(self._state["locking"]["mode"]).name.casefold() + if ( + state := cast(SureFlap, self.coordinator.data[self._id]) + .raw_data() + .get("status") + ): + return LockState(state["locking"]["mode"]).name.casefold() + + return "Unknown" class Felaqua(SurePetcareSensor): """Sure Petcare Felaqua.""" - def __init__(self, _id: int, spc: SurePetcareAPI): - super().__init__(_id, spc) + def __init__(self, coordinator, _id: int, spc: SurePetcareAPI): + super().__init__(coordinator, _id, spc) self._surepy_entity: SureFelaqua - self._attr_entity_picture = self._surepy_entity.icon + self._attr_unit_of_measurement = VOLUME_MILLILITERS - if self._surepy_entity.water_remaining: - self._attr_state = self._surepy_entity.water_remaining.__round__() - else: - self._attr_state = "unknown" + @property + def state(self) -> int | None: + """Return the remaining water.""" + if felaqua := cast(SureFelaqua, self.coordinator.data[self._id]): + return max(0, int(felaqua.water_remaining or 0)) - self._attr_unit_of_measurement = VOLUME_MILLILITERS + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the remaining water.""" - if self._state: - self._attr_extra_state_attributes = {} + attrs = {} - for weight in self._state.get("drink", {}).get("weights", {}): + if ( + state := cast(SureFelaqua, self.coordinator.data[self._id]) + .raw_data() + .get("status") + ): + for weight in state.get("drink", {}).get("weights", {}): attr_key = f"weight_{weight['index']}" - self._attr_extra_state_attributes[attr_key] = weight + attrs[attr_key] = weight - @property - def state(self) -> int: - """Return the remaining water.""" - return int(self._surepy_entity.water_remaining or 0) + return attrs @property def device_info(self): device = {} - try: - model = f"{self._surepy_entity.type.name.replace('_', ' ').title()}" + if felaqua := cast(SureFelaqua, self.coordinator.data[self._id]): - if serial := self._surepy_entity.raw_data().get("serial_number", None): - model = f"{model} ({serial})" + try: + model = f"{felaqua.type.name.replace('_', ' ').title()}" - device = { - "identifiers": {(DOMAIN, self._id)}, - "name": self._surepy_entity.name.capitalize(), # type: ignore - "manufacturer": SURE_MANUFACTURER, - "model": model, - } + if serial := felaqua.raw_data().get("serial_number", None): + model = f"{model} ({serial})" - except AttributeError: - pass + device = { + "identifiers": {(DOMAIN, self._id)}, + "name": felaqua.name.capitalize(), + "manufacturer": SURE_MANUFACTURER, + "model": model, + } + + except AttributeError: + pass return device @@ -262,9 +247,15 @@ def device_info(self): class FeederBowl(SurePetcareSensor): """Sure Petcare Feeder Bowl.""" - def __init__(self, _id: int, spc: SurePetcareAPI, bowl_data: dict[str, int | str]): + def __init__( + self, + coordinator, + _id: int, + spc: SurePetcareAPI, + bowl_data: dict[str, int | str], + ): """Initialize a Bowl sensor.""" - super().__init__(_id, spc) + super().__init__(coordinator, _id, spc) self.feeder_id = _id self.bowl_id = int(bowl_data["index"]) @@ -272,8 +263,10 @@ def __init__(self, _id: int, spc: SurePetcareAPI, bowl_data: dict[str, int | str self._id = int(f"{_id}{str(self.bowl_id)}") self._spc: SurePetcareAPI = spc - self._surepy_feeder_entity: SurepyEntity = self._spc.states[_id] - self._surepy_entity: SureFeederBowl = self._spc.states[_id].bowls[self.bowl_id] + self._surepy_feeder_entity: SurepyEntity = self._coordinator.data[_id] + self._surepy_entity: SureFeederBowl = self._coordinator.data[_id].bowls[ + self.bowl_id + ] self._state: dict[str, Any] = bowl_data # https://github.com/PyCQA/pylint/issues/2062 @@ -293,39 +286,25 @@ def __init__(self, _id: int, spc: SurePetcareAPI, bowl_data: dict[str, int | str @property def state(self) -> int | None: """Return the remaining water.""" - return int(self._surepy_entity.weight) - - @callback - def _async_update(self) -> None: - """Get the latest data and update the state.""" - - self._surepy_feeder_entity = self._spc.states[self.feeder_id] - self._surepy_entity = self._spc.states[self.feeder_id].bowls[self.bowl_id] - self._state = self._surepy_entity.raw_data() - - _LOGGER.debug( - "馃惥 \x1b[38;2;0;255;0m路\x1b[0m %s updated", - self._surepy_entity.name.capitalize(), - ) + if feeder := cast(SureFeeder, self.coordinator.data[self.feeder_id]): + return max(0, int(feeder.bowls[self.bowl_id].weight)) class Feeder(SurePetcareSensor): """Sure Petcare Feeder.""" - def __init__(self, _id: int, spc: SurePetcareAPI): - super().__init__(_id, spc) + def __init__(self, coordinator, _id: int, spc: SurePetcareAPI): + super().__init__(coordinator, _id, spc) self._surepy_entity: SureFeeder - self._attr_entity_picture = self._surepy_entity.icon - self._attr_state = int(self._surepy_entity.total_weight) self._attr_unit_of_measurement = MASS_GRAMS @property def state(self) -> int | None: """Return the total remaining food.""" - self._surepy_entity: SureFeeder - return int(self._surepy_entity.total_weight) + if feeder := cast(SureFeeder, self.coordinator.data[self._id]): + return int(feeder.total_weight) @property def device_info(self): @@ -340,7 +319,7 @@ def device_info(self): device = { "identifiers": {(DOMAIN, self._id)}, - "name": self._surepy_entity.name.capitalize(), # type: ignore + "name": self._surepy_entity.name.capitalize(), "manufacturer": SURE_MANUFACTURER, "model": model, } @@ -350,53 +329,47 @@ def device_info(self): return device - @callback - def _async_update(self) -> None: - """Get the latest data and update the state.""" - self._surepy_entity: SureFeeder = self._spc.states[self._id] - self._state = self._surepy_entity.raw_data()["status"] - - if lunch_data := self._surepy_entity.raw_data().get("lunch"): - for bowl_data in lunch_data["weights"]: - - # this should be fixed in the library - # pylint: disable=protected-access - self._surepy_entity.bowls[bowl_data["index"]]._data = bowl_data - - _LOGGER.debug( - "馃惥 \x1b[38;2;0;255;0m路\x1b[0m %s updated", - self._surepy_entity.name.capitalize(), - ) - - -class SureBattery(SurePetcareSensor): +class Battery(SurePetcareSensor): """Sure Petcare Flap.""" - def __init__(self, _id: int, spc: SurePetcareAPI): - super().__init__(_id, spc) + def __init__(self, coordinator, _id: int, spc: SurePetcareAPI): + super().__init__(coordinator, _id, spc) self._surepy_entity: SurepyDevice - self._attr_device_class = DEVICE_CLASS_BATTERY self._attr_name = f"{self._attr_name} Battery Level" - self._attr_unit_of_measurement = PERCENTAGE + + self._attr_device_class = DEVICE_CLASS_BATTERY self._attr_unique_id = ( f"{self._surepy_entity.household_id}-{self._surepy_entity.id}-battery" ) - if self._state: - self._attr_extra_state_attributes = {} + @property + def state(self) -> int | None: + """Return battery level in percent.""" + if ( + battery := cast(SurepyDevice, self.coordinator.data[self._id]) + ) and battery.battery_level: + return max(0, battery.battery_level) - voltage_per_battery = float(self._state["battery"]) / 4 - self._attr_extra_state_attributes = { - ATTR_VOLTAGE: f"{float(self._state['battery']):.2f}", + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the additional attrs.""" + + attrs = {} + + if ( + state := cast(SurepyDevice, self.coordinator.data[self._id]) + .raw_data() + .get("status") + ): + voltage_per_battery = float(state["battery"]) / 4 + attrs = { + ATTR_VOLTAGE: f"{float(state['battery']):.2f}", f"{ATTR_VOLTAGE}_per_battery": f"{voltage_per_battery:.2f}", - "alt-battery": (1 - pow(6 - float(self._state["battery"]), 2)) * 100, + "alt-battery": (1 - pow(6 - float(state["battery"]), 2)) * 100, } - @property - def state(self) -> int | None: - """Return battery level in percent.""" - return self._surepy_entity.battery_level + return attrs