Fix and upgrade surepetcare (#49223)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Ben 2021-04-27 20:58:52 +02:00 committed by GitHub
parent 3f3f77c6e6
commit ebbcfb1bc7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 192 additions and 330 deletions

View File

@ -1,26 +1,17 @@
"""Support for Sure Petcare cat/pet flaps.""" """The surepetcare integration."""
from __future__ import annotations from __future__ import annotations
from datetime import timedelta
import logging import logging
from typing import Any from typing import Any
from surepy import ( from surepy import Surepy
MESTART_RESOURCE, from surepy.enums import LockState
SureLockStateID, from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError
SurePetcare,
SurePetcareAuthenticationError,
SurePetcareError,
SurepyProduct,
)
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME
CONF_ID, from homeassistant.core import HomeAssistant
CONF_PASSWORD,
CONF_SCAN_INTERVAL,
CONF_TYPE,
CONF_USERNAME,
)
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
@ -31,11 +22,7 @@ from .const import (
ATTR_LOCK_STATE, ATTR_LOCK_STATE,
CONF_FEEDERS, CONF_FEEDERS,
CONF_FLAPS, CONF_FLAPS,
CONF_PARENT,
CONF_PETS, CONF_PETS,
CONF_PRODUCT_ID,
DATA_SURE_PETCARE,
DEFAULT_SCAN_INTERVAL,
DOMAIN, DOMAIN,
SERVICE_SET_LOCK_STATE, SERVICE_SET_LOCK_STATE,
SPC, SPC,
@ -45,50 +32,49 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORMS = ["binary_sensor", "sensor"]
SCAN_INTERVAL = timedelta(minutes=3)
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: vol.Schema( DOMAIN: vol.Schema(
{ vol.All(
vol.Required(CONF_USERNAME): cv.string, {
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_FEEDERS, default=[]): vol.All( vol.Required(CONF_PASSWORD): cv.string,
cv.ensure_list, [cv.positive_int] vol.Optional(CONF_FEEDERS): vol.All(
), cv.ensure_list, [cv.positive_int]
vol.Optional(CONF_FLAPS, default=[]): vol.All( ),
cv.ensure_list, [cv.positive_int] vol.Optional(CONF_FLAPS): vol.All(
), cv.ensure_list, [cv.positive_int]
vol.Optional(CONF_PETS): vol.All(cv.ensure_list, [cv.positive_int]), ),
vol.Optional( vol.Optional(CONF_PETS): vol.All(cv.ensure_list, [cv.positive_int]),
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL vol.Optional(CONF_SCAN_INTERVAL): cv.time_period,
): cv.time_period, },
} cv.deprecated(CONF_FEEDERS),
cv.deprecated(CONF_FLAPS),
cv.deprecated(CONF_PETS),
cv.deprecated(CONF_SCAN_INTERVAL),
)
) )
}, },
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
async def async_setup(hass, config) -> bool: async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""Initialize the Sure Petcare component.""" """Set up the Sure Petcare integration."""
conf = config[DOMAIN] conf = config[DOMAIN]
hass.data.setdefault(DOMAIN, {})
# update interval
scan_interval = conf[CONF_SCAN_INTERVAL]
# shared data
hass.data[DOMAIN] = hass.data[DATA_SURE_PETCARE] = {}
# sure petcare api connection
try: try:
surepy = SurePetcare( surepy = Surepy(
conf[CONF_USERNAME], conf[CONF_USERNAME],
conf[CONF_PASSWORD], conf[CONF_PASSWORD],
hass.loop, auth_token=None,
async_get_clientsession(hass),
api_timeout=SURE_API_TIMEOUT, api_timeout=SURE_API_TIMEOUT,
session=async_get_clientsession(hass),
) )
except SurePetcareAuthenticationError: except SurePetcareAuthenticationError:
_LOGGER.error("Unable to connect to surepetcare.io: Wrong credentials!") _LOGGER.error("Unable to connect to surepetcare.io: Wrong credentials!")
return False return False
@ -96,50 +82,12 @@ async def async_setup(hass, config) -> bool:
_LOGGER.error("Unable to connect to surepetcare.io: Wrong %s!", error) _LOGGER.error("Unable to connect to surepetcare.io: Wrong %s!", error)
return False return False
# add feeders spc = SurePetcareAPI(hass, surepy)
things = [ hass.data[DOMAIN][SPC] = spc
{CONF_ID: feeder, CONF_TYPE: SurepyProduct.FEEDER}
for feeder in conf[CONF_FEEDERS]
]
# add flaps (don't differentiate between CAT and PET for now)
things.extend(
[
{CONF_ID: flap, CONF_TYPE: SurepyProduct.PET_FLAP}
for flap in conf[CONF_FLAPS]
]
)
# discover hubs the flaps/feeders are connected to
hub_ids = set()
for device in things.copy():
device_data = await surepy.device(device[CONF_ID])
if (
CONF_PARENT in device_data
and device_data[CONF_PARENT][CONF_PRODUCT_ID] == SurepyProduct.HUB
and device_data[CONF_PARENT][CONF_ID] not in hub_ids
):
things.append(
{
CONF_ID: device_data[CONF_PARENT][CONF_ID],
CONF_TYPE: SurepyProduct.HUB,
}
)
hub_ids.add(device_data[CONF_PARENT][CONF_ID])
# add pets
things.extend(
[{CONF_ID: pet, CONF_TYPE: SurepyProduct.PET} for pet in conf[CONF_PETS]]
)
_LOGGER.debug("Devices and Pets to setup: %s", things)
spc = hass.data[DATA_SURE_PETCARE][SPC] = SurePetcareAPI(hass, surepy, things)
# initial update
await spc.async_update() await spc.async_update()
async_track_time_interval(hass, spc.async_update, scan_interval) async_track_time_interval(hass, spc.async_update, SCAN_INTERVAL)
# load platforms # load platforms
hass.async_create_task( hass.async_create_task(
@ -164,10 +112,12 @@ async def async_setup(hass, config) -> bool:
vol.Lower, vol.Lower,
vol.In( vol.In(
[ [
SureLockStateID.UNLOCKED.name.lower(), # https://github.com/PyCQA/pylint/issues/2062
SureLockStateID.LOCKED_IN.name.lower(), # pylint: disable=no-member
SureLockStateID.LOCKED_OUT.name.lower(), LockState.UNLOCKED.name.lower(),
SureLockStateID.LOCKED_ALL.name.lower(), LockState.LOCKED_IN.name.lower(),
LockState.LOCKED_OUT.name.lower(),
LockState.LOCKED_ALL.name.lower(),
] ]
), ),
), ),
@ -187,50 +137,32 @@ async def async_setup(hass, config) -> bool:
class SurePetcareAPI: class SurePetcareAPI:
"""Define a generic Sure Petcare object.""" """Define a generic Sure Petcare object."""
def __init__(self, hass, surepy: SurePetcare, ids: list[dict[str, Any]]) -> None: def __init__(self, hass: HomeAssistant, surepy: Surepy) -> None:
"""Initialize the Sure Petcare object.""" """Initialize the Sure Petcare object."""
self.hass = hass self.hass = hass
self.surepy = surepy self.surepy = surepy
self.ids = ids self.states = {}
self.states: dict[str, Any] = {}
async def async_update(self, arg: Any = None) -> None: async def async_update(self, _: Any = None) -> None:
"""Refresh Sure Petcare data.""" """Get the latest data from Sure Petcare."""
# Fetch all data from SurePet API, refreshing the surepy cache try:
# TODO: get surepy upstream to add a method to clear the cache explicitly pylint: disable=fixme self.states = await self.surepy.get_entities()
await self.surepy._get_resource( # pylint: disable=protected-access except SurePetcareError as error:
resource=MESTART_RESOURCE _LOGGER.error("Unable to fetch data: %s", error)
)
for thing in self.ids:
sure_id = thing[CONF_ID]
sure_type = thing[CONF_TYPE]
try:
type_state = self.states.setdefault(sure_type, {})
if sure_type in [
SurepyProduct.CAT_FLAP,
SurepyProduct.PET_FLAP,
SurepyProduct.FEEDER,
SurepyProduct.HUB,
]:
type_state[sure_id] = await self.surepy.device(sure_id)
elif sure_type == SurepyProduct.PET:
type_state[sure_id] = await self.surepy.pet(sure_id)
except SurePetcareError as error:
_LOGGER.error("Unable to retrieve data from surepetcare.io: %s", error)
async_dispatcher_send(self.hass, TOPIC_UPDATE) async_dispatcher_send(self.hass, TOPIC_UPDATE)
async def set_lock_state(self, flap_id: int, state: str) -> None: async def set_lock_state(self, flap_id: int, state: str) -> None:
"""Update the lock state of a flap.""" """Update the lock state of a flap."""
if state == SureLockStateID.UNLOCKED.name.lower():
# https://github.com/PyCQA/pylint/issues/2062
# pylint: disable=no-member
if state == LockState.UNLOCKED.name.lower():
await self.surepy.unlock(flap_id) await self.surepy.unlock(flap_id)
elif state == SureLockStateID.LOCKED_IN.name.lower(): elif state == LockState.LOCKED_IN.name.lower():
await self.surepy.lock_in(flap_id) await self.surepy.lock_in(flap_id)
elif state == SureLockStateID.LOCKED_OUT.name.lower(): elif state == LockState.LOCKED_OUT.name.lower():
await self.surepy.lock_out(flap_id) await self.surepy.lock_out(flap_id)
elif state == SureLockStateID.LOCKED_ALL.name.lower(): elif state == LockState.LOCKED_ALL.name.lower():
await self.surepy.lock(flap_id) await self.surepy.lock(flap_id)

View File

@ -1,23 +1,22 @@
"""Support for Sure PetCare Flaps/Pets binary sensors.""" """Support for Sure PetCare Flaps/Pets binary sensors."""
from __future__ import annotations from __future__ import annotations
from datetime import datetime
import logging import logging
from typing import Any from typing import Any
from surepy import SureLocationID, SurepyProduct from surepy.entities import SurepyEntity
from surepy.enums import EntityType, Location, SureEnum
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_CONNECTIVITY,
DEVICE_CLASS_PRESENCE, DEVICE_CLASS_PRESENCE,
BinarySensorEntity, BinarySensorEntity,
) )
from homeassistant.const import CONF_ID, CONF_TYPE
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import SurePetcareAPI from . import SurePetcareAPI
from .const import DATA_SURE_PETCARE, SPC, TOPIC_UPDATE from .const import DOMAIN, SPC, TOPIC_UPDATE
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -29,30 +28,27 @@ async def async_setup_platform(
if discovery_info is None: if discovery_info is None:
return return
entities = [] entities: list[SurepyEntity] = []
spc = hass.data[DATA_SURE_PETCARE][SPC] spc: SurePetcareAPI = hass.data[DOMAIN][SPC]
for thing in spc.ids: for surepy_entity in spc.states.values():
sure_id = thing[CONF_ID]
sure_type = thing[CONF_TYPE]
# connectivity # connectivity
if sure_type in [ if surepy_entity.type in [
SurepyProduct.CAT_FLAP, EntityType.CAT_FLAP,
SurepyProduct.PET_FLAP, EntityType.PET_FLAP,
SurepyProduct.FEEDER, EntityType.FEEDER,
EntityType.FELAQUA,
]: ]:
entities.append(DeviceConnectivity(sure_id, sure_type, spc)) entities.append(
DeviceConnectivity(surepy_entity.id, surepy_entity.type, spc)
)
if sure_type == SurepyProduct.PET: if surepy_entity.type == EntityType.PET:
entity = Pet(sure_id, spc) entities.append(Pet(surepy_entity.id, spc))
elif sure_type == SurepyProduct.HUB: elif surepy_entity.type == EntityType.HUB:
entity = Hub(sure_id, spc) entities.append(Hub(surepy_entity.id, spc))
else:
continue
entities.append(entity)
async_add_entities(entities, True) async_add_entities(entities, True)
@ -65,35 +61,29 @@ class SurePetcareBinarySensor(BinarySensorEntity):
_id: int, _id: int,
spc: SurePetcareAPI, spc: SurePetcareAPI,
device_class: str, device_class: str,
sure_type: SurepyProduct, sure_type: EntityType,
): ):
"""Initialize a Sure Petcare binary sensor.""" """Initialize a Sure Petcare binary sensor."""
self._id = _id self._id = _id
self._sure_type = sure_type
self._device_class = device_class self._device_class = device_class
self._spc: SurePetcareAPI = spc self._spc: SurePetcareAPI = spc
self._spc_data: dict[str, Any] = self._spc.states[self._sure_type].get(self._id)
self._state: dict[str, Any] = {} self._surepy_entity: SurepyEntity = self._spc.states[self._id]
self._state: SureEnum | dict[str, Any] = None
# cover special case where a device has no name set # cover special case where a device has no name set
if "name" in self._spc_data: if self._surepy_entity.name:
name = self._spc_data["name"] name = self._surepy_entity.name
else: else:
name = f"Unnamed {self._sure_type.name.capitalize()}" name = f"Unnamed {self._surepy_entity.type.name.capitalize()}"
self._name = f"{self._sure_type.name.capitalize()} {name.capitalize()}" self._name = f"{self._surepy_entity.type.name.capitalize()} {name.capitalize()}"
self._async_unsub_dispatcher_connect = None
@property
def is_on(self) -> bool | None:
"""Return true if entity is on/unlocked."""
return bool(self._state)
@property @property
def should_poll(self) -> bool: def should_poll(self) -> bool:
"""Return true.""" """Return if the entity should use default polling."""
return False return False
@property @property
@ -109,30 +99,21 @@ class SurePetcareBinarySensor(BinarySensorEntity):
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Return an unique ID.""" """Return an unique ID."""
return f"{self._spc_data['household_id']}-{self._id}" return f"{self._surepy_entity.household_id}-{self._id}"
async def async_update(self) -> None: @callback
def _async_update(self) -> None:
"""Get the latest data and update the state.""" """Get the latest data and update the state."""
self._spc_data = self._spc.states[self._sure_type].get(self._id) self._surepy_entity = self._spc.states[self._id]
self._state = self._spc_data.get("status") self._state = self._surepy_entity.raw_data()["status"]
_LOGGER.debug("%s -> self._state: %s", self._name, self._state) _LOGGER.debug("%s -> self._state: %s", self._name, self._state)
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Register callbacks.""" """Register callbacks."""
self.async_on_remove(
@callback async_dispatcher_connect(self.hass, TOPIC_UPDATE, self._async_update)
def update() -> None:
"""Update the state."""
self.async_schedule_update_ha_state(True)
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
self.hass, TOPIC_UPDATE, update
) )
self._async_update()
async def async_will_remove_from_hass(self) -> None:
"""Disconnect dispatcher listener when removed."""
if self._async_unsub_dispatcher_connect:
self._async_unsub_dispatcher_connect()
class Hub(SurePetcareBinarySensor): class Hub(SurePetcareBinarySensor):
@ -140,7 +121,7 @@ class Hub(SurePetcareBinarySensor):
def __init__(self, _id: int, spc: SurePetcareAPI) -> None: def __init__(self, _id: int, spc: SurePetcareAPI) -> None:
"""Initialize a Sure Petcare Hub.""" """Initialize a Sure Petcare Hub."""
super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY, SurepyProduct.HUB) super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY, EntityType.HUB)
@property @property
def available(self) -> bool: def available(self) -> bool:
@ -156,10 +137,12 @@ class Hub(SurePetcareBinarySensor):
def extra_state_attributes(self) -> dict[str, Any] | None: def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the device.""" """Return the state attributes of the device."""
attributes = None attributes = None
if self._state: if self._surepy_entity.raw_data():
attributes = { attributes = {
"led_mode": int(self._state["led_mode"]), "led_mode": int(self._surepy_entity.raw_data()["status"]["led_mode"]),
"pairing_mode": bool(self._state["pairing_mode"]), "pairing_mode": bool(
self._surepy_entity.raw_data()["status"]["pairing_mode"]
),
} }
return attributes return attributes
@ -170,13 +153,13 @@ class Pet(SurePetcareBinarySensor):
def __init__(self, _id: int, spc: SurePetcareAPI) -> None: def __init__(self, _id: int, spc: SurePetcareAPI) -> None:
"""Initialize a Sure Petcare Pet.""" """Initialize a Sure Petcare Pet."""
super().__init__(_id, spc, DEVICE_CLASS_PRESENCE, SurepyProduct.PET) super().__init__(_id, spc, DEVICE_CLASS_PRESENCE, EntityType.PET)
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return true if entity is at home.""" """Return true if entity is at home."""
try: try:
return bool(SureLocationID(self._state["where"]) == SureLocationID.INSIDE) return bool(Location(self._state.where) == Location.INSIDE)
except (KeyError, TypeError): except (KeyError, TypeError):
return False return False
@ -185,19 +168,15 @@ class Pet(SurePetcareBinarySensor):
"""Return the state attributes of the device.""" """Return the state attributes of the device."""
attributes = None attributes = None
if self._state: if self._state:
attributes = { attributes = {"since": self._state.since, "where": self._state.where}
"since": str(
datetime.fromisoformat(self._state["since"]).replace(tzinfo=None)
),
"where": SureLocationID(self._state["where"]).name.capitalize(),
}
return attributes return attributes
async def async_update(self) -> None: @callback
def _async_update(self) -> None:
"""Get the latest data and update the state.""" """Get the latest data and update the state."""
self._spc_data = self._spc.states[self._sure_type].get(self._id) self._surepy_entity = self._spc.states[self._id]
self._state = self._spc_data.get("position") self._state = self._surepy_entity.location
_LOGGER.debug("%s -> self._state: %s", self._name, self._state) _LOGGER.debug("%s -> self._state: %s", self._name, self._state)
@ -207,7 +186,7 @@ class DeviceConnectivity(SurePetcareBinarySensor):
def __init__( def __init__(
self, self,
_id: int, _id: int,
sure_type: SurepyProduct, sure_type: EntityType,
spc: SurePetcareAPI, spc: SurePetcareAPI,
) -> None: ) -> None:
"""Initialize a Sure Petcare Device.""" """Initialize a Sure Petcare Device."""
@ -221,7 +200,7 @@ class DeviceConnectivity(SurePetcareBinarySensor):
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Return an unique ID.""" """Return an unique ID."""
return f"{self._spc_data['household_id']}-{self._id}-connectivity" return f"{self._surepy_entity.household_id}-{self._id}-connectivity"
@property @property
def available(self) -> bool: def available(self) -> bool:

