Files
Home-Assistant/custom_components/wyzeapi/__init__.py
T
2026-06-12 09:59:05 -04:00

216 lines
7.2 KiB
Python

"""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()