237 lines
7.2 KiB
Python
237 lines
7.2 KiB
Python
"""Support for Sure Petcare cat/pet flaps."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from surepy import (
|
|
MESTART_RESOURCE,
|
|
SureLockStateID,
|
|
SurePetcare,
|
|
SurePetcareAuthenticationError,
|
|
SurePetcareError,
|
|
SurepyProduct,
|
|
)
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.const import (
|
|
CONF_ID,
|
|
CONF_PASSWORD,
|
|
CONF_SCAN_INTERVAL,
|
|
CONF_TYPE,
|
|
CONF_USERNAME,
|
|
)
|
|
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 .const import (
|
|
ATTR_FLAP_ID,
|
|
ATTR_LOCK_STATE,
|
|
CONF_FEEDERS,
|
|
CONF_FLAPS,
|
|
CONF_PARENT,
|
|
CONF_PETS,
|
|
CONF_PRODUCT_ID,
|
|
DATA_SURE_PETCARE,
|
|
DEFAULT_SCAN_INTERVAL,
|
|
DOMAIN,
|
|
SERVICE_SET_LOCK_STATE,
|
|
SPC,
|
|
SURE_API_TIMEOUT,
|
|
TOPIC_UPDATE,
|
|
)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
DOMAIN: vol.Schema(
|
|
{
|
|
vol.Required(CONF_USERNAME): cv.string,
|
|
vol.Required(CONF_PASSWORD): cv.string,
|
|
vol.Optional(CONF_FEEDERS, default=[]): vol.All(
|
|
cv.ensure_list, [cv.positive_int]
|
|
),
|
|
vol.Optional(CONF_FLAPS, default=[]): vol.All(
|
|
cv.ensure_list, [cv.positive_int]
|
|
),
|
|
vol.Optional(CONF_PETS): vol.All(cv.ensure_list, [cv.positive_int]),
|
|
vol.Optional(
|
|
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
|
|
): cv.time_period,
|
|
}
|
|
)
|
|
},
|
|
extra=vol.ALLOW_EXTRA,
|
|
)
|
|
|
|
|
|
async def async_setup(hass, config) -> bool:
|
|
"""Initialize the Sure Petcare component."""
|
|
conf = config[DOMAIN]
|
|
|
|
# update interval
|
|
scan_interval = conf[CONF_SCAN_INTERVAL]
|
|
|
|
# shared data
|
|
hass.data[DOMAIN] = hass.data[DATA_SURE_PETCARE] = {}
|
|
|
|
# sure petcare api connection
|
|
try:
|
|
surepy = SurePetcare(
|
|
conf[CONF_USERNAME],
|
|
conf[CONF_PASSWORD],
|
|
hass.loop,
|
|
async_get_clientsession(hass),
|
|
api_timeout=SURE_API_TIMEOUT,
|
|
)
|
|
|
|
except SurePetcareAuthenticationError:
|
|
_LOGGER.error("Unable to connect to surepetcare.io: Wrong credentials!")
|
|
return False
|
|
except SurePetcareError as error:
|
|
_LOGGER.error("Unable to connect to surepetcare.io: Wrong %s!", error)
|
|
return False
|
|
|
|
# add feeders
|
|
things = [
|
|
{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()
|
|
|
|
async_track_time_interval(hass, spc.async_update, scan_interval)
|
|
|
|
# load platforms
|
|
hass.async_create_task(
|
|
hass.helpers.discovery.async_load_platform("binary_sensor", DOMAIN, {}, config)
|
|
)
|
|
hass.async_create_task(
|
|
hass.helpers.discovery.async_load_platform("sensor", DOMAIN, {}, config)
|
|
)
|
|
|
|
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()
|
|
|
|
lock_state_service_schema = vol.Schema(
|
|
{
|
|
vol.Required(ATTR_FLAP_ID): vol.All(
|
|
cv.positive_int, vol.In(conf[CONF_FLAPS])
|
|
),
|
|
vol.Required(ATTR_LOCK_STATE): vol.All(
|
|
cv.string,
|
|
vol.Lower,
|
|
vol.In(
|
|
[
|
|
SureLockStateID.UNLOCKED.name.lower(),
|
|
SureLockStateID.LOCKED_IN.name.lower(),
|
|
SureLockStateID.LOCKED_OUT.name.lower(),
|
|
SureLockStateID.LOCKED_ALL.name.lower(),
|
|
]
|
|
),
|
|
),
|
|
}
|
|
)
|
|
|
|
hass.services.async_register(
|
|
DOMAIN,
|
|
SERVICE_SET_LOCK_STATE,
|
|
handle_set_lock_state,
|
|
schema=lock_state_service_schema,
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
class SurePetcareAPI:
|
|
"""Define a generic Sure Petcare object."""
|
|
|
|
def __init__(self, hass, surepy: SurePetcare, ids: list[dict[str, Any]]) -> None:
|
|
"""Initialize the Sure Petcare object."""
|
|
self.hass = hass
|
|
self.surepy = surepy
|
|
self.ids = ids
|
|
self.states: dict[str, Any] = {}
|
|
|
|
async def async_update(self, arg: Any = None) -> None:
|
|
"""Refresh Sure Petcare data."""
|
|
|
|
# Fetch all data from SurePet API, refreshing the surepy cache
|
|
# TODO: get surepy upstream to add a method to clear the cache explicitly pylint: disable=fixme
|
|
await self.surepy._get_resource( # pylint: disable=protected-access
|
|
resource=MESTART_RESOURCE
|
|
)
|
|
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 def set_lock_state(self, flap_id: int, state: str) -> None:
|
|
"""Update the lock state of a flap."""
|
|
if state == SureLockStateID.UNLOCKED.name.lower():
|
|
await self.surepy.unlock(flap_id)
|
|
elif state == SureLockStateID.LOCKED_IN.name.lower():
|
|
await self.surepy.lock_in(flap_id)
|
|
elif state == SureLockStateID.LOCKED_OUT.name.lower():
|
|
await self.surepy.lock_out(flap_id)
|
|
elif state == SureLockStateID.LOCKED_ALL.name.lower():
|
|
await self.surepy.lock(flap_id)
|