diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 89595fdebc4..c21bfbc1042 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -7,33 +7,37 @@ from collections.abc import Callable, Coroutine, Iterable, ValuesView from datetime import datetime from itertools import chain import logging -from typing import Any +from typing import Any, cast from aiohttp import ClientError, ClientResponseError +from path import Path from yalexs.activity import ActivityTypes from yalexs.const import DEFAULT_BRAND from yalexs.doorbell import Doorbell, DoorbellDetail from yalexs.exceptions import AugustApiAIOHTTPError from yalexs.lock import Lock, LockDetail +from yalexs.manager.activity import ActivityStream +from yalexs.manager.const import CONF_BRAND +from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation +from yalexs.manager.gateway import Config as YaleXSConfig +from yalexs.manager.subscriber import SubscriberMixin from yalexs.pubnub_activity import activities_from_pubnub_message from yalexs.pubnub_async import AugustPubNub, async_create_pubnub from yalexs_ble import YaleXSBLEDiscovery from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry -from homeassistant.const import CONF_PASSWORD -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.const import CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ( ConfigEntryAuthFailed, ConfigEntryNotReady, HomeAssistantError, ) from homeassistant.helpers import device_registry as dr, discovery_flow +from homeassistant.util.async_ import create_eager_task -from .activity import ActivityStream -from .const import CONF_BRAND, DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS -from .exceptions import CannotConnect, InvalidAuth, RequireValidation +from .const import DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS from .gateway import AugustGateway -from .subscriber import AugustSubscriberMixin from .util import async_create_august_clientsession _LOGGER = logging.getLogger(__name__) @@ -52,10 +56,8 @@ type AugustConfigEntry = ConfigEntry[AugustData] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up August from a config entry.""" session = async_create_august_clientsession(hass) - august_gateway = AugustGateway(hass, session) - + august_gateway = AugustGateway(Path(hass.config.config_dir), session) try: - await august_gateway.async_setup(entry.data) return await async_setup_august(hass, entry, august_gateway) except (RequireValidation, InvalidAuth) as err: raise ConfigEntryAuthFailed from err @@ -67,7 +69,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bool: """Unload a config entry.""" - entry.runtime_data.async_stop() return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) @@ -75,6 +76,8 @@ async def async_setup_august( hass: HomeAssistant, config_entry: AugustConfigEntry, august_gateway: AugustGateway ) -> bool: """Set up the August component.""" + config = cast(YaleXSConfig, config_entry.data) + await august_gateway.async_setup(config) if CONF_PASSWORD in config_entry.data: # We no longer need to store passwords since we do not @@ -116,7 +119,7 @@ def _async_trigger_ble_lock_discovery( ) -class AugustData(AugustSubscriberMixin): +class AugustData(SubscriberMixin): """August data object.""" def __init__( @@ -126,17 +129,17 @@ class AugustData(AugustSubscriberMixin): august_gateway: AugustGateway, ) -> None: """Init August data object.""" - super().__init__(hass, MIN_TIME_BETWEEN_DETAIL_UPDATES) + super().__init__(MIN_TIME_BETWEEN_DETAIL_UPDATES) self._config_entry = config_entry self._hass = hass self._august_gateway = august_gateway - self.activity_stream: ActivityStream = None # type: ignore[assignment] + self.activity_stream: ActivityStream = None self._api = august_gateway.api self._device_detail_by_id: dict[str, LockDetail | DoorbellDetail] = {} self._doorbells_by_id: dict[str, Doorbell] = {} self._locks_by_id: dict[str, Lock] = {} self._house_ids: set[str] = set() - self._pubnub_unsub: CALLBACK_TYPE | None = None + self._pubnub_unsub: Callable[[], Coroutine[Any, Any, None]] | None = None @property def brand(self) -> str: @@ -148,13 +151,8 @@ class AugustData(AugustSubscriberMixin): token = self._august_gateway.access_token # This used to be a gather but it was less reliable with august's recent api changes. user_data = await self._api.async_get_user(token) - locks: list[Lock] = await self._api.async_get_operable_locks(token) - doorbells: list[Doorbell] = await self._api.async_get_doorbells(token) - if not doorbells: - doorbells = [] - if not locks: - locks = [] - + locks: list[Lock] = await self._api.async_get_operable_locks(token) or [] + doorbells: list[Doorbell] = await self._api.async_get_doorbells(token) or [] self._doorbells_by_id = {device.device_id: device for device in doorbells} self._locks_by_id = {device.device_id: device for device in locks} self._house_ids = {device.house_id for device in chain(locks, doorbells)} @@ -175,9 +173,14 @@ class AugustData(AugustSubscriberMixin): pubnub.register_device(device) self.activity_stream = ActivityStream( - self._hass, self._api, self._august_gateway, self._house_ids, pubnub + self._api, self._august_gateway, self._house_ids, pubnub ) + self._config_entry.async_on_unload( + self._hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self.async_stop) + ) + self._config_entry.async_on_unload(self.async_stop) await self.activity_stream.async_setup() + pubnub.subscribe(self.async_pubnub_message) self._pubnub_unsub = async_create_pubnub( user_data["UserID"], @@ -200,8 +203,10 @@ class AugustData(AugustSubscriberMixin): # awake when they come back online for result in await asyncio.gather( *[ - self.async_status_async( - device_id, bool(detail.bridge and detail.bridge.hyper_bridge) + create_eager_task( + self.async_status_async( + device_id, bool(detail.bridge and detail.bridge.hyper_bridge) + ) ) for device_id, detail in self._device_detail_by_id.items() if device_id in self._locks_by_id @@ -231,11 +236,10 @@ class AugustData(AugustSubscriberMixin): self.async_signal_device_id_update(device.device_id) activity_stream.async_schedule_house_id_refresh(device.house_id) - @callback - def async_stop(self) -> None: + async def async_stop(self, event: Event | None = None) -> None: """Stop the subscriptions.""" if self._pubnub_unsub: - self._pubnub_unsub() + await self._pubnub_unsub() self.activity_stream.async_stop() @property diff --git a/homeassistant/components/august/activity.py b/homeassistant/components/august/activity.py deleted file mode 100644 index ee180ab5480..00000000000 --- a/homeassistant/components/august/activity.py +++ /dev/null @@ -1,231 +0,0 @@ -"""Consume the august activity stream.""" - -from __future__ import annotations - -from datetime import datetime -from functools import partial -import logging -from time import monotonic - -from aiohttp import ClientError -from yalexs.activity import Activity, ActivityType -from yalexs.api_async import ApiAsync -from yalexs.pubnub_async import AugustPubNub -from yalexs.util import get_latest_activity - -from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback -from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.event import async_call_later -from homeassistant.util.dt import utcnow - -from .const import ACTIVITY_UPDATE_INTERVAL -from .gateway import AugustGateway -from .subscriber import AugustSubscriberMixin - -_LOGGER = logging.getLogger(__name__) - -ACTIVITY_STREAM_FETCH_LIMIT = 10 -ACTIVITY_CATCH_UP_FETCH_LIMIT = 2500 - -INITIAL_LOCK_RESYNC_TIME = 60 - -# If there is a storm of activity (ie lock, unlock, door open, door close, etc) -# we want to debounce the updates so we don't hammer the activity api too much. -ACTIVITY_DEBOUNCE_COOLDOWN = 4 - - -@callback -def _async_cancel_future_scheduled_updates(cancels: list[CALLBACK_TYPE]) -> None: - """Cancel future scheduled updates.""" - for cancel in cancels: - cancel() - cancels.clear() - - -class ActivityStream(AugustSubscriberMixin): - """August activity stream handler.""" - - def __init__( - self, - hass: HomeAssistant, - api: ApiAsync, - august_gateway: AugustGateway, - house_ids: set[str], - pubnub: AugustPubNub, - ) -> None: - """Init August activity stream object.""" - super().__init__(hass, ACTIVITY_UPDATE_INTERVAL) - self._hass = hass - self._schedule_updates: dict[str, list[CALLBACK_TYPE]] = {} - self._august_gateway = august_gateway - self._api = api - self._house_ids = house_ids - self._latest_activities: dict[str, dict[ActivityType, Activity]] = {} - self._did_first_update = False - self.pubnub = pubnub - self._update_debounce: dict[str, Debouncer] = {} - self._update_debounce_jobs: dict[str, HassJob] = {} - self._start_time: float | None = None - - @callback - def _async_update_house_id_later(self, debouncer: Debouncer, _: datetime) -> None: - """Call a debouncer from async_call_later.""" - debouncer.async_schedule_call() - - async def async_setup(self) -> None: - """Token refresh check and catch up the activity stream.""" - self._start_time = monotonic() - update_debounce = self._update_debounce - update_debounce_jobs = self._update_debounce_jobs - for house_id in self._house_ids: - debouncer = Debouncer( - self._hass, - _LOGGER, - cooldown=ACTIVITY_DEBOUNCE_COOLDOWN, - immediate=True, - function=partial(self._async_update_house_id, house_id), - background=True, - ) - update_debounce[house_id] = debouncer - update_debounce_jobs[house_id] = HassJob( - partial(self._async_update_house_id_later, debouncer), - f"debounced august activity update for {house_id}", - cancel_on_shutdown=True, - ) - - await self._async_refresh(utcnow()) - self._did_first_update = True - - @callback - def async_stop(self) -> None: - """Cleanup any debounces.""" - for debouncer in self._update_debounce.values(): - debouncer.async_cancel() - for cancels in self._schedule_updates.values(): - _async_cancel_future_scheduled_updates(cancels) - - def get_latest_device_activity( - self, device_id: str, activity_types: set[ActivityType] - ) -> Activity | None: - """Return latest activity that is one of the activity_types.""" - if not (latest_device_activities := self._latest_activities.get(device_id)): - return None - - latest_activity: Activity | None = None - - for activity_type in activity_types: - if activity := latest_device_activities.get(activity_type): - if ( - latest_activity - and activity.activity_start_time - <= latest_activity.activity_start_time - ): - continue - latest_activity = activity - - return latest_activity - - async def _async_refresh(self, time: datetime) -> None: - """Update the activity stream from August.""" - # This is the only place we refresh the api token - await self._august_gateway.async_refresh_access_token_if_needed() - if self.pubnub.connected: - _LOGGER.debug("Skipping update because pubnub is connected") - return - _LOGGER.debug("Start retrieving device activities") - # Await in sequence to avoid hammering the API - for debouncer in self._update_debounce.values(): - await debouncer.async_call() - - @callback - def async_schedule_house_id_refresh(self, house_id: str) -> None: - """Update for a house activities now and once in the future.""" - if future_updates := self._schedule_updates.setdefault(house_id, []): - _async_cancel_future_scheduled_updates(future_updates) - - debouncer = self._update_debounce[house_id] - debouncer.async_schedule_call() - - # Schedule two updates past the debounce time - # to ensure we catch the case where the activity - # api does not update right away and we need to poll - # it again. Sometimes the lock operator or a doorbell - # will not show up in the activity stream right away. - # Only do additional polls if we are past - # the initial lock resync time to avoid a storm - # of activity at setup. - if ( - not self._start_time - or monotonic() - self._start_time < INITIAL_LOCK_RESYNC_TIME - ): - _LOGGER.debug( - "Skipping additional updates due to ongoing initial lock resync time" - ) - return - - _LOGGER.debug("Scheduling additional updates for house id %s", house_id) - job = self._update_debounce_jobs[house_id] - for step in (1, 2): - future_updates.append( - async_call_later( - self._hass, - (step * ACTIVITY_DEBOUNCE_COOLDOWN) + 0.1, - job, - ) - ) - - async def _async_update_house_id(self, house_id: str) -> None: - """Update device activities for a house.""" - if self._did_first_update: - limit = ACTIVITY_STREAM_FETCH_LIMIT - else: - limit = ACTIVITY_CATCH_UP_FETCH_LIMIT - - _LOGGER.debug("Updating device activity for house id %s", house_id) - try: - activities = await self._api.async_get_house_activities( - self._august_gateway.access_token, house_id, limit=limit - ) - except ClientError as ex: - _LOGGER.error( - "Request error trying to retrieve activity for house id %s: %s", - house_id, - ex, - ) - # Make sure we process the next house if one of them fails - return - - _LOGGER.debug( - "Completed retrieving device activities for house id %s", house_id - ) - for device_id in self.async_process_newer_device_activities(activities): - _LOGGER.debug( - "async_signal_device_id_update (from activity stream): %s", - device_id, - ) - self.async_signal_device_id_update(device_id) - - def async_process_newer_device_activities( - self, activities: list[Activity] - ) -> set[str]: - """Process activities if they are newer than the last one.""" - updated_device_ids = set() - latest_activities = self._latest_activities - for activity in activities: - device_id = activity.device_id - activity_type = activity.activity_type - device_activities = latest_activities.setdefault(device_id, {}) - # Ignore activities that are older than the latest one unless it is a non - # locking or unlocking activity with the exact same start time. - last_activity = device_activities.get(activity_type) - # The activity stream can have duplicate activities. So we need - # to call get_latest_activity to figure out if if the activity - # is actually newer than the last one. - latest_activity = get_latest_activity(activity, last_activity) - if latest_activity != activity: - continue - - device_activities[activity_type] = activity - updated_device_ids.add(device_id) - - return updated_device_ids diff --git a/homeassistant/components/august/config_flow.py b/homeassistant/components/august/config_flow.py index 08401e15b84..75543311fdd 100644 --- a/homeassistant/components/august/config_flow.py +++ b/homeassistant/components/august/config_flow.py @@ -3,12 +3,14 @@ from collections.abc import Mapping from dataclasses import dataclass import logging +from pathlib import Path from typing import Any import aiohttp import voluptuous as vol from yalexs.authenticator import ValidationResult from yalexs.const import BRANDS, DEFAULT_BRAND +from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -23,7 +25,6 @@ from .const import ( LOGIN_METHODS, VERIFICATION_CODE_KEY, ) -from .exceptions import CannotConnect, InvalidAuth, RequireValidation from .gateway import AugustGateway from .util import async_create_august_clientsession @@ -164,7 +165,9 @@ class AugustConfigFlow(ConfigFlow, domain=DOMAIN): if self._august_gateway is not None: return self._august_gateway self._aiohttp_session = async_create_august_clientsession(self.hass) - self._august_gateway = AugustGateway(self.hass, self._aiohttp_session) + self._august_gateway = AugustGateway( + Path(self.hass.config.config_dir), self._aiohttp_session + ) return self._august_gateway @callback diff --git a/homeassistant/components/august/exceptions.py b/homeassistant/components/august/exceptions.py deleted file mode 100644 index edd418c9519..00000000000 --- a/homeassistant/components/august/exceptions.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Shared exceptions for the august integration.""" - -from homeassistant import exceptions - - -class RequireValidation(exceptions.HomeAssistantError): - """Error to indicate we require validation (2fa).""" - - -class CannotConnect(exceptions.HomeAssistantError): - """Error to indicate we cannot connect.""" - - -class InvalidAuth(exceptions.HomeAssistantError): - """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/august/gateway.py b/homeassistant/components/august/gateway.py index 63bc085b811..2c6ad739bdc 100644 --- a/homeassistant/components/august/gateway.py +++ b/homeassistant/components/august/gateway.py @@ -1,56 +1,23 @@ """Handle August connection setup and authentication.""" -import asyncio -from collections.abc import Mapping -from http import HTTPStatus -import logging -import os from typing import Any -from aiohttp import ClientError, ClientResponseError, ClientSession -from yalexs.api_async import ApiAsync -from yalexs.authenticator_async import AuthenticationState, AuthenticatorAsync -from yalexs.authenticator_common import Authentication from yalexs.const import DEFAULT_BRAND -from yalexs.exceptions import AugustApiAIOHTTPError +from yalexs.manager.gateway import Gateway -from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import CONF_USERNAME from .const import ( CONF_ACCESS_TOKEN_CACHE_FILE, CONF_BRAND, CONF_INSTALL_ID, CONF_LOGIN_METHOD, - DEFAULT_AUGUST_CONFIG_FILE, - DEFAULT_TIMEOUT, - VERIFICATION_CODE_KEY, ) -from .exceptions import CannotConnect, InvalidAuth, RequireValidation - -_LOGGER = logging.getLogger(__name__) -class AugustGateway: +class AugustGateway(Gateway): """Handle the connection to August.""" - api: ApiAsync - authenticator: AuthenticatorAsync - authentication: Authentication - _access_token_cache_file: str - - def __init__(self, hass: HomeAssistant, aiohttp_session: ClientSession) -> None: - """Init the connection.""" - self._aiohttp_session = aiohttp_session - self._token_refresh_lock = asyncio.Lock() - self._hass: HomeAssistant = hass - self._config: Mapping[str, Any] | None = None - - @property - def access_token(self) -> str: - """Access token for the api.""" - return self.authentication.access_token - def config_entry(self) -> dict[str, Any]: """Config entry.""" assert self._config is not None @@ -61,101 +28,3 @@ class AugustGateway: CONF_INSTALL_ID: self._config.get(CONF_INSTALL_ID), CONF_ACCESS_TOKEN_CACHE_FILE: self._access_token_cache_file, } - - @callback - def async_configure_access_token_cache_file( - self, username: str, access_token_cache_file: str | None - ) -> str: - """Configure the access token cache file.""" - file = access_token_cache_file or f".{username}{DEFAULT_AUGUST_CONFIG_FILE}" - self._access_token_cache_file = file - return self._hass.config.path(file) - - async def async_setup(self, conf: Mapping[str, Any]) -> None: - """Create the api and authenticator objects.""" - if conf.get(VERIFICATION_CODE_KEY): - return - - access_token_cache_file_path = self.async_configure_access_token_cache_file( - conf[CONF_USERNAME], conf.get(CONF_ACCESS_TOKEN_CACHE_FILE) - ) - self._config = conf - - self.api = ApiAsync( - self._aiohttp_session, - timeout=self._config.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), - brand=self._config.get(CONF_BRAND, DEFAULT_BRAND), - ) - - self.authenticator = AuthenticatorAsync( - self.api, - self._config[CONF_LOGIN_METHOD], - self._config[CONF_USERNAME], - self._config.get(CONF_PASSWORD, ""), - install_id=self._config.get(CONF_INSTALL_ID), - access_token_cache_file=access_token_cache_file_path, - ) - - await self.authenticator.async_setup_authentication() - - async def async_authenticate(self) -> Authentication: - """Authenticate with the details provided to setup.""" - try: - self.authentication = await self.authenticator.async_authenticate() - if self.authentication.state == AuthenticationState.AUTHENTICATED: - # Call the locks api to verify we are actually - # authenticated because we can be authenticated - # by have no access - await self.api.async_get_operable_locks(self.access_token) - except AugustApiAIOHTTPError as ex: - if ex.auth_failed: - raise InvalidAuth from ex - raise CannotConnect from ex - except ClientResponseError as ex: - if ex.status == HTTPStatus.UNAUTHORIZED: - raise InvalidAuth from ex - - raise CannotConnect from ex - except ClientError as ex: - _LOGGER.error("Unable to connect to August service: %s", str(ex)) - raise CannotConnect from ex - - if self.authentication.state == AuthenticationState.BAD_PASSWORD: - raise InvalidAuth - - if self.authentication.state == AuthenticationState.REQUIRES_VALIDATION: - raise RequireValidation - - if self.authentication.state != AuthenticationState.AUTHENTICATED: - _LOGGER.error("Unknown authentication state: %s", self.authentication.state) - raise InvalidAuth - - return self.authentication - - async def async_reset_authentication(self) -> None: - """Remove the cache file.""" - await self._hass.async_add_executor_job(self._reset_authentication) - - def _reset_authentication(self) -> None: - """Remove the cache file.""" - path = self._hass.config.path(self._access_token_cache_file) - if os.path.exists(path): - os.unlink(path) - - async def async_refresh_access_token_if_needed(self) -> None: - """Refresh the august access token if needed.""" - if not self.authenticator.should_refresh(): - return - async with self._token_refresh_lock: - refreshed_authentication = ( - await self.authenticator.async_refresh_access_token(force=False) - ) - _LOGGER.info( - ( - "Refreshed august access token. The old token expired at %s, and" - " the new token expires at %s" - ), - self.authentication.access_token_expires, - refreshed_authentication.access_token_expires, - ) - self.authentication = refreshed_authentication diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index f85e75664eb..179e85de7f0 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==3.1.0", "yalexs-ble==2.4.2"] + "requirements": ["yalexs==5.2.0", "yalexs-ble==2.4.2"] } diff --git a/homeassistant/components/august/subscriber.py b/homeassistant/components/august/subscriber.py deleted file mode 100644 index bec8e2f0b97..00000000000 --- a/homeassistant/components/august/subscriber.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Base class for August entity.""" - -from __future__ import annotations - -from abc import abstractmethod -from datetime import datetime, timedelta - -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback -from homeassistant.helpers.event import async_track_time_interval - - -class AugustSubscriberMixin: - """Base implementation for a subscriber.""" - - def __init__(self, hass: HomeAssistant, update_interval: timedelta) -> None: - """Initialize an subscriber.""" - super().__init__() - self._hass = hass - self._update_interval = update_interval - self._subscriptions: dict[str, list[CALLBACK_TYPE]] = {} - self._unsub_interval: CALLBACK_TYPE | None = None - self._stop_interval: CALLBACK_TYPE | None = None - - @callback - def async_subscribe_device_id( - self, device_id: str, update_callback: CALLBACK_TYPE - ) -> CALLBACK_TYPE: - """Add an callback subscriber. - - Returns a callable that can be used to unsubscribe. - """ - if not self._subscriptions: - self._async_setup_listeners() - - self._subscriptions.setdefault(device_id, []).append(update_callback) - - def _unsubscribe() -> None: - self.async_unsubscribe_device_id(device_id, update_callback) - - return _unsubscribe - - @abstractmethod - async def _async_refresh(self, time: datetime) -> None: - """Refresh data.""" - - @callback - def _async_scheduled_refresh(self, now: datetime) -> None: - """Call the refresh method.""" - self._hass.async_create_background_task( - self._async_refresh(now), name=f"{self} schedule refresh", eager_start=True - ) - - @callback - def _async_cancel_update_interval(self, _: Event | None = None) -> None: - """Cancel the scheduled update.""" - if self._unsub_interval: - self._unsub_interval() - self._unsub_interval = None - - @callback - def _async_setup_listeners(self) -> None: - """Create interval and stop listeners.""" - self._async_cancel_update_interval() - self._unsub_interval = async_track_time_interval( - self._hass, - self._async_scheduled_refresh, - self._update_interval, - name="august refresh", - ) - - if not self._stop_interval: - self._stop_interval = self._hass.bus.async_listen( - EVENT_HOMEASSISTANT_STOP, - self._async_cancel_update_interval, - ) - - @callback - def async_unsubscribe_device_id( - self, device_id: str, update_callback: CALLBACK_TYPE - ) -> None: - """Remove a callback subscriber.""" - self._subscriptions[device_id].remove(update_callback) - if not self._subscriptions[device_id]: - del self._subscriptions[device_id] - - if self._subscriptions: - return - self._async_cancel_update_interval() - - @callback - def async_signal_device_id_update(self, device_id: str) -> None: - """Call the callbacks for a device_id.""" - if not self._subscriptions.get(device_id): - return - - for update_callback in self._subscriptions[device_id]: - update_callback() diff --git a/requirements_all.txt b/requirements_all.txt index fc80c0d87bf..7acc4348952 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2933,7 +2933,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.2 # homeassistant.components.august -yalexs==3.1.0 +yalexs==5.2.0 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df36cf3c3cf..6c8d765f9e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2289,7 +2289,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.2 # homeassistant.components.august -yalexs==3.1.0 +yalexs==5.2.0 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index e0bc67f510f..b8d394fa067 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -58,8 +58,8 @@ def _mock_authenticator(auth_state): return authenticator -@patch("homeassistant.components.august.gateway.ApiAsync") -@patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate") +@patch("yalexs.manager.gateway.ApiAsync") +@patch("yalexs.manager.gateway.AuthenticatorAsync.async_authenticate") async def _mock_setup_august( hass, api_instance, pubnub_mock, authenticate_mock, api_mock, brand ): @@ -77,7 +77,10 @@ async def _mock_setup_august( ) entry.add_to_hass(hass) with ( - patch("homeassistant.components.august.async_create_pubnub"), + patch( + "homeassistant.components.august.async_create_pubnub", + return_value=AsyncMock(), + ), patch("homeassistant.components.august.AugustPubNub", return_value=pubnub_mock), ): assert await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/august/test_config_flow.py b/tests/components/august/test_config_flow.py index e1e6f622c2e..aec08864c65 100644 --- a/tests/components/august/test_config_flow.py +++ b/tests/components/august/test_config_flow.py @@ -3,6 +3,7 @@ from unittest.mock import patch from yalexs.authenticator import ValidationResult +from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation from homeassistant import config_entries from homeassistant.components.august.const import ( @@ -13,11 +14,6 @@ from homeassistant.components.august.const import ( DOMAIN, VERIFICATION_CODE_KEY, ) -from homeassistant.components.august.exceptions import ( - CannotConnect, - InvalidAuth, - RequireValidation, -) from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -151,7 +147,7 @@ async def test_form_needs_validate(hass: HomeAssistant) -> None: side_effect=RequireValidation, ), patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", + "yalexs.manager.gateway.AuthenticatorAsync.async_send_verification_code", return_value=True, ) as mock_send_verification_code, ): @@ -176,11 +172,11 @@ async def test_form_needs_validate(hass: HomeAssistant) -> None: side_effect=RequireValidation, ), patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_validate_verification_code", + "yalexs.manager.gateway.AuthenticatorAsync.async_validate_verification_code", return_value=ValidationResult.INVALID_VERIFICATION_CODE, ) as mock_validate_verification_code, patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", + "yalexs.manager.gateway.AuthenticatorAsync.async_send_verification_code", return_value=True, ) as mock_send_verification_code, ): @@ -204,11 +200,11 @@ async def test_form_needs_validate(hass: HomeAssistant) -> None: return_value=True, ), patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_validate_verification_code", + "yalexs.manager.gateway.AuthenticatorAsync.async_validate_verification_code", return_value=ValidationResult.VALIDATED, ) as mock_validate_verification_code, patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", + "yalexs.manager.gateway.AuthenticatorAsync.async_send_verification_code", return_value=True, ) as mock_send_verification_code, patch( @@ -310,7 +306,7 @@ async def test_form_reauth_with_2fa(hass: HomeAssistant) -> None: side_effect=RequireValidation, ), patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", + "yalexs.manager.gateway.AuthenticatorAsync.async_send_verification_code", return_value=True, ) as mock_send_verification_code, ): @@ -334,11 +330,11 @@ async def test_form_reauth_with_2fa(hass: HomeAssistant) -> None: return_value=True, ), patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_validate_verification_code", + "yalexs.manager.gateway.AuthenticatorAsync.async_validate_verification_code", return_value=ValidationResult.VALIDATED, ) as mock_validate_verification_code, patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", + "yalexs.manager.gateway.AuthenticatorAsync.async_send_verification_code", return_value=True, ) as mock_send_verification_code, patch( diff --git a/tests/components/august/test_gateway.py b/tests/components/august/test_gateway.py index 535e547d915..e605fd74f0a 100644 --- a/tests/components/august/test_gateway.py +++ b/tests/components/august/test_gateway.py @@ -1,5 +1,6 @@ """The gateway tests for the august platform.""" +from pathlib import Path from unittest.mock import MagicMock, patch from yalexs.authenticator_common import AuthenticationState @@ -16,12 +17,10 @@ async def test_refresh_access_token(hass: HomeAssistant) -> None: await _patched_refresh_access_token(hass, "new_token", 5678) -@patch("homeassistant.components.august.gateway.ApiAsync.async_get_operable_locks") -@patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate") -@patch("homeassistant.components.august.gateway.AuthenticatorAsync.should_refresh") -@patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_refresh_access_token" -) +@patch("yalexs.manager.gateway.ApiAsync.async_get_operable_locks") +@patch("yalexs.manager.gateway.AuthenticatorAsync.async_authenticate") +@patch("yalexs.manager.gateway.AuthenticatorAsync.should_refresh") +@patch("yalexs.manager.gateway.AuthenticatorAsync.async_refresh_access_token") async def _patched_refresh_access_token( hass, new_token, @@ -36,7 +35,7 @@ async def _patched_refresh_access_token( "original_token", 1234, AuthenticationState.AUTHENTICATED ) ) - august_gateway = AugustGateway(hass, MagicMock()) + august_gateway = AugustGateway(Path(hass.config.config_dir), MagicMock()) mocked_config = _mock_get_config() await august_gateway.async_setup(mocked_config[DOMAIN]) await august_gateway.async_authenticate() diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index a79ee7ffbf1..8bb71826d24 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -6,9 +6,9 @@ from unittest.mock import Mock from aiohttp import ClientResponseError from freezegun.api import FrozenDateTimeFactory import pytest +from yalexs.manager.activity import INITIAL_LOCK_RESYNC_TIME from yalexs.pubnub_async import AugustPubNub -from homeassistant.components.august.activity import INITIAL_LOCK_RESYNC_TIME from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, STATE_JAMMED,