221 lines
7.6 KiB
Python
221 lines
7.6 KiB
Python
"""The TTLock integration."""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
import secrets
|
|
|
|
from aiohttp.web import Request
|
|
|
|
from homeassistant.components import cloud, persistent_notification, webhook
|
|
from homeassistant.components.webhook import (
|
|
async_register as webhook_register,
|
|
async_unregister as webhook_unregister,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import (
|
|
CONF_WEBHOOK_ID,
|
|
EVENT_HOMEASSISTANT_STARTED,
|
|
EVENT_HOMEASSISTANT_STOP,
|
|
Platform,
|
|
)
|
|
from homeassistant.core import CoreState, Event, HomeAssistant
|
|
from homeassistant.helpers import (
|
|
aiohttp_client,
|
|
config_entry_oauth2_flow,
|
|
issue_registry as ir,
|
|
)
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
|
from homeassistant.helpers.network import NoURLAvailableError
|
|
|
|
from .api import TTLockApi
|
|
from .const import (
|
|
CONF_WEBHOOK_STATUS,
|
|
CONF_WEBHOOK_URL,
|
|
DOMAIN,
|
|
SIGNAL_NEW_DATA,
|
|
TT_API,
|
|
TT_LOCKS,
|
|
)
|
|
from .coordinator import LockUpdateCoordinator
|
|
from .models import WebhookEvent
|
|
from .services import Services
|
|
|
|
PLATFORMS: list[Platform] = [
|
|
Platform.LOCK,
|
|
Platform.SENSOR,
|
|
Platform.BINARY_SENSOR,
|
|
Platform.SWITCH,
|
|
]
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
def setup(hass: HomeAssistant, config: ConfigEntry) -> bool:
|
|
"""Set up the TTLock component."""
|
|
|
|
Services(hass).register()
|
|
|
|
return True
|
|
|
|
|
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
"""Set up TTLock from a config entry."""
|
|
implementation = (
|
|
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
|
hass, entry
|
|
)
|
|
)
|
|
|
|
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
|
client = TTLockApi(aiohttp_client.async_get_clientsession(hass), session)
|
|
|
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {TT_API: client}
|
|
|
|
locks = [
|
|
LockUpdateCoordinator(hass, client, lock_id)
|
|
for lock_id in await client.get_locks()
|
|
]
|
|
await asyncio.gather(
|
|
*[coordinator.async_config_entry_first_refresh() for coordinator in locks]
|
|
)
|
|
hass.data[DOMAIN][entry.entry_id][TT_LOCKS] = locks
|
|
|
|
await WebhookHandler(hass, entry).setup()
|
|
|
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
|
|
|
return True
|
|
|
|
|
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
"""Unload a config entry."""
|
|
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
|
hass.data[DOMAIN].pop(entry.entry_id)
|
|
|
|
return unload_ok
|
|
|
|
|
|
class WebhookHandler:
|
|
"""Responsible for setting up/processing webhook data."""
|
|
|
|
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
|
"""Init the thing."""
|
|
self.hass = hass
|
|
self.entry = entry
|
|
|
|
async def setup(self) -> None:
|
|
"""Actually register the webhook."""
|
|
if self.hass.state == CoreState.running:
|
|
await self.register_webhook()
|
|
else:
|
|
self.hass.bus.async_listen_once(
|
|
EVENT_HOMEASSISTANT_STARTED, self.register_webhook
|
|
)
|
|
|
|
async def get_url(self) -> str:
|
|
"""Get the webhook url depending on the setup."""
|
|
if cloud.async_active_subscription(self.hass):
|
|
if CONF_WEBHOOK_URL not in self.entry.data:
|
|
try:
|
|
return await cloud.async_create_cloudhook(
|
|
self.hass, self.entry.data[CONF_WEBHOOK_ID]
|
|
)
|
|
except cloud.CloudNotConnected:
|
|
return webhook.async_generate_url(
|
|
self.hass, self.entry.data[CONF_WEBHOOK_ID]
|
|
)
|
|
else:
|
|
return self.entry.data[CONF_WEBHOOK_URL]
|
|
else:
|
|
return webhook.async_generate_url(
|
|
self.hass, self.entry.data[CONF_WEBHOOK_ID]
|
|
)
|
|
|
|
async def register_webhook(self, event: Event | None = None) -> None:
|
|
"""Set up a webhook to receive pushed data."""
|
|
if CONF_WEBHOOK_ID not in self.entry.data:
|
|
_LOGGER.info("Webhook not found in config entry, creating new one")
|
|
data = {**self.entry.data, CONF_WEBHOOK_ID: secrets.token_hex()}
|
|
self.hass.config_entries.async_update_entry(self.entry, data=data)
|
|
|
|
try:
|
|
webhook_url = await self.get_url()
|
|
data = {**self.entry.data, CONF_WEBHOOK_URL: webhook_url}
|
|
self.hass.config_entries.async_update_entry(self.entry, data=data)
|
|
except NoURLAvailableError:
|
|
_LOGGER.exception("Could not find base URL for installation")
|
|
ir.async_create_issue(
|
|
self.hass,
|
|
DOMAIN,
|
|
"no_webhook_url",
|
|
is_fixable=False,
|
|
severity=ir.IssueSeverity.ERROR,
|
|
translation_key="no_webhook_url",
|
|
)
|
|
return
|
|
else:
|
|
ir.async_delete_issue(self.hass, DOMAIN, "no_webhook_url")
|
|
|
|
if CONF_WEBHOOK_STATUS not in self.entry.data:
|
|
self.async_show_setup_message(webhook_url)
|
|
|
|
_LOGGER.info("Webhook registered at %s", webhook_url)
|
|
|
|
# Ensure the webhook is not registered already
|
|
webhook_unregister(self.hass, self.entry.data[CONF_WEBHOOK_ID])
|
|
|
|
webhook_register(
|
|
self.hass,
|
|
DOMAIN,
|
|
"TTLock",
|
|
self.entry.data[CONF_WEBHOOK_ID],
|
|
self.handle_webhook,
|
|
)
|
|
|
|
self.hass.bus.async_listen_once(
|
|
EVENT_HOMEASSISTANT_STOP, self.unregister_webhook
|
|
)
|
|
|
|
async def handle_webhook(
|
|
self, hass: HomeAssistant, webhook_id: str, request: Request
|
|
) -> None:
|
|
"""Handle webhook callback."""
|
|
|
|
success = False
|
|
try:
|
|
# {'lockId': ['7252408'], 'notifyType': ['1'], 'records': ['[{"lockId":7252408,"electricQuantity":93,"serverDate":1680810180029,"recordTypeFromLock":17,"recordType":7,"success":1,"lockMac":"16:72:4C:CC:01:C4","keyboardPwd":"<digits>","lockDate":1680810186000,"username":"Jonas"}]'], 'admin': ['jonas@lemon.nz'], 'lockMac': ['16:72:4C:CC:01:C4']}
|
|
if data := await request.post():
|
|
_LOGGER.debug("Got webhook data: %s", data)
|
|
for raw_records in data.getall("records", []):
|
|
for record in json.loads(raw_records):
|
|
async_dispatcher_send(
|
|
hass, SIGNAL_NEW_DATA, WebhookEvent.parse_obj(record)
|
|
)
|
|
success = True
|
|
else:
|
|
_LOGGER.debug("handle_webhook, empty payload: %s", await request.text())
|
|
except ValueError as ex:
|
|
_LOGGER.exception("Exception parsing webhook data: %s", ex)
|
|
return
|
|
|
|
if success and CONF_WEBHOOK_STATUS not in self.entry.data:
|
|
self.async_dismiss_setup_message()
|
|
|
|
async def unregister_webhook(self, event: Event | None = None) -> None:
|
|
"""Remove the webhook (before stop)."""
|
|
webhook_unregister(self.hass, self.entry.data[CONF_WEBHOOK_ID])
|
|
|
|
def async_show_setup_message(self, uri: str) -> None:
|
|
"""Display persistent notification with setup information."""
|
|
persistent_notification.async_create(
|
|
self.hass, f"Webhook url: {uri}", "TTLock Setup", self.entry.entry_id
|
|
)
|
|
|
|
def async_dismiss_setup_message(self) -> None:
|
|
"""Dismiss persistent notification."""
|
|
data = {**self.entry.data, CONF_WEBHOOK_STATUS: True}
|
|
self.hass.config_entries.async_update_entry(self.entry, data=data)
|
|
persistent_notification.async_dismiss(self.hass, self.entry.entry_id)
|