"""Provides the coordinator for a LOQED lock.""" import asyncio import logging from typing import TypedDict from aiohttp.web import Request from loqedAPI import loqed from homeassistant.components import cloud, webhook from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import CONF_CLOUDHOOK_URL, DOMAIN _LOGGER = logging.getLogger(__name__) class BatteryMessage(TypedDict): """Properties in a battery update message.""" mac_wifi: str mac_ble: str battery_type: str battery_percentage: int class StateReachedMessage(TypedDict): """Properties in a battery update message.""" requested_state: str requested_state_numeric: int event_type: str key_local_id: int mac_wifi: str mac_ble: str class TransitionMessage(TypedDict): """Properties in a battery update message.""" go_to_state: str go_to_state_numeric: int event_type: str key_local_id: int mac_wifi: str mac_ble: str class StatusMessage(TypedDict): """Properties returned by the status endpoint of the bridhge.""" battery_percentage: int battery_type: str battery_type_numeric: int battery_voltage: float bolt_state: str bolt_state_numeric: int bridge_mac_wifi: str bridge_mac_ble: str lock_online: int webhooks_number: int ip_address: str up_timestamp: int wifi_strength: int ble_strength: int class LoqedDataCoordinator(DataUpdateCoordinator[StatusMessage]): """Data update coordinator for the loqed platform.""" def __init__( self, hass: HomeAssistant, api: loqed.LoqedAPI, lock: loqed.Lock, entry: ConfigEntry, ) -> None: """Initialize the Loqed Data Update coordinator.""" super().__init__(hass, _LOGGER, name="Loqed sensors") self._api = api self._entry = entry self.lock = lock self.device_name = self._entry.data[CONF_NAME] async def _async_update_data(self) -> StatusMessage: """Fetch data from API endpoint.""" async with asyncio.timeout(10): return await self._api.async_get_lock_details() async def _handle_webhook( self, hass: HomeAssistant, webhook_id: str, request: Request ) -> None: """Handle incoming Loqed messages.""" _LOGGER.debug("Callback received: %s", request.headers) received_ts = request.headers["TIMESTAMP"] received_hash = request.headers["HASH"] body = await request.text() _LOGGER.debug("Callback body: %s", body) event_data = await self.lock.receiveWebhook(body, received_hash, received_ts) if "error" in event_data: _LOGGER.warning("Incorrect callback received:: %s", event_data) return self.async_update_listeners() async def ensure_webhooks(self) -> None: """Register webhook on LOQED bridge.""" webhook_id = self._entry.data[CONF_WEBHOOK_ID] webhook.async_register( self.hass, DOMAIN, "Loqed", webhook_id, self._handle_webhook ) if cloud.async_active_subscription(self.hass): webhook_url = await async_cloudhook_generate_url(self.hass, self._entry) else: webhook_url = webhook.async_generate_url( self.hass, self._entry.data[CONF_WEBHOOK_ID] ) _LOGGER.debug("Webhook URL: %s", webhook_url) webhooks = await self.lock.getWebhooks() webhook_index = next( (x["id"] for x in webhooks if x["url"] == webhook_url), None ) if not webhook_index: await self.lock.registerWebhook(webhook_url) webhooks = await self.lock.getWebhooks() webhook_index = next(x["id"] for x in webhooks if x["url"] == webhook_url) _LOGGER.debug("Webhook got index %s", webhook_index) async def remove_webhooks(self) -> None: """Remove webhook from LOQED bridge.""" webhook_id = self._entry.data[CONF_WEBHOOK_ID] if CONF_CLOUDHOOK_URL in self._entry.data: webhook_url = self._entry.data[CONF_CLOUDHOOK_URL] else: webhook_url = webhook.async_generate_url(self.hass, webhook_id) webhook.async_unregister( self.hass, webhook_id, ) _LOGGER.debug("Webhook URL: %s", webhook_url) webhooks = await self.lock.getWebhooks() webhook_index = next( (x["id"] for x in webhooks if x["url"] == webhook_url), None ) if webhook_index: await self.lock.deleteWebhook(webhook_index) async def async_cloudhook_generate_url(hass: HomeAssistant, entry: ConfigEntry) -> str: """Generate the full URL for a webhook_id.""" if CONF_CLOUDHOOK_URL not in entry.data: webhook_url = await cloud.async_create_cloudhook( hass, entry.data[CONF_WEBHOOK_ID] ) data = {**entry.data, CONF_CLOUDHOOK_URL: webhook_url} hass.config_entries.async_update_entry(entry, data=data) return webhook_url return str(entry.data[CONF_CLOUDHOOK_URL])