View File

@ -1,24 +1,11 @@
"""Constants for the Sure Petcare component.""" """Constants for the Sure Petcare component."""
from datetime import timedelta
DOMAIN = "surepetcare" DOMAIN = "surepetcare"
DEFAULT_DEVICE_CLASS = "lock"
DEFAULT_ICON = "mdi:cat"
DEFAULT_SCAN_INTERVAL = timedelta(minutes=3)
DATA_SURE_PETCARE = f"data_{DOMAIN}"
SPC = "spc" SPC = "spc"
SUREPY = "surepy"
CONF_HOUSEHOLD_ID = "household_id"
CONF_FEEDERS = "feeders" CONF_FEEDERS = "feeders"
CONF_FLAPS = "flaps" CONF_FLAPS = "flaps"
CONF_PARENT = "parent"
CONF_PETS = "pets" CONF_PETS = "pets"
CONF_PRODUCT_ID = "product_id"
CONF_DATA = "data"
SURE_IDS = "sure_ids"
# platforms # platforms
TOPIC_UPDATE = f"{DOMAIN}_data_update" TOPIC_UPDATE = f"{DOMAIN}_data_update"
@ -27,7 +14,6 @@ TOPIC_UPDATE = f"{DOMAIN}_data_update"
SURE_API_TIMEOUT = 60 SURE_API_TIMEOUT = 60
# flap # flap
BATTERY_ICON = "mdi:battery"
SURE_BATT_VOLTAGE_FULL = 1.6 # voltage SURE_BATT_VOLTAGE_FULL = 1.6 # voltage
SURE_BATT_VOLTAGE_LOW = 1.25 # voltage SURE_BATT_VOLTAGE_LOW = 1.25 # voltage
SURE_BATT_VOLTAGE_DIFF = SURE_BATT_VOLTAGE_FULL - SURE_BATT_VOLTAGE_LOW SURE_BATT_VOLTAGE_DIFF = SURE_BATT_VOLTAGE_FULL - SURE_BATT_VOLTAGE_LOW

