"""The Wyze Home Assistant Integration.""" from __future__ import annotations import logging from aiohttp.client_exceptions import ClientConnectorError from homeassistant.config_entries import ConfigEntry, ConfigEntryNotReady, SOURCE_IMPORT from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import device_registry as dr from homeassistant.helpers.check_config import HomeAssistantConfig from homeassistant.components import bluetooth from wyzeapy import Wyzeapy from wyzeapy.exceptions import AccessTokenError from wyzeapy.wyze_auth_lib import Token from .const import ( DOMAIN, CONF_CLIENT, ACCESS_TOKEN, REFRESH_TOKEN, REFRESH_TIME, WYZE_NOTIFICATION_TOGGLE, BULB_LOCAL_CONTROL, DEFAULT_LOCAL_CONTROL, KEY_ID, API_KEY, ) from .coordinator import WyzeLockBoltCoordinator from .token_manager import TokenManager PLATFORMS = [ "light", "switch", "lock", "climate", "alarm_control_panel", "sensor", "siren", "cover", "number", "button", "camera", ] # Fixme: Re add scene _LOGGER = logging.getLogger(__name__) # noinspection PyUnusedLocal async def async_setup( hass: HomeAssistant, config: HomeAssistantConfig, discovery_info=None ): # pylint: disable=unused-argument """Set up the WyzeApi domain.""" if hass.config_entries.async_entries(DOMAIN): _LOGGER.debug( "Nothing to import from configuration.yaml, loading from Integrations", ) return True # noinspection SpellCheckingInspection domainconfig = config.get(DOMAIN) # pylint: disable=logging-not-lazy _LOGGER.debug( "Importing config information for %s from configuration.yml" % domainconfig[CONF_USERNAME] ) if hass.config_entries.async_entries(DOMAIN): _LOGGER.debug("Found existing config entries") for entry in hass.config_entries.async_entries(DOMAIN): if entry: entry_data = entry.as_dict().get("data") hass.config_entries.async_update_entry( entry, data=entry_data, ) break else: _LOGGER.debug("Creating new config entry") hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data={ CONF_USERNAME: domainconfig[CONF_USERNAME], CONF_PASSWORD: domainconfig[CONF_PASSWORD], ACCESS_TOKEN: domainconfig[ACCESS_TOKEN], REFRESH_TOKEN: domainconfig[REFRESH_TOKEN], REFRESH_TIME: domainconfig[REFRESH_TIME], KEY_ID: domainconfig[KEY_ID], API_KEY: domainconfig[API_KEY], }, ) ) return True # noinspection DuplicatedCode async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Wyze Home Assistant Integration from a config entry.""" hass.data.setdefault(DOMAIN, {}) key_id = config_entry.data.get(KEY_ID) api_key = config_entry.data.get(API_KEY) client = await Wyzeapy.create() token = None if config_entry.data.get(ACCESS_TOKEN): token = Token( config_entry.data.get(ACCESS_TOKEN), config_entry.data.get(REFRESH_TOKEN), float(config_entry.data.get(REFRESH_TIME)), ) a_tkn_manager = TokenManager(hass, config_entry) client.register_for_token_callback(a_tkn_manager.token_callback) # We should probably try/catch here to invalidate the login credentials and throw a notification if we cannot get # a login with the token try: await client.login( config_entry.data.get(CONF_USERNAME), config_entry.data.get(CONF_PASSWORD), key_id, api_key, token, ) except ClientConnectorError as e: raise ConfigEntryNotReady("Unable to login due to network issues.") from e except AccessTokenError as e: _LOGGER.error( "Wyzeapi: Could not login. Please re-login through integration configuration" ) _LOGGER.error(e) raise ConfigEntryAuthFailed("Unable to login, please re-login.") from None hass.data[DOMAIN][config_entry.entry_id] = { CONF_CLIENT: client, "key_id": KEY_ID, "api_key": API_KEY, "coordinators": {}, } await setup_coordinators(hass, config_entry, client) options_dict = { BULB_LOCAL_CONTROL: config_entry.options.get( BULB_LOCAL_CONTROL, DEFAULT_LOCAL_CONTROL ) } hass.config_entries.async_update_entry(config_entry, options=options_dict) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) mac_addresses = await client.unique_device_ids mac_addresses.add(WYZE_NOTIFICATION_TOGGLE) hms_service = await client.hms_service hms_id = hms_service.hms_id if hms_id is not None: mac_addresses.add(hms_id) device_registry = dr.async_get(hass) for device in dr.async_entries_for_config_entry( device_registry, config_entry.entry_id ): for identifier in device.identifiers: # domain has to remain here. If it is removed the integration will remove all entities for not being in # the mac address list each boot. domain, mac = identifier if mac not in mac_addresses: _LOGGER.warning( "%s is not in the mac_addresses list, removing the entry", mac ) device_registry.async_remove_device(device.id) return True async def options_update_listener(hass: HomeAssistant, config_entry: ConfigEntry): """Handle options update.""" _LOGGER.debug("Updated options") entry_data = config_entry.as_dict().get("data") hass.config_entries.async_update_entry( config_entry, data=entry_data, ) _LOGGER.debug("Reload entry: " + config_entry.entry_id) await hass.config_entries.async_reload(config_entry.entry_id) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def setup_coordinators( hass: HomeAssistant, config_entry: ConfigEntry, client: Wyzeapy ): """Set up coordinators for Wyze devices that require Bluetooth.""" # Check if Bluetooth is active and functioning if bluetooth.async_scanner_count(hass, connectable=True) == 0: _LOGGER.info( "Bluetooth is not active or no scanners available. Skipping WyzeLockBoltCoordinator setup." ) return lock_service = await client.lock_service for lock in await lock_service.get_locks(): if lock.product_model == "YD_BT1": coordinators = hass.data[DOMAIN][config_entry.entry_id].setdefault( "coordinators", {} ) coordinators[lock.mac] = WyzeLockBoltCoordinator(hass, lock_service, lock) await coordinators[lock.mac].update_lock_info()