diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index 00c45701423..a6e9dca703c 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -3,9 +3,8 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any -from surepy import Surepy +from surepy import Surepy, SurepyEntity from surepy.enums import LockState from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError import voluptuous as vol @@ -14,9 +13,8 @@ from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.core import HomeAssistant 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.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( ATTR_FLAP_ID, @@ -26,9 +24,7 @@ from .const import ( CONF_PETS, DOMAIN, SERVICE_SET_LOCK_STATE, - SPC, SURE_API_TIMEOUT, - TOPIC_UPDATE, ) _LOGGER = logging.getLogger(__name__) @@ -83,12 +79,23 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.error("Unable to connect to surepetcare.io: Wrong %s!", error) return False - spc = SurePetcareAPI(hass, surepy) - hass.data[DOMAIN][SPC] = spc + async def _update_method() -> dict[int, SurepyEntity]: + """Get the latest data from Sure Petcare.""" + try: + return await surepy.get_entities(refresh=True) + except SurePetcareError as err: + raise UpdateFailed(f"Unable to fetch data: {err}") from err - await spc.async_update() + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=DOMAIN, + update_method=_update_method, + update_interval=SCAN_INTERVAL, + ) - async_track_time_interval(hass, spc.async_update, SCAN_INTERVAL) + hass.data[DOMAIN] = coordinator + await coordinator.async_config_entry_first_refresh() # load platforms for platform in PLATFORMS: @@ -96,27 +103,29 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.helpers.discovery.async_load_platform(platform, DOMAIN, {}, config) ) + lock_states = { + LockState.UNLOCKED.name.lower(): surepy.sac.unlock, + LockState.LOCKED_IN.name.lower(): surepy.sac.lock_in, + LockState.LOCKED_OUT.name.lower(): surepy.sac.lock_out, + LockState.LOCKED_ALL.name.lower(): surepy.sac.lock, + } + async def handle_set_lock_state(call): """Call when setting the lock state.""" - await spc.set_lock_state(call.data[ATTR_FLAP_ID], call.data[ATTR_LOCK_STATE]) - await spc.async_update() + flap_id = call.data[ATTR_FLAP_ID] + state = call.data[ATTR_LOCK_STATE] + await lock_states[state](flap_id) + await coordinator.async_request_refresh() lock_state_service_schema = vol.Schema( { vol.Required(ATTR_FLAP_ID): vol.All( - cv.positive_int, vol.In(spc.states.keys()) + cv.positive_int, vol.In(coordinator.data.keys()) ), vol.Required(ATTR_LOCK_STATE): vol.All( cv.string, vol.Lower, - vol.In( - [ - LockState.UNLOCKED.name.lower(), - LockState.LOCKED_IN.name.lower(), - LockState.LOCKED_OUT.name.lower(), - LockState.LOCKED_ALL.name.lower(), - ] - ), + vol.In(lock_states.keys()), ), } ) @@ -129,36 +138,3 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) return True - - -class SurePetcareAPI: - """Define a generic Sure Petcare object.""" - - def __init__(self, hass: HomeAssistant, surepy: Surepy) -> None: - """Initialize the Sure Petcare object.""" - self.hass = hass - 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) - except SurePetcareError as error: - _LOGGER.error("Unable to fetch data: %s", error) - return - - async_dispatcher_send(self.hass, TOPIC_UPDATE) - - async def set_lock_state(self, flap_id: int, state: str) -> None: - """Update the lock state of a flap.""" - - if state == LockState.UNLOCKED.name.lower(): - await self.surepy.sac.unlock(flap_id) - elif state == LockState.LOCKED_IN.name.lower(): - await self.surepy.sac.lock_in(flap_id) - elif state == LockState.LOCKED_OUT.name.lower(): - await self.surepy.sac.lock_out(flap_id) - elif state == LockState.LOCKED_ALL.name.lower(): - await self.surepy.sac.lock(flap_id) diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py index 0f536d6135d..50a890112bc 100644 --- a/homeassistant/components/surepetcare/binary_sensor.py +++ b/homeassistant/components/surepetcare/binary_sensor.py @@ -13,10 +13,12 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) -from . import SurePetcareAPI -from .const import DOMAIN, SPC, TOPIC_UPDATE +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -30,9 +32,9 @@ async def async_setup_platform( entities: list[SurepyEntity | Pet | Hub | DeviceConnectivity] = [] - spc: SurePetcareAPI = hass.data[DOMAIN][SPC] + coordinator: DataUpdateCoordinator = hass.data[DOMAIN] - for surepy_entity in spc.states.values(): + for surepy_entity in coordinator.data.values(): # connectivity if surepy_entity.type in [ @@ -41,32 +43,30 @@ async def async_setup_platform( EntityType.FEEDER, EntityType.FELAQUA, ]: - entities.append(DeviceConnectivity(surepy_entity.id, spc)) + entities.append(DeviceConnectivity(surepy_entity.id, coordinator)) elif surepy_entity.type == EntityType.PET: - entities.append(Pet(surepy_entity.id, spc)) + entities.append(Pet(surepy_entity.id, coordinator)) elif surepy_entity.type == EntityType.HUB: - entities.append(Hub(surepy_entity.id, spc)) + entities.append(Hub(surepy_entity.id, coordinator)) - async_add_entities(entities, True) + async_add_entities(entities) -class SurePetcareBinarySensor(BinarySensorEntity): +class SurePetcareBinarySensor(BinarySensorEntity, CoordinatorEntity): """A binary sensor implementation for Sure Petcare Entities.""" - _attr_should_poll = False - def __init__( self, _id: int, - spc: SurePetcareAPI, + coordinator: DataUpdateCoordinator, device_class: str, ) -> None: """Initialize a Sure Petcare binary sensor.""" + super().__init__(coordinator) self._id = _id - self._spc: SurePetcareAPI = spc - surepy_entity: SurepyEntity = self._spc.states[self._id] + surepy_entity: SurepyEntity = coordinator.data[self._id] # cover special case where a device has no name set if surepy_entity.name: @@ -77,31 +77,36 @@ class SurePetcareBinarySensor(BinarySensorEntity): self._attr_device_class = device_class self._attr_name = f"{surepy_entity.type.name.capitalize()} {name.capitalize()}" self._attr_unique_id = f"{surepy_entity.household_id}-{self._id}" + self._update_attr() @abstractmethod @callback - def _async_update(self) -> None: - """Get the latest data and update the state.""" + def _update_attr(self) -> None: + """Update the state and attributes.""" - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - self.async_on_remove( - async_dispatcher_connect(self.hass, TOPIC_UPDATE, self._async_update) - ) - self._async_update() + @callback + def _handle_coordinator_update(self) -> None: + """Get the latest data and update the state.""" + self._update_attr() + self.async_write_ha_state() class Hub(SurePetcareBinarySensor): """Sure Petcare Hub.""" - def __init__(self, _id: int, spc: SurePetcareAPI) -> None: + def __init__(self, _id: int, coordinator: DataUpdateCoordinator) -> None: """Initialize a Sure Petcare Hub.""" - super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY) + super().__init__(_id, coordinator, DEVICE_CLASS_CONNECTIVITY) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return super().available and bool(self._attr_is_on) @callback - def _async_update(self) -> None: + def _update_attr(self) -> None: """Get the latest data and update the state.""" - surepy_entity = self._spc.states[self._id] + surepy_entity = self.coordinator.data[self._id] state = surepy_entity.raw_data()["status"] self._attr_is_on = self._attr_available = bool(state["online"]) if surepy_entity.raw_data(): @@ -114,20 +119,19 @@ class Hub(SurePetcareBinarySensor): else: self._attr_extra_state_attributes = {} _LOGGER.debug("%s -> state: %s", self.name, state) - self.async_write_ha_state() class Pet(SurePetcareBinarySensor): """Sure Petcare Pet.""" - def __init__(self, _id: int, spc: SurePetcareAPI) -> None: + def __init__(self, _id: int, coordinator: DataUpdateCoordinator) -> None: """Initialize a Sure Petcare Pet.""" - super().__init__(_id, spc, DEVICE_CLASS_PRESENCE) + super().__init__(_id, coordinator, DEVICE_CLASS_PRESENCE) @callback - def _async_update(self) -> None: + def _update_attr(self) -> None: """Get the latest data and update the state.""" - surepy_entity = self._spc.states[self._id] + surepy_entity = self.coordinator.data[self._id] state = surepy_entity.location try: self._attr_is_on = bool(Location(state.where) == Location.INSIDE) @@ -141,7 +145,6 @@ class Pet(SurePetcareBinarySensor): else: self._attr_extra_state_attributes = {} _LOGGER.debug("%s -> state: %s", self.name, state) - self.async_write_ha_state() class DeviceConnectivity(SurePetcareBinarySensor): @@ -150,21 +153,20 @@ class DeviceConnectivity(SurePetcareBinarySensor): def __init__( self, _id: int, - spc: SurePetcareAPI, + coordinator: DataUpdateCoordinator, ) -> None: """Initialize a Sure Petcare Device.""" - super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY) + super().__init__(_id, coordinator, DEVICE_CLASS_CONNECTIVITY) self._attr_name = f"{self.name}_connectivity" self._attr_unique_id = ( - f"{self._spc.states[self._id].household_id}-{self._id}-connectivity" + f"{self.coordinator.data[self._id].household_id}-{self._id}-connectivity" ) @callback - def _async_update(self) -> None: - """Get the latest data and update the state.""" - surepy_entity = self._spc.states[self._id] + def _update_attr(self): + surepy_entity = self.coordinator.data[self._id] state = surepy_entity.raw_data()["status"] - self._attr_is_on = self._attr_available = bool(state) + self._attr_is_on = bool(state) if state: self._attr_extra_state_attributes = { "device_rssi": f'{state["signal"]["device_rssi"]:.2f}', @@ -173,4 +175,3 @@ class DeviceConnectivity(SurePetcareBinarySensor): else: self._attr_extra_state_attributes = {} _LOGGER.debug("%s -> state: %s", self.name, state) - self.async_write_ha_state() diff --git a/homeassistant/components/surepetcare/const.py b/homeassistant/components/surepetcare/const.py index cb5a78a3c1e..6349ebe14a8 100644 --- a/homeassistant/components/surepetcare/const.py +++ b/homeassistant/components/surepetcare/const.py @@ -1,15 +1,10 @@ """Constants for the Sure Petcare component.""" DOMAIN = "surepetcare" -SPC = "spc" - CONF_FEEDERS = "feeders" CONF_FLAPS = "flaps" CONF_PETS = "pets" -# platforms -TOPIC_UPDATE = f"{DOMAIN}_data_update" - # sure petcare api SURE_API_TIMEOUT = 60 diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py index 35d35e9be1f..6b07408f6b1 100644 --- a/homeassistant/components/surepetcare/sensor.py +++ b/homeassistant/components/surepetcare/sensor.py @@ -9,17 +9,13 @@ from surepy.enums import EntityType from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_VOLTAGE, DEVICE_CLASS_BATTERY, PERCENTAGE from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -from . import SurePetcareAPI -from .const import ( - DOMAIN, - SPC, - SURE_BATT_VOLTAGE_DIFF, - SURE_BATT_VOLTAGE_LOW, - TOPIC_UPDATE, +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, ) +from .const import DOMAIN, SURE_BATT_VOLTAGE_DIFF, SURE_BATT_VOLTAGE_LOW + _LOGGER = logging.getLogger(__name__) @@ -30,9 +26,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= entities: list[SurepyEntity] = [] - spc: SurePetcareAPI = hass.data[DOMAIN][SPC] + coordinator: DataUpdateCoordinator = hass.data[DOMAIN] - for surepy_entity in spc.states.values(): + for surepy_entity in coordinator.data.values(): if surepy_entity.type in [ EntityType.CAT_FLAP, @@ -40,23 +36,21 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= EntityType.FEEDER, EntityType.FELAQUA, ]: - entities.append(SureBattery(surepy_entity.id, spc)) + entities.append(SureBattery(surepy_entity.id, coordinator)) async_add_entities(entities) -class SureBattery(SensorEntity): +class SureBattery(SensorEntity, CoordinatorEntity): """A sensor implementation for Sure Petcare Entities.""" - _attr_should_poll = False - - def __init__(self, _id: int, spc: SurePetcareAPI) -> None: + def __init__(self, _id: int, coordinator: DataUpdateCoordinator) -> None: """Initialize a Sure Petcare sensor.""" + super().__init__(coordinator) self._id = _id - self._spc: SurePetcareAPI = spc - surepy_entity: SurepyEntity = self._spc.states[_id] + surepy_entity: SurepyEntity = coordinator.data[_id] self._attr_device_class = DEVICE_CLASS_BATTERY if surepy_entity.name: @@ -67,14 +61,20 @@ class SureBattery(SensorEntity): self._attr_unique_id = ( f"{surepy_entity.household_id}-{surepy_entity.id}-battery" ) + self._update_attr() @callback - def _async_update(self) -> None: + def _handle_coordinator_update(self) -> None: """Get the latest data and update the state.""" - surepy_entity = self._spc.states[self._id] + self._update_attr() + self.async_write_ha_state() + + @callback + def _update_attr(self) -> None: + """Update the state and attributes.""" + surepy_entity = self.coordinator.data[self._id] state = surepy_entity.raw_data()["status"] - self._attr_available = bool(state) try: per_battery_voltage = state["battery"] / 4 voltage_diff = per_battery_voltage - SURE_BATT_VOLTAGE_LOW @@ -92,12 +92,4 @@ class SureBattery(SensorEntity): } else: self._attr_extra_state_attributes = {} - self.async_write_ha_state() _LOGGER.debug("%s -> state: %s", self.name, state) - - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - self.async_on_remove( - async_dispatcher_connect(self.hass, TOPIC_UPDATE, self._async_update) - ) - self._async_update()