View File

@ -3,6 +3,6 @@
"name": "Sure Petcare", "name": "Sure Petcare",
"documentation": "https://www.home-assistant.io/integrations/surepetcare", "documentation": "https://www.home-assistant.io/integrations/surepetcare",
"codeowners": ["@benleb"], "codeowners": ["@benleb"],
"requirements": ["surepy==0.4.0"], "requirements": ["surepy==0.6.0"],
"iot_class": "cloud_polling" "iot_class": "cloud_polling"
} }

View File

@ -4,22 +4,17 @@ from __future__ import annotations
import logging import logging
from typing import Any from typing import Any
from surepy import SureLockStateID, SurepyProduct from surepy.entities import SurepyEntity
from surepy.enums import EntityType
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ( from homeassistant.const import ATTR_VOLTAGE, DEVICE_CLASS_BATTERY, PERCENTAGE
ATTR_VOLTAGE,
CONF_ID,
CONF_TYPE,
DEVICE_CLASS_BATTERY,
PERCENTAGE,
)
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import SurePetcareAPI from . import SurePetcareAPI
from .const import ( from .const import (
DATA_SURE_PETCARE, DOMAIN,
SPC, SPC,
SURE_BATT_VOLTAGE_DIFF, SURE_BATT_VOLTAGE_DIFF,
SURE_BATT_VOLTAGE_LOW, SURE_BATT_VOLTAGE_LOW,
@ -34,56 +29,39 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
if discovery_info is None: if discovery_info is None:
return return
entities = [] entities: list[SurepyEntity] = []
spc = hass.data[DATA_SURE_PETCARE][SPC] spc: SurePetcareAPI = hass.data[DOMAIN][SPC]
for entity in spc.ids: for surepy_entity in spc.states.values():
sure_type = entity[CONF_TYPE]
if sure_type in [ if surepy_entity.type in [
SurepyProduct.CAT_FLAP, EntityType.CAT_FLAP,
SurepyProduct.PET_FLAP, EntityType.PET_FLAP,
SurepyProduct.FEEDER, EntityType.FEEDER,
EntityType.FELAQUA,
]: ]:
entities.append(SureBattery(entity[CONF_ID], sure_type, spc)) entities.append(SureBattery(surepy_entity.id, spc))
if sure_type in [SurepyProduct.CAT_FLAP, SurepyProduct.PET_FLAP]: async_add_entities(entities)
entities.append(Flap(entity[CONF_ID], sure_type, spc))
async_add_entities(entities, True)
class SurePetcareSensor(SensorEntity): class SurePetcareSensor(SensorEntity):
"""A binary sensor implementation for Sure Petcare Entities.""" """A binary sensor implementation for Sure Petcare Entities."""
def __init__(self, _id: int, sure_type: SurepyProduct, spc: SurePetcareAPI): def __init__(self, _id: int, spc: SurePetcareAPI):
"""Initialize a Sure Petcare sensor.""" """Initialize a Sure Petcare sensor."""
self._id = _id self._id = _id
self._sure_type = sure_type self._spc: SurePetcareAPI = spc
self._spc = spc self._surepy_entity: SurepyEntity = self._spc.states[_id]
self._spc_data: dict[str, Any] = self._spc.states[self._sure_type].get(self._id)
self._state: dict[str, Any] = {} self._state: dict[str, Any] = {}
self._name = ( self._name = (
f"{self._sure_type.name.capitalize()} " f"{self._surepy_entity.type.name.capitalize()} "
f"{self._spc_data['name'].capitalize()}" f"{self._surepy_entity.name.capitalize()}"
) )
self._async_unsub_dispatcher_connect = None
@property
def name(self) -> str:
"""Return the name of the device if any."""
return self._name
@property
def unique_id(self) -> str:
"""Return an unique ID."""
return f"{self._spc_data['household_id']}-{self._id}"
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return true if entity is available.""" """Return true if entity is available."""
@ -94,46 +72,19 @@ class SurePetcareSensor(SensorEntity):
"""Return true.""" """Return true."""
return False return False
async def async_update(self) -> None: @callback
def _async_update(self) -> None:
"""Get the latest data and update the state.""" """Get the latest data and update the state."""
self._spc_data = self._spc.states[self._sure_type].get(self._id) self._surepy_entity = self._spc.states[self._id]
self._state = self._spc_data.get("status") self._state = self._surepy_entity.raw_data()["status"]
_LOGGER.debug("%s -> self._state: %s", self._name, self._state) _LOGGER.debug("%s -> self._state: %s", self._name, self._state)
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Register callbacks.""" """Register callbacks."""
self.async_on_remove(
@callback async_dispatcher_connect(self.hass, TOPIC_UPDATE, self._async_update)
def update() -> None:
"""Update the state."""
self.async_schedule_update_ha_state(True)
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
self.hass, TOPIC_UPDATE, update
) )
self._async_update()
async def async_will_remove_from_hass(self) -> None:
"""Disconnect dispatcher listener when removed."""
if self._async_unsub_dispatcher_connect:
self._async_unsub_dispatcher_connect()
class Flap(SurePetcareSensor):
"""Sure Petcare Flap."""
@property
def state(self) -> int | None:
"""Return battery level in percent."""
return SureLockStateID(self._state["locking"]["mode"]).name.capitalize()
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the device."""
attributes = None
if self._state:
attributes = {"learn_mode": bool(self._state["learn_mode"])}
return attributes
class SureBattery(SurePetcareSensor): class SureBattery(SurePetcareSensor):
@ -160,7 +111,7 @@ class SureBattery(SurePetcareSensor):
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Return an unique ID.""" """Return an unique ID."""
return f"{self._spc_data['household_id']}-{self._id}-battery" return f"{self._surepy_entity.household_id}-{self._surepy_entity.id}-battery"
@property @property
def device_class(self) -> str: def device_class(self) -> str:

View File

@ -2178,7 +2178,7 @@ sucks==0.9.4
sunwatcher==0.2.1 sunwatcher==0.2.1
# homeassistant.components.surepetcare # homeassistant.components.surepetcare
surepy==0.4.0 surepy==0.6.0
# homeassistant.components.swiss_hydrological_data # homeassistant.components.swiss_hydrological_data
swisshydrodata==0.1.0 swisshydrodata==0.1.0

View File

@ -1165,7 +1165,7 @@ subarulink==0.3.12
sunwatcher==0.2.1 sunwatcher==0.2.1
# homeassistant.components.surepetcare # homeassistant.components.surepetcare
surepy==0.4.0 surepy==0.6.0
# homeassistant.components.synology_dsm # homeassistant.components.synology_dsm
synologydsm-api==1.0.2 synologydsm-api==1.0.2

View File

@ -1,11 +1,9 @@
"""Tests for Sure Petcare integration.""" """Tests for Sure Petcare integration."""
from unittest.mock import patch
from homeassistant.components.surepetcare.const import DOMAIN from homeassistant.components.surepetcare.const import DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
HOUSEHOLD_ID = "household-id" HOUSEHOLD_ID = 987654321
HUB_ID = "hub-id" HUB_ID = 123456789
MOCK_HUB = { MOCK_HUB = {
"id": HUB_ID, "id": HUB_ID,
@ -79,10 +77,3 @@ MOCK_CONFIG = {
"pets": [24680], "pets": [24680],
}, },
} }
def _patch_sensor_setup():
return patch(
"homeassistant.components.surepetcare.sensor.async_setup_platform",
return_value=True,
)

