Migrate august to use yalexs 5.2.0 (#119178)

This commit is contained in:
J. Nick Koston 2024-06-09 05:30:41 -05:00 committed by GitHub
parent ff493a8a9d
commit d9f1d40805
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 65 additions and 535 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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."""

View file

@ -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

View file

@ -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"]
}

View file

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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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(

View file

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

View file

@ -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,