Migrate august to use yalexs 5.2.0 (#119178)
This commit is contained in:
parent
ff493a8a9d
commit
d9f1d40805
13 changed files with 65 additions and 535 deletions
|
@ -7,33 +7,37 @@ from collections.abc import Callable, Coroutine, Iterable, ValuesView
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any, cast
|
||||||
|
|
||||||
from aiohttp import ClientError, ClientResponseError
|
from aiohttp import ClientError, ClientResponseError
|
||||||
|
from path import Path
|
||||||
from yalexs.activity import ActivityTypes
|
from yalexs.activity import ActivityTypes
|
||||||
from yalexs.const import DEFAULT_BRAND
|
from yalexs.const import DEFAULT_BRAND
|
||||||
from yalexs.doorbell import Doorbell, DoorbellDetail
|
from yalexs.doorbell import Doorbell, DoorbellDetail
|
||||||
from yalexs.exceptions import AugustApiAIOHTTPError
|
from yalexs.exceptions import AugustApiAIOHTTPError
|
||||||
from yalexs.lock import Lock, LockDetail
|
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_activity import activities_from_pubnub_message
|
||||||
from yalexs.pubnub_async import AugustPubNub, async_create_pubnub
|
from yalexs.pubnub_async import AugustPubNub, async_create_pubnub
|
||||||
from yalexs_ble import YaleXSBLEDiscovery
|
from yalexs_ble import YaleXSBLEDiscovery
|
||||||
|
|
||||||
from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry
|
from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry
|
||||||
from homeassistant.const import CONF_PASSWORD
|
from homeassistant.const import CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
from homeassistant.core import Event, HomeAssistant, callback
|
||||||
from homeassistant.exceptions import (
|
from homeassistant.exceptions import (
|
||||||
ConfigEntryAuthFailed,
|
ConfigEntryAuthFailed,
|
||||||
ConfigEntryNotReady,
|
ConfigEntryNotReady,
|
||||||
HomeAssistantError,
|
HomeAssistantError,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers import device_registry as dr, discovery_flow
|
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 DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS
|
||||||
from .const import CONF_BRAND, DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS
|
|
||||||
from .exceptions import CannotConnect, InvalidAuth, RequireValidation
|
|
||||||
from .gateway import AugustGateway
|
from .gateway import AugustGateway
|
||||||
from .subscriber import AugustSubscriberMixin
|
|
||||||
from .util import async_create_august_clientsession
|
from .util import async_create_august_clientsession
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -52,10 +56,8 @@ type AugustConfigEntry = ConfigEntry[AugustData]
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up August from a config entry."""
|
"""Set up August from a config entry."""
|
||||||
session = async_create_august_clientsession(hass)
|
session = async_create_august_clientsession(hass)
|
||||||
august_gateway = AugustGateway(hass, session)
|
august_gateway = AugustGateway(Path(hass.config.config_dir), session)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await august_gateway.async_setup(entry.data)
|
|
||||||
return await async_setup_august(hass, entry, august_gateway)
|
return await async_setup_august(hass, entry, august_gateway)
|
||||||
except (RequireValidation, InvalidAuth) as err:
|
except (RequireValidation, InvalidAuth) as err:
|
||||||
raise ConfigEntryAuthFailed from 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:
|
async def async_unload_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
entry.runtime_data.async_stop()
|
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
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
|
hass: HomeAssistant, config_entry: AugustConfigEntry, august_gateway: AugustGateway
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Set up the August component."""
|
"""Set up the August component."""
|
||||||
|
config = cast(YaleXSConfig, config_entry.data)
|
||||||
|
await august_gateway.async_setup(config)
|
||||||
|
|
||||||
if CONF_PASSWORD in config_entry.data:
|
if CONF_PASSWORD in config_entry.data:
|
||||||
# We no longer need to store passwords since we do not
|
# 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."""
|
"""August data object."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -126,17 +129,17 @@ class AugustData(AugustSubscriberMixin):
|
||||||
august_gateway: AugustGateway,
|
august_gateway: AugustGateway,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Init August data object."""
|
"""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._config_entry = config_entry
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._august_gateway = august_gateway
|
self._august_gateway = august_gateway
|
||||||
self.activity_stream: ActivityStream = None # type: ignore[assignment]
|
self.activity_stream: ActivityStream = None
|
||||||
self._api = august_gateway.api
|
self._api = august_gateway.api
|
||||||
self._device_detail_by_id: dict[str, LockDetail | DoorbellDetail] = {}
|
self._device_detail_by_id: dict[str, LockDetail | DoorbellDetail] = {}
|
||||||
self._doorbells_by_id: dict[str, Doorbell] = {}
|
self._doorbells_by_id: dict[str, Doorbell] = {}
|
||||||
self._locks_by_id: dict[str, Lock] = {}
|
self._locks_by_id: dict[str, Lock] = {}
|
||||||
self._house_ids: set[str] = set()
|
self._house_ids: set[str] = set()
|
||||||
self._pubnub_unsub: CALLBACK_TYPE | None = None
|
self._pubnub_unsub: Callable[[], Coroutine[Any, Any, None]] | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def brand(self) -> str:
|
def brand(self) -> str:
|
||||||
|
@ -148,13 +151,8 @@ class AugustData(AugustSubscriberMixin):
|
||||||
token = self._august_gateway.access_token
|
token = self._august_gateway.access_token
|
||||||
# This used to be a gather but it was less reliable with august's recent api changes.
|
# 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)
|
user_data = await self._api.async_get_user(token)
|
||||||
locks: list[Lock] = await self._api.async_get_operable_locks(token)
|
locks: list[Lock] = await self._api.async_get_operable_locks(token) or []
|
||||||
doorbells: list[Doorbell] = await self._api.async_get_doorbells(token)
|
doorbells: list[Doorbell] = await self._api.async_get_doorbells(token) or []
|
||||||
if not doorbells:
|
|
||||||
doorbells = []
|
|
||||||
if not locks:
|
|
||||||
locks = []
|
|
||||||
|
|
||||||
self._doorbells_by_id = {device.device_id: device for device in doorbells}
|
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._locks_by_id = {device.device_id: device for device in locks}
|
||||||
self._house_ids = {device.house_id for device in chain(locks, doorbells)}
|
self._house_ids = {device.house_id for device in chain(locks, doorbells)}
|
||||||
|
@ -175,9 +173,14 @@ class AugustData(AugustSubscriberMixin):
|
||||||
pubnub.register_device(device)
|
pubnub.register_device(device)
|
||||||
|
|
||||||
self.activity_stream = ActivityStream(
|
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()
|
await self.activity_stream.async_setup()
|
||||||
|
|
||||||
pubnub.subscribe(self.async_pubnub_message)
|
pubnub.subscribe(self.async_pubnub_message)
|
||||||
self._pubnub_unsub = async_create_pubnub(
|
self._pubnub_unsub = async_create_pubnub(
|
||||||
user_data["UserID"],
|
user_data["UserID"],
|
||||||
|
@ -200,8 +203,10 @@ class AugustData(AugustSubscriberMixin):
|
||||||
# awake when they come back online
|
# awake when they come back online
|
||||||
for result in await asyncio.gather(
|
for result in await asyncio.gather(
|
||||||
*[
|
*[
|
||||||
self.async_status_async(
|
create_eager_task(
|
||||||
device_id, bool(detail.bridge and detail.bridge.hyper_bridge)
|
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()
|
for device_id, detail in self._device_detail_by_id.items()
|
||||||
if device_id in self._locks_by_id
|
if device_id in self._locks_by_id
|
||||||
|
@ -231,11 +236,10 @@ class AugustData(AugustSubscriberMixin):
|
||||||
self.async_signal_device_id_update(device.device_id)
|
self.async_signal_device_id_update(device.device_id)
|
||||||
activity_stream.async_schedule_house_id_refresh(device.house_id)
|
activity_stream.async_schedule_house_id_refresh(device.house_id)
|
||||||
|
|
||||||
@callback
|
async def async_stop(self, event: Event | None = None) -> None:
|
||||||
def async_stop(self) -> None:
|
|
||||||
"""Stop the subscriptions."""
|
"""Stop the subscriptions."""
|
||||||
if self._pubnub_unsub:
|
if self._pubnub_unsub:
|
||||||
self._pubnub_unsub()
|
await self._pubnub_unsub()
|
||||||
self.activity_stream.async_stop()
|
self.activity_stream.async_stop()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -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
|
|
|
@ -3,12 +3,14 @@
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from yalexs.authenticator import ValidationResult
|
from yalexs.authenticator import ValidationResult
|
||||||
from yalexs.const import BRANDS, DEFAULT_BRAND
|
from yalexs.const import BRANDS, DEFAULT_BRAND
|
||||||
|
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
@ -23,7 +25,6 @@ from .const import (
|
||||||
LOGIN_METHODS,
|
LOGIN_METHODS,
|
||||||
VERIFICATION_CODE_KEY,
|
VERIFICATION_CODE_KEY,
|
||||||
)
|
)
|
||||||
from .exceptions import CannotConnect, InvalidAuth, RequireValidation
|
|
||||||
from .gateway import AugustGateway
|
from .gateway import AugustGateway
|
||||||
from .util import async_create_august_clientsession
|
from .util import async_create_august_clientsession
|
||||||
|
|
||||||
|
@ -164,7 +165,9 @@ class AugustConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
if self._august_gateway is not None:
|
if self._august_gateway is not None:
|
||||||
return self._august_gateway
|
return self._august_gateway
|
||||||
self._aiohttp_session = async_create_august_clientsession(self.hass)
|
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
|
return self._august_gateway
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
|
|
@ -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."""
|
|
|
@ -1,56 +1,23 @@
|
||||||
"""Handle August connection setup and authentication."""
|
"""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 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.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.const import CONF_USERNAME
|
||||||
from homeassistant.core import HomeAssistant, callback
|
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_ACCESS_TOKEN_CACHE_FILE,
|
CONF_ACCESS_TOKEN_CACHE_FILE,
|
||||||
CONF_BRAND,
|
CONF_BRAND,
|
||||||
CONF_INSTALL_ID,
|
CONF_INSTALL_ID,
|
||||||
CONF_LOGIN_METHOD,
|
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."""
|
"""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]:
|
def config_entry(self) -> dict[str, Any]:
|
||||||
"""Config entry."""
|
"""Config entry."""
|
||||||
assert self._config is not None
|
assert self._config is not None
|
||||||
|
@ -61,101 +28,3 @@ class AugustGateway:
|
||||||
CONF_INSTALL_ID: self._config.get(CONF_INSTALL_ID),
|
CONF_INSTALL_ID: self._config.get(CONF_INSTALL_ID),
|
||||||
CONF_ACCESS_TOKEN_CACHE_FILE: self._access_token_cache_file,
|
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
|
|
||||||
|
|
|
@ -28,5 +28,5 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["pubnub", "yalexs"],
|
"loggers": ["pubnub", "yalexs"],
|
||||||
"requirements": ["yalexs==3.1.0", "yalexs-ble==2.4.2"]
|
"requirements": ["yalexs==5.2.0", "yalexs-ble==2.4.2"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
|
@ -2933,7 +2933,7 @@ yalesmartalarmclient==0.3.9
|
||||||
yalexs-ble==2.4.2
|
yalexs-ble==2.4.2
|
||||||
|
|
||||||
# homeassistant.components.august
|
# homeassistant.components.august
|
||||||
yalexs==3.1.0
|
yalexs==5.2.0
|
||||||
|
|
||||||
# homeassistant.components.yeelight
|
# homeassistant.components.yeelight
|
||||||
yeelight==0.7.14
|
yeelight==0.7.14
|
||||||
|
|
|
@ -2289,7 +2289,7 @@ yalesmartalarmclient==0.3.9
|
||||||
yalexs-ble==2.4.2
|
yalexs-ble==2.4.2
|
||||||
|
|
||||||
# homeassistant.components.august
|
# homeassistant.components.august
|
||||||
yalexs==3.1.0
|
yalexs==5.2.0
|
||||||
|
|
||||||
# homeassistant.components.yeelight
|
# homeassistant.components.yeelight
|
||||||
yeelight==0.7.14
|
yeelight==0.7.14
|
||||||
|
|
|
@ -58,8 +58,8 @@ def _mock_authenticator(auth_state):
|
||||||
return authenticator
|
return authenticator
|
||||||
|
|
||||||
|
|
||||||
@patch("homeassistant.components.august.gateway.ApiAsync")
|
@patch("yalexs.manager.gateway.ApiAsync")
|
||||||
@patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate")
|
@patch("yalexs.manager.gateway.AuthenticatorAsync.async_authenticate")
|
||||||
async def _mock_setup_august(
|
async def _mock_setup_august(
|
||||||
hass, api_instance, pubnub_mock, authenticate_mock, api_mock, brand
|
hass, api_instance, pubnub_mock, authenticate_mock, api_mock, brand
|
||||||
):
|
):
|
||||||
|
@ -77,7 +77,10 @@ async def _mock_setup_august(
|
||||||
)
|
)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
with (
|
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),
|
patch("homeassistant.components.august.AugustPubNub", return_value=pubnub_mock),
|
||||||
):
|
):
|
||||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from yalexs.authenticator import ValidationResult
|
from yalexs.authenticator import ValidationResult
|
||||||
|
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.august.const import (
|
from homeassistant.components.august.const import (
|
||||||
|
@ -13,11 +14,6 @@ from homeassistant.components.august.const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
VERIFICATION_CODE_KEY,
|
VERIFICATION_CODE_KEY,
|
||||||
)
|
)
|
||||||
from homeassistant.components.august.exceptions import (
|
|
||||||
CannotConnect,
|
|
||||||
InvalidAuth,
|
|
||||||
RequireValidation,
|
|
||||||
)
|
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
@ -151,7 +147,7 @@ async def test_form_needs_validate(hass: HomeAssistant) -> None:
|
||||||
side_effect=RequireValidation,
|
side_effect=RequireValidation,
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code",
|
"yalexs.manager.gateway.AuthenticatorAsync.async_send_verification_code",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
) as mock_send_verification_code,
|
) as mock_send_verification_code,
|
||||||
):
|
):
|
||||||
|
@ -176,11 +172,11 @@ async def test_form_needs_validate(hass: HomeAssistant) -> None:
|
||||||
side_effect=RequireValidation,
|
side_effect=RequireValidation,
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.august.gateway.AuthenticatorAsync.async_validate_verification_code",
|
"yalexs.manager.gateway.AuthenticatorAsync.async_validate_verification_code",
|
||||||
return_value=ValidationResult.INVALID_VERIFICATION_CODE,
|
return_value=ValidationResult.INVALID_VERIFICATION_CODE,
|
||||||
) as mock_validate_verification_code,
|
) as mock_validate_verification_code,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code",
|
"yalexs.manager.gateway.AuthenticatorAsync.async_send_verification_code",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
) as mock_send_verification_code,
|
) as mock_send_verification_code,
|
||||||
):
|
):
|
||||||
|
@ -204,11 +200,11 @@ async def test_form_needs_validate(hass: HomeAssistant) -> None:
|
||||||
return_value=True,
|
return_value=True,
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.august.gateway.AuthenticatorAsync.async_validate_verification_code",
|
"yalexs.manager.gateway.AuthenticatorAsync.async_validate_verification_code",
|
||||||
return_value=ValidationResult.VALIDATED,
|
return_value=ValidationResult.VALIDATED,
|
||||||
) as mock_validate_verification_code,
|
) as mock_validate_verification_code,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code",
|
"yalexs.manager.gateway.AuthenticatorAsync.async_send_verification_code",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
) as mock_send_verification_code,
|
) as mock_send_verification_code,
|
||||||
patch(
|
patch(
|
||||||
|
@ -310,7 +306,7 @@ async def test_form_reauth_with_2fa(hass: HomeAssistant) -> None:
|
||||||
side_effect=RequireValidation,
|
side_effect=RequireValidation,
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code",
|
"yalexs.manager.gateway.AuthenticatorAsync.async_send_verification_code",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
) as mock_send_verification_code,
|
) as mock_send_verification_code,
|
||||||
):
|
):
|
||||||
|
@ -334,11 +330,11 @@ async def test_form_reauth_with_2fa(hass: HomeAssistant) -> None:
|
||||||
return_value=True,
|
return_value=True,
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.august.gateway.AuthenticatorAsync.async_validate_verification_code",
|
"yalexs.manager.gateway.AuthenticatorAsync.async_validate_verification_code",
|
||||||
return_value=ValidationResult.VALIDATED,
|
return_value=ValidationResult.VALIDATED,
|
||||||
) as mock_validate_verification_code,
|
) as mock_validate_verification_code,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code",
|
"yalexs.manager.gateway.AuthenticatorAsync.async_send_verification_code",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
) as mock_send_verification_code,
|
) as mock_send_verification_code,
|
||||||
patch(
|
patch(
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""The gateway tests for the august platform."""
|
"""The gateway tests for the august platform."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from yalexs.authenticator_common import AuthenticationState
|
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)
|
await _patched_refresh_access_token(hass, "new_token", 5678)
|
||||||
|
|
||||||
|
|
||||||
@patch("homeassistant.components.august.gateway.ApiAsync.async_get_operable_locks")
|
@patch("yalexs.manager.gateway.ApiAsync.async_get_operable_locks")
|
||||||
@patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate")
|
@patch("yalexs.manager.gateway.AuthenticatorAsync.async_authenticate")
|
||||||
@patch("homeassistant.components.august.gateway.AuthenticatorAsync.should_refresh")
|
@patch("yalexs.manager.gateway.AuthenticatorAsync.should_refresh")
|
||||||
@patch(
|
@patch("yalexs.manager.gateway.AuthenticatorAsync.async_refresh_access_token")
|
||||||
"homeassistant.components.august.gateway.AuthenticatorAsync.async_refresh_access_token"
|
|
||||||
)
|
|
||||||
async def _patched_refresh_access_token(
|
async def _patched_refresh_access_token(
|
||||||
hass,
|
hass,
|
||||||
new_token,
|
new_token,
|
||||||
|
@ -36,7 +35,7 @@ async def _patched_refresh_access_token(
|
||||||
"original_token", 1234, AuthenticationState.AUTHENTICATED
|
"original_token", 1234, AuthenticationState.AUTHENTICATED
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
august_gateway = AugustGateway(hass, MagicMock())
|
august_gateway = AugustGateway(Path(hass.config.config_dir), MagicMock())
|
||||||
mocked_config = _mock_get_config()
|
mocked_config = _mock_get_config()
|
||||||
await august_gateway.async_setup(mocked_config[DOMAIN])
|
await august_gateway.async_setup(mocked_config[DOMAIN])
|
||||||
await august_gateway.async_authenticate()
|
await august_gateway.async_authenticate()
|
||||||
|
|
|
@ -6,9 +6,9 @@ from unittest.mock import Mock
|
||||||
from aiohttp import ClientResponseError
|
from aiohttp import ClientResponseError
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
import pytest
|
import pytest
|
||||||
|
from yalexs.manager.activity import INITIAL_LOCK_RESYNC_TIME
|
||||||
from yalexs.pubnub_async import AugustPubNub
|
from yalexs.pubnub_async import AugustPubNub
|
||||||
|
|
||||||
from homeassistant.components.august.activity import INITIAL_LOCK_RESYNC_TIME
|
|
||||||
from homeassistant.components.lock import (
|
from homeassistant.components.lock import (
|
||||||
DOMAIN as LOCK_DOMAIN,
|
DOMAIN as LOCK_DOMAIN,
|
||||||
STATE_JAMMED,
|
STATE_JAMMED,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue