Add surepetcare component (#24426)
* add surepetcare * cleanup * remove unused imports and comments * remove comment * fix bug which prevented updating the sensors * improve config validation * fix voluptuous usage * fix format & credential storage * various fixes to hass-conform * small format fixes * change False to None * still trying to be hass-conform * remove unused class * fix imports * fix f-string * add guard clause?! * central data fetch * do not pass in hass, will be provided automatically * make the linters happy * disable constant-test warning and add commas * worksforme * fix link in manifest * remove icon * bump surepy to 0.1.5 * worksforme * small doc fixes * add discovery_info guard * result of another awesome review * and again :) * exclude surepetcare in .coveragerc
This commit is contained in:
parent
0971c681af
commit
1fffa210e1
8 changed files with 513 additions and 0 deletions
|
@ -665,6 +665,7 @@ omit =
|
|||
homeassistant/components/streamlabswater/*
|
||||
homeassistant/components/suez_water/*
|
||||
homeassistant/components/supervisord/sensor.py
|
||||
homeassistant/components/surepetcare/*.py
|
||||
homeassistant/components/swiss_hydrological_data/sensor.py
|
||||
homeassistant/components/swiss_public_transport/sensor.py
|
||||
homeassistant/components/swisscom/device_tracker.py
|
||||
|
|
|
@ -314,6 +314,7 @@ homeassistant/components/stt/* @pvizeli
|
|||
homeassistant/components/suez_water/* @ooii
|
||||
homeassistant/components/sun/* @Swamp-Ig
|
||||
homeassistant/components/supla/* @mwegrzynek
|
||||
homeassistant/components/surepetcare/* @benleb
|
||||
homeassistant/components/swiss_hydrological_data/* @fabaff
|
||||
homeassistant/components/swiss_public_transport/* @fabaff
|
||||
homeassistant/components/switchbot/* @danielhiversen
|
||||
|
|
163
homeassistant/components/surepetcare/__init__.py
Normal file
163
homeassistant/components/surepetcare/__init__.py
Normal file
|
@ -0,0 +1,163 @@
|
|||
"""Support for Sure Petcare cat/pet flaps."""
|
||||
import logging
|
||||
|
||||
from surepy import (
|
||||
SurePetcare,
|
||||
SurePetcareAuthenticationError,
|
||||
SurePetcareError,
|
||||
SureThingID,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_ID,
|
||||
CONF_NAME,
|
||||
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 (
|
||||
CONF_FLAPS,
|
||||
CONF_HOUSEHOLD_ID,
|
||||
CONF_PETS,
|
||||
DATA_SURE_PETCARE,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DOMAIN,
|
||||
SPC,
|
||||
TOPIC_UPDATE,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
FLAP_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_ID): cv.positive_int, vol.Required(CONF_NAME): cv.string}
|
||||
)
|
||||
|
||||
PET_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_ID): cv.positive_int, vol.Required(CONF_NAME): cv.string}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_HOUSEHOLD_ID): cv.positive_int,
|
||||
vol.Required(CONF_FLAPS): vol.All(cv.ensure_list, [FLAP_SCHEMA]),
|
||||
vol.Required(CONF_PETS): vol.All(cv.ensure_list, [PET_SCHEMA]),
|
||||
vol.Optional(
|
||||
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
|
||||
): cv.time_period,
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""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],
|
||||
conf[CONF_HOUSEHOLD_ID],
|
||||
hass.loop,
|
||||
async_get_clientsession(hass),
|
||||
)
|
||||
await surepy.refresh_token()
|
||||
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 flaps
|
||||
things = [
|
||||
{
|
||||
CONF_NAME: flap[CONF_NAME],
|
||||
CONF_ID: flap[CONF_ID],
|
||||
CONF_TYPE: SureThingID.FLAP.name,
|
||||
}
|
||||
for flap in conf[CONF_FLAPS]
|
||||
]
|
||||
|
||||
# add pets
|
||||
things.extend(
|
||||
[
|
||||
{
|
||||
CONF_NAME: pet[CONF_NAME],
|
||||
CONF_ID: pet[CONF_ID],
|
||||
CONF_TYPE: SureThingID.PET.name,
|
||||
}
|
||||
for pet in conf[CONF_PETS]
|
||||
]
|
||||
)
|
||||
|
||||
spc = hass.data[DATA_SURE_PETCARE][SPC] = SurePetcareAPI(
|
||||
hass, surepy, things, conf[CONF_HOUSEHOLD_ID]
|
||||
)
|
||||
|
||||
# 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)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class SurePetcareAPI:
|
||||
"""Define a generic Sure Petcare object."""
|
||||
|
||||
def __init__(self, hass, surepy, ids, household_id):
|
||||
"""Initialize the Sure Petcare object."""
|
||||
self.hass = hass
|
||||
self.surepy = surepy
|
||||
self.household_id = household_id
|
||||
self.ids = ids
|
||||
self.states = {}
|
||||
|
||||
async def async_update(self, args=None):
|
||||
"""Refresh Sure Petcare data."""
|
||||
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 == SureThingID.FLAP.name:
|
||||
type_state[sure_id] = await self.surepy.get_flap_data(sure_id)
|
||||
elif sure_type == SureThingID.PET.name:
|
||||
type_state[sure_id] = await self.surepy.get_pet_data(sure_id)
|
||||
|
||||
except SurePetcareError as error:
|
||||
_LOGGER.error("Unable to retrieve data from surepetcare.io: %s", error)
|
||||
|
||||
async_dispatcher_send(self.hass, TOPIC_UPDATE)
|
176
homeassistant/components/surepetcare/binary_sensor.py
Normal file
176
homeassistant/components/surepetcare/binary_sensor.py
Normal file
|
@ -0,0 +1,176 @@
|
|||
"""Support for Sure PetCare Flaps/Pets binary sensors."""
|
||||
import logging
|
||||
|
||||
from surepy import SureLocationID, SureLockStateID, SureThingID
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASS_LOCK,
|
||||
DEVICE_CLASS_PRESENCE,
|
||||
BinarySensorDevice,
|
||||
)
|
||||
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .const import DATA_SURE_PETCARE, DEFAULT_DEVICE_CLASS, SPC, TOPIC_UPDATE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up Sure PetCare Flaps sensors based on a config entry."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
entities = []
|
||||
|
||||
spc = hass.data[DATA_SURE_PETCARE][SPC]
|
||||
|
||||
for thing in spc.ids:
|
||||
sure_id = thing[CONF_ID]
|
||||
sure_type = thing[CONF_TYPE]
|
||||
|
||||
if sure_type == SureThingID.FLAP.name:
|
||||
entity = Flap(sure_id, thing[CONF_NAME], spc)
|
||||
elif sure_type == SureThingID.PET.name:
|
||||
entity = Pet(sure_id, thing[CONF_NAME], spc)
|
||||
|
||||
entities.append(entity)
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class SurePetcareBinarySensor(BinarySensorDevice):
|
||||
"""A binary sensor implementation for Sure Petcare Entities."""
|
||||
|
||||
def __init__(
|
||||
self, _id: int, name: str, spc, device_class: str, sure_type: SureThingID
|
||||
):
|
||||
"""Initialize a Sure Petcare binary sensor."""
|
||||
self._id = _id
|
||||
self._name = name
|
||||
self._spc = spc
|
||||
self._device_class = device_class
|
||||
self._sure_type = sure_type
|
||||
self._state = {}
|
||||
|
||||
self._async_unsub_dispatcher_connect = None
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if entity is on/unlocked."""
|
||||
return bool(self._state)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return true."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the device."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class."""
|
||||
return DEFAULT_DEVICE_CLASS if not self._device_class else self._device_class
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return an unique ID."""
|
||||
return f"{self._spc.household_id}-{self._id}"
|
||||
|
||||
async def async_update(self):
|
||||
"""Get the latest data and update the state."""
|
||||
self._state = self._spc.states[self._sure_type][self._id].get("data")
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
|
||||
@callback
|
||||
def update():
|
||||
"""Update the state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
|
||||
self.hass, TOPIC_UPDATE, update
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect dispatcher listener when removed."""
|
||||
if self._async_unsub_dispatcher_connect:
|
||||
self._async_unsub_dispatcher_connect()
|
||||
|
||||
|
||||
class Flap(SurePetcareBinarySensor):
|
||||
"""Sure Petcare Flap."""
|
||||
|
||||
def __init__(self, _id: int, name: str, spc):
|
||||
"""Initialize a Sure Petcare Flap."""
|
||||
super().__init__(
|
||||
_id,
|
||||
f"Flap {name.capitalize()}",
|
||||
spc,
|
||||
DEVICE_CLASS_LOCK,
|
||||
SureThingID.FLAP.name,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if entity is on/unlocked."""
|
||||
try:
|
||||
return bool(self._state["locking"]["mode"] == SureLockStateID.UNLOCKED)
|
||||
except (KeyError, TypeError):
|
||||
return None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the device."""
|
||||
attributes = None
|
||||
if self._state:
|
||||
try:
|
||||
attributes = {
|
||||
"battery_voltage": self._state["battery"] / 4,
|
||||
"locking_mode": self._state["locking"]["mode"],
|
||||
"device_rssi": self._state["signal"]["device_rssi"],
|
||||
"hub_rssi": self._state["signal"]["hub_rssi"],
|
||||
}
|
||||
|
||||
except (KeyError, TypeError) as error:
|
||||
_LOGGER.error(
|
||||
"Error getting device state attributes from %s: %s\n\n%s",
|
||||
self._name,
|
||||
error,
|
||||
self._state,
|
||||
)
|
||||
attributes = self._state
|
||||
|
||||
return attributes
|
||||
|
||||
|
||||
class Pet(SurePetcareBinarySensor):
|
||||
"""Sure Petcare Pet."""
|
||||
|
||||
def __init__(self, _id: int, name: str, spc):
|
||||
"""Initialize a Sure Petcare Pet."""
|
||||
super().__init__(
|
||||
_id,
|
||||
f"Pet {name.capitalize()}",
|
||||
spc,
|
||||
DEVICE_CLASS_PRESENCE,
|
||||
SureThingID.PET.name,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if entity is at home."""
|
||||
try:
|
||||
return bool(self._state["where"] == SureLocationID.INSIDE)
|
||||
except (KeyError, TypeError):
|
||||
return False
|
27
homeassistant/components/surepetcare/const.py
Normal file
27
homeassistant/components/surepetcare/const.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
"""Constants for the Sure Petcare component."""
|
||||
from datetime import timedelta
|
||||
|
||||
DOMAIN = "surepetcare"
|
||||
DEFAULT_DEVICE_CLASS = "lock"
|
||||
DEFAULT_ICON = "mdi:cat"
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(minutes=3)
|
||||
|
||||
DATA_SURE_PETCARE = f"data_{DOMAIN}"
|
||||
SPC = "spc"
|
||||
SUREPY = "surepy"
|
||||
|
||||
CONF_HOUSEHOLD_ID = "household_id"
|
||||
CONF_FLAPS = "flaps"
|
||||
CONF_PETS = "pets"
|
||||
CONF_DATA = "data"
|
||||
|
||||
SURE_IDS = "sure_ids"
|
||||
|
||||
# platforms
|
||||
TOPIC_UPDATE = f"{DOMAIN}_data_update"
|
||||
|
||||
# flap
|
||||
BATTERY_ICON = "mdi:battery"
|
||||
SURE_BATT_VOLTAGE_FULL = 1.6 # voltage
|
||||
SURE_BATT_VOLTAGE_LOW = 1.25 # voltage
|
||||
SURE_BATT_VOLTAGE_DIFF = SURE_BATT_VOLTAGE_FULL - SURE_BATT_VOLTAGE_LOW
|
8
homeassistant/components/surepetcare/manifest.json
Normal file
8
homeassistant/components/surepetcare/manifest.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"domain": "surepetcare",
|
||||
"name": "Sure Petcare",
|
||||
"documentation": "https://www.home-assistant.io/integrations/surepetcare",
|
||||
"dependencies": [],
|
||||
"codeowners": ["@benleb"],
|
||||
"requirements": ["surepy==0.1.10"]
|
||||
}
|
134
homeassistant/components/surepetcare/sensor.py
Normal file
134
homeassistant/components/surepetcare/sensor.py
Normal file
|
@ -0,0 +1,134 @@
|
|||
"""Support for Sure PetCare Flaps/Pets sensors."""
|
||||
import logging
|
||||
|
||||
from surepy import SureThingID
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_VOLTAGE,
|
||||
CONF_ID,
|
||||
CONF_NAME,
|
||||
CONF_TYPE,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import (
|
||||
DATA_SURE_PETCARE,
|
||||
SPC,
|
||||
SURE_BATT_VOLTAGE_DIFF,
|
||||
SURE_BATT_VOLTAGE_LOW,
|
||||
TOPIC_UPDATE,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up Sure PetCare Flaps sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
spc = hass.data[DATA_SURE_PETCARE][SPC]
|
||||
async_add_entities(
|
||||
[
|
||||
FlapBattery(entity[CONF_ID], entity[CONF_NAME], spc)
|
||||
for entity in spc.ids
|
||||
if entity[CONF_TYPE] == SureThingID.FLAP.name
|
||||
],
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
class FlapBattery(Entity):
|
||||
"""Sure Petcare Flap."""
|
||||
|
||||
def __init__(self, _id: int, name: str, spc):
|
||||
"""Initialize a Sure Petcare Flap battery sensor."""
|
||||
self._id = _id
|
||||
self._name = f"Flap {name.capitalize()} Battery Level"
|
||||
self._spc = spc
|
||||
self._state = self._spc.states[SureThingID.FLAP.name][self._id].get("data")
|
||||
|
||||
self._async_unsub_dispatcher_connect = None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return true."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return battery level in percent."""
|
||||
try:
|
||||
per_battery_voltage = self._state["battery"] / 4
|
||||
voltage_diff = per_battery_voltage - SURE_BATT_VOLTAGE_LOW
|
||||
battery_percent = int(voltage_diff / SURE_BATT_VOLTAGE_DIFF * 100)
|
||||
except (KeyError, TypeError):
|
||||
battery_percent = None
|
||||
|
||||
return battery_percent
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return an unique ID."""
|
||||
return f"{self._spc.household_id}-{self._id}"
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class."""
|
||||
return DEVICE_CLASS_BATTERY
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return state attributes."""
|
||||
attributes = None
|
||||
if self._state:
|
||||
try:
|
||||
voltage_per_battery = float(self._state["battery"]) / 4
|
||||
attributes = {
|
||||
ATTR_VOLTAGE: f"{float(self._state['battery']):.2f}",
|
||||
f"{ATTR_VOLTAGE}_per_battery": f"{voltage_per_battery:.2f}",
|
||||
}
|
||||
except (KeyError, TypeError) as error:
|
||||
attributes = self._state
|
||||
_LOGGER.error(
|
||||
"Error getting device state attributes from %s: %s\n\n%s",
|
||||
self._name,
|
||||
error,
|
||||
self._state,
|
||||
)
|
||||
|
||||
return attributes
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return "%"
|
||||
|
||||
async def async_update(self):
|
||||
"""Get the latest data and update the state."""
|
||||
self._state = self._spc.states[SureThingID.FLAP.name][self._id].get("data")
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
|
||||
@callback
|
||||
def update():
|
||||
"""Update the state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
|
||||
self.hass, TOPIC_UPDATE, update
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect dispatcher listener when removed."""
|
||||
if self._async_unsub_dispatcher_connect:
|
||||
self._async_unsub_dispatcher_connect()
|
|
@ -1914,6 +1914,9 @@ sucks==0.9.4
|
|||
# homeassistant.components.solarlog
|
||||
sunwatcher==0.2.1
|
||||
|
||||
# homeassistant.components.surepetcare
|
||||
surepy==0.1.10
|
||||
|
||||
# homeassistant.components.swiss_hydrological_data
|
||||
swisshydrodata==0.0.3
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue