Fix rachio webhook not being unregistered on unload (#73795)
This commit is contained in:
parent
504f4a7acf
commit
4bfdc61045
7 changed files with 89 additions and 55 deletions
|
@ -17,6 +17,7 @@ from .device import RachioPerson
|
|||
from .webhooks import (
|
||||
async_get_or_create_registered_webhook_id_and_url,
|
||||
async_register_webhook,
|
||||
async_unregister_webhook,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -28,8 +29,8 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
|
|||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
async_unregister_webhook(hass, entry)
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
|
||||
|
@ -59,10 +60,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
# Get the URL of this server
|
||||
rachio.webhook_auth = secrets.token_hex()
|
||||
try:
|
||||
(
|
||||
webhook_id,
|
||||
webhook_url,
|
||||
) = await async_get_or_create_registered_webhook_id_and_url(hass, entry)
|
||||
webhook_url = await async_get_or_create_registered_webhook_id_and_url(
|
||||
hass, entry
|
||||
)
|
||||
except cloud.CloudNotConnected as exc:
|
||||
# User has an active cloud subscription, but the connection to the cloud is down
|
||||
raise ConfigEntryNotReady from exc
|
||||
|
@ -92,9 +92,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
)
|
||||
|
||||
# Enable platform
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = person
|
||||
async_register_webhook(hass, webhook_id, entry.entry_id)
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = person
|
||||
async_register_webhook(hass, entry)
|
||||
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ from homeassistant.components.binary_sensor import (
|
|||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
|
@ -21,6 +22,7 @@ from .const import (
|
|||
SIGNAL_RACHIO_RAIN_SENSOR_UPDATE,
|
||||
STATUS_ONLINE,
|
||||
)
|
||||
from .device import RachioPerson
|
||||
from .entity import RachioDevice
|
||||
from .webhooks import (
|
||||
SUBTYPE_COLD_REBOOT,
|
||||
|
@ -41,12 +43,13 @@ async def async_setup_entry(
|
|||
"""Set up the Rachio binary sensors."""
|
||||
entities = await hass.async_add_executor_job(_create_entities, hass, config_entry)
|
||||
async_add_entities(entities)
|
||||
_LOGGER.info("%d Rachio binary sensor(s) added", len(entities))
|
||||
_LOGGER.debug("%d Rachio binary sensor(s) added", len(entities))
|
||||
|
||||
|
||||
def _create_entities(hass, config_entry):
|
||||
entities = []
|
||||
for controller in hass.data[DOMAIN_RACHIO][config_entry.entry_id].controllers:
|
||||
def _create_entities(hass: HomeAssistant, config_entry: ConfigEntry) -> list[Entity]:
|
||||
entities: list[Entity] = []
|
||||
person: RachioPerson = hass.data[DOMAIN_RACHIO][config_entry.entry_id]
|
||||
for controller in person.controllers:
|
||||
entities.append(RachioControllerOnlineBinarySensor(controller))
|
||||
entities.append(RachioRainSensor(controller))
|
||||
return entities
|
||||
|
|
|
@ -67,3 +67,13 @@ SIGNAL_RACHIO_SCHEDULE_UPDATE = f"{SIGNAL_RACHIO_UPDATE}_schedule"
|
|||
|
||||
CONF_WEBHOOK_ID = "webhook_id"
|
||||
CONF_CLOUDHOOK_URL = "cloudhook_url"
|
||||
|
||||
# Webhook callbacks
|
||||
LISTEN_EVENT_TYPES = [
|
||||
"DEVICE_STATUS_EVENT",
|
||||
"ZONE_STATUS_EVENT",
|
||||
"RAIN_DELAY_EVENT",
|
||||
"RAIN_SENSOR_DETECTION_EVENT",
|
||||
"SCHEDULE_STATUS_EVENT",
|
||||
]
|
||||
WEBHOOK_CONST_ID = "homeassistant.rachio:"
|
||||
|
|
|
@ -3,11 +3,14 @@ from __future__ import annotations
|
|||
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from rachiopy import Rachio
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import ServiceCall
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
|
@ -26,12 +29,13 @@ from .const import (
|
|||
KEY_STATUS,
|
||||
KEY_USERNAME,
|
||||
KEY_ZONES,
|
||||
LISTEN_EVENT_TYPES,
|
||||
MODEL_GENERATION_1,
|
||||
SERVICE_PAUSE_WATERING,
|
||||
SERVICE_RESUME_WATERING,
|
||||
SERVICE_STOP_WATERING,
|
||||
WEBHOOK_CONST_ID,
|
||||
)
|
||||
from .webhooks import LISTEN_EVENT_TYPES, WEBHOOK_CONST_ID
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -54,16 +58,16 @@ STOP_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_DEVICES): cv.string})
|
|||
class RachioPerson:
|
||||
"""Represent a Rachio user."""
|
||||
|
||||
def __init__(self, rachio, config_entry):
|
||||
def __init__(self, rachio: Rachio, config_entry: ConfigEntry) -> None:
|
||||
"""Create an object from the provided API instance."""
|
||||
# Use API token to get user ID
|
||||
self.rachio = rachio
|
||||
self.config_entry = config_entry
|
||||
self.username = None
|
||||
self._id = None
|
||||
self._controllers = []
|
||||
self._id: str | None = None
|
||||
self._controllers: list[RachioIro] = []
|
||||
|
||||
async def async_setup(self, hass):
|
||||
async def async_setup(self, hass: HomeAssistant) -> None:
|
||||
"""Create rachio devices and services."""
|
||||
await hass.async_add_executor_job(self._setup, hass)
|
||||
can_pause = False
|
||||
|
@ -121,7 +125,7 @@ class RachioPerson:
|
|||
schema=RESUME_SERVICE_SCHEMA,
|
||||
)
|
||||
|
||||
def _setup(self, hass):
|
||||
def _setup(self, hass: HomeAssistant) -> None:
|
||||
"""Rachio device setup."""
|
||||
rachio = self.rachio
|
||||
|
||||
|
@ -139,7 +143,7 @@ class RachioPerson:
|
|||
if int(data[0][KEY_STATUS]) != HTTPStatus.OK:
|
||||
raise ConfigEntryNotReady(f"API Error: {data}")
|
||||
self.username = data[1][KEY_USERNAME]
|
||||
devices = data[1][KEY_DEVICES]
|
||||
devices: list[dict[str, Any]] = data[1][KEY_DEVICES]
|
||||
for controller in devices:
|
||||
webhooks = rachio.notification.get_device_webhook(controller[KEY_ID])[1]
|
||||
# The API does not provide a way to tell if a controller is shared
|
||||
|
@ -169,12 +173,12 @@ class RachioPerson:
|
|||
_LOGGER.info('Using Rachio API as user "%s"', self.username)
|
||||
|
||||
@property
|
||||
def user_id(self) -> str:
|
||||
def user_id(self) -> str | None:
|
||||
"""Get the user ID as defined by the Rachio API."""
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def controllers(self) -> list:
|
||||
def controllers(self) -> list[RachioIro]:
|
||||
"""Get a list of controllers managed by this account."""
|
||||
return self._controllers
|
||||
|
||||
|
@ -186,7 +190,13 @@ class RachioPerson:
|
|||
class RachioIro:
|
||||
"""Represent a Rachio Iro."""
|
||||
|
||||
def __init__(self, hass, rachio, data, webhooks):
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
rachio: Rachio,
|
||||
data: dict[str, Any],
|
||||
webhooks: list[dict[str, Any]],
|
||||
) -> None:
|
||||
"""Initialize a Rachio device."""
|
||||
self.hass = hass
|
||||
self.rachio = rachio
|
||||
|
@ -199,10 +209,10 @@ class RachioIro:
|
|||
self._schedules = data[KEY_SCHEDULES]
|
||||
self._flex_schedules = data[KEY_FLEX_SCHEDULES]
|
||||
self._init_data = data
|
||||
self._webhooks = webhooks
|
||||
self._webhooks: list[dict[str, Any]] = webhooks
|
||||
_LOGGER.debug('%s has ID "%s"', self, self.controller_id)
|
||||
|
||||
def setup(self):
|
||||
def setup(self) -> None:
|
||||
"""Rachio Iro setup for webhooks."""
|
||||
# Listen for all updates
|
||||
self._init_webhooks()
|
||||
|
@ -226,7 +236,7 @@ class RachioIro:
|
|||
or webhook[KEY_ID] == current_webhook_id
|
||||
):
|
||||
self.rachio.notification.delete(webhook[KEY_ID])
|
||||
self._webhooks = None
|
||||
self._webhooks = []
|
||||
|
||||
_deinit_webhooks(None)
|
||||
|
||||
|
@ -306,9 +316,6 @@ class RachioIro:
|
|||
_LOGGER.debug("Resuming watering on %s", self)
|
||||
|
||||
|
||||
def is_invalid_auth_code(http_status_code):
|
||||
def is_invalid_auth_code(http_status_code: int) -> bool:
|
||||
"""HTTP status codes that mean invalid auth."""
|
||||
if http_status_code in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN):
|
||||
return True
|
||||
|
||||
return False
|
||||
return http_status_code in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN)
|
||||
|
|
|
@ -4,25 +4,19 @@ from homeassistant.helpers import device_registry
|
|||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
|
||||
from .const import DEFAULT_NAME, DOMAIN
|
||||
from .device import RachioIro
|
||||
|
||||
|
||||
class RachioDevice(Entity):
|
||||
"""Base class for rachio devices."""
|
||||
|
||||
def __init__(self, controller):
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(self, controller: RachioIro) -> None:
|
||||
"""Initialize a Rachio device."""
|
||||
super().__init__()
|
||||
self._controller = controller
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Declare that this entity pushes its state to HA."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device_info of the device."""
|
||||
return DeviceInfo(
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={
|
||||
(
|
||||
DOMAIN,
|
||||
|
|
|
@ -13,6 +13,7 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback
|
|||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.util.dt import as_timestamp, now, parse_datetime, utc_from_timestamp
|
||||
|
@ -52,6 +53,7 @@ from .const import (
|
|||
SLOPE_SLIGHT,
|
||||
SLOPE_STEEP,
|
||||
)
|
||||
from .device import RachioPerson
|
||||
from .entity import RachioDevice
|
||||
from .webhooks import (
|
||||
SUBTYPE_RAIN_DELAY_OFF,
|
||||
|
@ -106,7 +108,7 @@ async def async_setup_entry(
|
|||
has_flex_sched = True
|
||||
|
||||
async_add_entities(entities)
|
||||
_LOGGER.info("%d Rachio switch(es) added", len(entities))
|
||||
_LOGGER.debug("%d Rachio switch(es) added", len(entities))
|
||||
|
||||
def start_multiple(service: ServiceCall) -> None:
|
||||
"""Service to start multiple zones in sequence."""
|
||||
|
@ -154,9 +156,9 @@ async def async_setup_entry(
|
|||
)
|
||||
|
||||
|
||||
def _create_entities(hass, config_entry):
|
||||
entities = []
|
||||
person = hass.data[DOMAIN_RACHIO][config_entry.entry_id]
|
||||
def _create_entities(hass: HomeAssistant, config_entry: ConfigEntry) -> list[Entity]:
|
||||
entities: list[Entity] = []
|
||||
person: RachioPerson = hass.data[DOMAIN_RACHIO][config_entry.entry_id]
|
||||
# Fetch the schedule once at startup
|
||||
# in order to avoid every zone doing it
|
||||
for controller in person.controllers:
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
"""Webhooks used by rachio."""
|
||||
from __future__ import annotations
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from homeassistant.components import cloud, webhook
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import URL_API
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from .const import (
|
||||
|
@ -18,6 +21,7 @@ from .const import (
|
|||
SIGNAL_RACHIO_SCHEDULE_UPDATE,
|
||||
SIGNAL_RACHIO_ZONE_UPDATE,
|
||||
)
|
||||
from .device import RachioPerson
|
||||
|
||||
# Device webhook values
|
||||
TYPE_CONTROLLER_STATUS = "DEVICE_STATUS"
|
||||
|
@ -79,16 +83,22 @@ SIGNAL_MAP = {
|
|||
|
||||
|
||||
@callback
|
||||
def async_register_webhook(hass, webhook_id, entry_id):
|
||||
def async_register_webhook(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Register a webhook."""
|
||||
webhook_id: str = entry.data[CONF_WEBHOOK_ID]
|
||||
|
||||
async def _async_handle_rachio_webhook(hass, webhook_id, request):
|
||||
async def _async_handle_rachio_webhook(
|
||||
hass: HomeAssistant, webhook_id: str, request: web.Request
|
||||
) -> web.Response:
|
||||
"""Handle webhook calls from the server."""
|
||||
person: RachioPerson = hass.data[DOMAIN][entry.entry_id]
|
||||
data = await request.json()
|
||||
|
||||
try:
|
||||
auth = data.get(KEY_EXTERNAL_ID, "").split(":")[1]
|
||||
assert auth == hass.data[DOMAIN][entry_id].rachio.webhook_auth
|
||||
assert (
|
||||
data.get(KEY_EXTERNAL_ID, "").split(":")[1]
|
||||
== person.rachio.webhook_auth
|
||||
)
|
||||
except (AssertionError, IndexError):
|
||||
return web.Response(status=web.HTTPForbidden.status_code)
|
||||
|
||||
|
@ -103,8 +113,17 @@ def async_register_webhook(hass, webhook_id, entry_id):
|
|||
)
|
||||
|
||||
|
||||
async def async_get_or_create_registered_webhook_id_and_url(hass, entry):
|
||||
"""Generate webhook ID."""
|
||||
@callback
|
||||
def async_unregister_webhook(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Unregister a webhook."""
|
||||
webhook_id: str = entry.data[CONF_WEBHOOK_ID]
|
||||
webhook.async_unregister(hass, webhook_id)
|
||||
|
||||
|
||||
async def async_get_or_create_registered_webhook_id_and_url(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> str:
|
||||
"""Generate webhook url."""
|
||||
config = entry.data.copy()
|
||||
|
||||
updated_config = False
|
||||
|
@ -128,4 +147,4 @@ async def async_get_or_create_registered_webhook_id_and_url(hass, entry):
|
|||
if updated_config:
|
||||
hass.config_entries.async_update_entry(entry, data=config)
|
||||
|
||||
return webhook_id, webhook_url
|
||||
return webhook_url
|
||||
|
|
Loading…
Add table
Reference in a new issue