#!/usr/bin/python3 """Platform for light integration.""" from abc import ABC from datetime import timedelta import logging from typing import Any, Callable, List from aiohttp.client_exceptions import ClientConnectionError from wyzeapy import LockService, Wyzeapy from wyzeapy.services.lock_service import Lock from wyzeapy.types import DeviceTypes from wyzeapy.exceptions import AccessTokenError, ParameterError, UnknownApiError import homeassistant.components.lock from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers import device_registry as dr from homeassistant.exceptions import HomeAssistantError from .const import CONF_CLIENT, DOMAIN, LOCK_UPDATED from .token_manager import token_exception_handler _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Data provided by Wyze" SCAN_INTERVAL = timedelta(seconds=10) MAX_OUT_OF_SYNC_COUNT = 5 @token_exception_handler async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable[[List[Any], bool], None], ) -> None: """ This function sets up the config_entry :param hass: Home Assistant instance :param config_entry: The current config_entry :param async_add_entities: This function adds entities to the config_entry :return: """ _LOGGER.debug("""Creating new WyzeApi lock component""") client: Wyzeapy = hass.data[DOMAIN][config_entry.entry_id][CONF_CLIENT] lock_service = await client.lock_service all_locks = await lock_service.get_locks() locks = [ WyzeLock(lock_service, lock) for lock in all_locks if lock.product_model != "YD_BT1" ] lock_bolts = [] coordinators = hass.data[DOMAIN][config_entry.entry_id].get("coordinators", {}) for lock in all_locks: if lock.product_model == "YD_BT1": coordinator = coordinators.get(lock.mac) if coordinator is None: _LOGGER.warning( "No coordinator found for Lock Bolt %s (%s), skipping", lock.nickname, lock.mac, ) continue lock_bolts.append(WyzeLockBolt(coordinator)) async_add_entities(locks + lock_bolts, False) class WyzeLock(homeassistant.components.lock.LockEntity, ABC): """Representation of a Wyze Lock.""" def __init__(self, lock_service: LockService, lock: Lock): """Initialize a Wyze lock.""" self._lock = lock if self._lock.type not in [DeviceTypes.LOCK]: raise AttributeError("Device type not supported") self._lock_service = lock_service self._out_of_sync_count = 0 @property def device_info(self): return { "identifiers": {(DOMAIN, self._lock.mac)}, "name": self._lock.nickname, "connections": { ( dr.CONNECTION_NETWORK_MAC, self._lock.mac, ) }, "manufacturer": "WyzeLabs", "model": self._lock.product_model, } def lock(self, **kwargs): raise NotImplementedError def unlock(self, **kwargs): raise NotImplementedError @property def should_poll(self) -> bool: return False @token_exception_handler async def async_lock(self, **kwargs): _LOGGER.debug("Turning on lock") try: await self._lock_service.lock(self._lock) except (AccessTokenError, ParameterError, UnknownApiError) as err: raise HomeAssistantError(f"Wyze returned an error: {err.args}") from err except ClientConnectionError as err: raise HomeAssistantError(err) from err else: self._lock.unlocked = False self.async_schedule_update_ha_state() @token_exception_handler async def async_unlock(self, **kwargs): try: await self._lock_service.unlock(self._lock) except (AccessTokenError, ParameterError, UnknownApiError) as err: raise HomeAssistantError(f"Wyze returned an error: {err.args}") from err except ClientConnectionError as err: raise HomeAssistantError(err) from err else: self._lock.unlocked = True self.async_schedule_update_ha_state() @property def is_locked(self): return not self._lock.unlocked @property def name(self): """Return the display name of this lock.""" return self._lock.nickname @property def unique_id(self): return self._lock.mac @property def available(self): """Return the connection status of this lock""" return self._lock.available @property def extra_state_attributes(self): """Return device attributes of the entity.""" dev_info = { ATTR_ATTRIBUTION: ATTRIBUTION, "door_open": self._lock.door_open, } # Add the lock battery value if it exists if self._lock.raw_dict.get("power"): dev_info["lock_battery"] = str(self._lock.raw_dict.get("power")) # Add the keypad's battery value if it exists if self._lock.raw_dict.get("keypad", {}).get("power"): dev_info["keypad_battery"] = str( self._lock.raw_dict.get("keypad", {}).get("power") ) return dev_info @property def supported_features(self): return None @token_exception_handler async def async_update(self): """ This function updates the entity """ lock = await self._lock_service.update(self._lock) if ( lock.unlocked == self._lock.unlocked or self._out_of_sync_count >= MAX_OUT_OF_SYNC_COUNT ): self._lock = lock self._out_of_sync_count = 0 else: self._out_of_sync_count += 1 @callback def async_update_callback(self, lock: Lock): """Update the switch's state.""" self._lock = lock async_dispatcher_send( self.hass, f"{LOCK_UPDATED}-{self._lock.mac}", lock, ) self.async_schedule_update_ha_state() async def async_added_to_hass(self) -> None: """Subscribe to update events.""" self._lock.callback_function = self.async_update_callback self._lock_service.register_updater(self._lock, 10) await self._lock_service.start_update_manager() return await super().async_added_to_hass() async def async_will_remove_from_hass(self) -> None: self._lock_service.unregister_updater(self._lock) class WyzeLockBolt(CoordinatorEntity, homeassistant.components.lock.LockEntity): def __init__(self, coordinator): super().__init__(coordinator) self._lock = coordinator._lock @property def name(self): """Return the display name of this lock.""" return self._lock.nickname @property def unique_id(self): return self._lock.mac @property def device_info(self): return { "identifiers": {(DOMAIN, self._lock.mac)}, "name": self._lock.nickname, "connections": { ( dr.CONNECTION_NETWORK_MAC, self.coordinator._mac, ), ("uuid", self.coordinator._uuid), ("serial_number", self._lock.raw_dict["hardware_info"]["sn"]), }, "manufacturer": "WyzeLabs", "model": self._lock.product_model, } @property def is_locked(self): if self.coordinator.data is None or self.coordinator.data.get("state") is None: return None return self.coordinator.data["state"] == 1 async def async_lock(self, **kwargs): return await self.coordinator.lock_unlock(command="lock") async def async_unlock(self, **kwargs): return await self.coordinator.lock_unlock(command="unlock") @property def is_locking(self, **kwargs): return self.coordinator._current_command == "lock" @property def is_unlocking(self, **kwargs): return self.coordinator._current_command == "unlock" @property def state_attributes(self): if self.coordinator.data is None: return {} return {"last_operated": self.coordinator.data["timestamp"]}