View File

@ -1,22 +1,18 @@
"""Define fixtures available for all tests.""" """Define fixtures available for all tests."""
from unittest.mock import AsyncMock, patch from unittest.mock import patch
from pytest import fixture import pytest
from surepy import SurePetcare from surepy import MESTART_RESOURCE
from homeassistant.helpers.aiohttp_client import async_get_clientsession from . import MOCK_API_DATA
@fixture @pytest.fixture
async def surepetcare(hass): async def surepetcare():
"""Mock the SurePetcare for easier testing.""" """Mock the SurePetcare for easier testing."""
with patch("homeassistant.components.surepetcare.SurePetcare") as mock_surepetcare: with patch("surepy.SureAPIClient", autospec=True) as mock_client_class, patch(
instance = mock_surepetcare.return_value = SurePetcare( "surepy.find_token"
"test-username", ):
"test-password", client = mock_client_class.return_value
hass.loop, client.resources = {MESTART_RESOURCE: {"data": MOCK_API_DATA}}
async_get_clientsession(hass), yield client
api_timeout=1,
)
instance._get_resource = AsyncMock(return_value=None)
yield mock_surepetcare

View File

@ -1,34 +1,32 @@
"""The tests for the Sure Petcare binary sensor platform.""" """The tests for the Sure Petcare binary sensor platform."""
from surepy import MESTART_RESOURCE
from homeassistant.components.surepetcare.const import DOMAIN from homeassistant.components.surepetcare.const import DOMAIN
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from . import MOCK_API_DATA, MOCK_CONFIG, _patch_sensor_setup from . import HOUSEHOLD_ID, HUB_ID, MOCK_CONFIG
EXPECTED_ENTITY_IDS = { EXPECTED_ENTITY_IDS = {
"binary_sensor.pet_flap_pet_flap_connectivity": "household-id-13576-connectivity", "binary_sensor.pet_flap_pet_flap_connectivity": f"{HOUSEHOLD_ID}-13576-connectivity",
"binary_sensor.pet_flap_cat_flap_connectivity": "household-id-13579-connectivity", "binary_sensor.cat_flap_cat_flap_connectivity": f"{HOUSEHOLD_ID}-13579-connectivity",
"binary_sensor.feeder_feeder_connectivity": "household-id-12345-connectivity", "binary_sensor.feeder_feeder_connectivity": f"{HOUSEHOLD_ID}-12345-connectivity",
"binary_sensor.pet_pet": "household-id-24680", "binary_sensor.pet_pet": f"{HOUSEHOLD_ID}-24680",
"binary_sensor.hub_hub": "household-id-hub-id", "binary_sensor.hub_hub": f"{HOUSEHOLD_ID}-{HUB_ID}",
} }
async def test_binary_sensors(hass, surepetcare) -> None: async def test_binary_sensors(hass, surepetcare) -> None:
"""Test the generation of unique ids.""" """Test the generation of unique ids."""
instance = surepetcare.return_value assert await async_setup_component(hass, DOMAIN, MOCK_CONFIG)
instance._resource[MESTART_RESOURCE] = {"data": MOCK_API_DATA} await hass.async_block_till_done()
with _patch_sensor_setup():
assert await async_setup_component(hass, DOMAIN, MOCK_CONFIG)
await hass.async_block_till_done()
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
state_entity_ids = hass.states.async_entity_ids() state_entity_ids = hass.states.async_entity_ids()
for entity_id, unique_id in EXPECTED_ENTITY_IDS.items(): for entity_id, unique_id in EXPECTED_ENTITY_IDS.items():
assert entity_id in state_entity_ids assert entity_id in state_entity_ids
state = hass.states.get(entity_id)
assert state
assert state.state == "on"
entity = entity_registry.async_get(entity_id) entity = entity_registry.async_get(entity_id)
assert entity.unique_id == unique_id assert entity.unique_id == unique_id

View File

@ -0,0 +1,29 @@
"""Test the surepetcare sensor platform."""
from homeassistant.components.surepetcare.const import DOMAIN
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from . import HOUSEHOLD_ID, MOCK_CONFIG
EXPECTED_ENTITY_IDS = {
"sensor.pet_flap_pet_flap_battery_level": f"{HOUSEHOLD_ID}-13576-battery",
"sensor.cat_flap_cat_flap_battery_level": f"{HOUSEHOLD_ID}-13579-battery",
"sensor.feeder_feeder_battery_level": f"{HOUSEHOLD_ID}-12345-battery",
}
async def test_binary_sensors(hass, surepetcare) -> None:
"""Test the generation of unique ids."""
assert await async_setup_component(hass, DOMAIN, MOCK_CONFIG)
await hass.async_block_till_done()
entity_registry = er.async_get(hass)
state_entity_ids = hass.states.async_entity_ids()
for entity_id, unique_id in EXPECTED_ENTITY_IDS.items():
assert entity_id in state_entity_ids
state = hass.states.get(entity_id)
assert state
assert state.state == "100"
entity = entity_registry.async_get(entity_id)
assert entity.unique_id == unique_id