Surepetcare, use DataUpdateCoordinator (#55982)

* Surepetcare, use dataupdater

Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>

* Review comment

Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>

* Apply suggestions from code review

Co-authored-by: J. Nick Koston <nick@koston.org>

* style

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Daniel Hjelseth Høyer 2021-09-10 08:37:00 +02:00 committed by GitHub
parent 89281a273c
commit c27ad3078a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 93 additions and 129 deletions

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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()