Split august and yale integrations (#124677)

* Split august and yale integrations [part 1] (#122253)

* merge with dev

* Remove unused constant

* Remove yale IPv6 workaround (#123409)

* Convert yale to use oauth (#123806)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update yale for switch from pubnub to websockets (#124675)

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
J. Nick Koston 2024-08-28 05:46:03 -10:00 committed by GitHub
parent edad766fd3
commit 03ead27f6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
72 changed files with 5811 additions and 10 deletions

View File

@ -1660,6 +1660,8 @@ build.json @home-assistant/supervisor
/tests/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG
/homeassistant/components/xiaomi_tv/ @simse
/homeassistant/components/xmpp/ @fabaff @flowolf
/homeassistant/components/yale/ @bdraco
/tests/components/yale/ @bdraco
/homeassistant/components/yale_smart_alarm/ @gjohansson-ST
/tests/components/yale_smart_alarm/ @gjohansson-ST
/homeassistant/components/yalexs_ble/ @bdraco

View File

@ -1,5 +1,11 @@
{
"domain": "yale",
"name": "Yale",
"integrations": ["august", "yale_smart_alarm", "yalexs_ble", "yale_home"]
"integrations": [
"august",
"yale_smart_alarm",
"yalexs_ble",
"yale_home",
"yale"
]
}

View File

@ -4,10 +4,6 @@
"codeowners": ["@bdraco"],
"config_flow": true,
"dhcp": [
{
"hostname": "yale-connect-plus",
"macaddress": "00177A*"
},
{
"hostname": "connect",
"macaddress": "D86162*"

View File

@ -0,0 +1,81 @@
"""Support for Yale devices."""
from __future__ import annotations
from pathlib import Path
from typing import cast
from aiohttp import ClientResponseError
from yalexs.const import Brand
from yalexs.exceptions import YaleApiError
from yalexs.manager.const import CONF_BRAND
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
from yalexs.manager.gateway import Config as YaleXSConfig
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_entry_oauth2_flow, device_registry as dr
from .const import DOMAIN, PLATFORMS
from .data import YaleData
from .gateway import YaleGateway
from .util import async_create_yale_clientsession
type YaleConfigEntry = ConfigEntry[YaleData]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up yale from a config entry."""
session = async_create_yale_clientsession(hass)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)
oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
yale_gateway = YaleGateway(Path(hass.config.config_dir), session, oauth_session)
try:
await async_setup_yale(hass, entry, yale_gateway)
except (RequireValidation, InvalidAuth) as err:
raise ConfigEntryAuthFailed from err
except TimeoutError as err:
raise ConfigEntryNotReady("Timed out connecting to yale api") from err
except (YaleApiError, ClientResponseError, CannotConnect) as err:
raise ConfigEntryNotReady from err
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: YaleConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_setup_yale(
hass: HomeAssistant, entry: YaleConfigEntry, yale_gateway: YaleGateway
) -> None:
"""Set up the yale component."""
config = cast(YaleXSConfig, entry.data)
await yale_gateway.async_setup({**config, CONF_BRAND: Brand.YALE_GLOBAL})
await yale_gateway.async_authenticate()
await yale_gateway.async_refresh_access_token_if_needed()
data = entry.runtime_data = YaleData(hass, yale_gateway)
entry.async_on_unload(
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, data.async_stop)
)
entry.async_on_unload(data.async_stop)
await data.async_setup()
async def async_remove_config_entry_device(
hass: HomeAssistant, config_entry: YaleConfigEntry, device_entry: dr.DeviceEntry
) -> bool:
"""Remove yale config entry from a device if its no longer present."""
return not any(
identifier
for identifier in device_entry.identifiers
if identifier[0] == DOMAIN
and config_entry.runtime_data.get_device(identifier[1])
)

View File

@ -0,0 +1,15 @@
"""application_credentials platform the yale integration."""
from homeassistant.components.application_credentials import AuthorizationServer
from homeassistant.core import HomeAssistant
OAUTH2_AUTHORIZE = "https://oauth.aaecosystem.com/authorize"
OAUTH2_TOKEN = "https://oauth.aaecosystem.com/access_token"
async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer:
"""Return authorization server."""
return AuthorizationServer(
authorize_url=OAUTH2_AUTHORIZE,
token_url=OAUTH2_TOKEN,
)

View File

@ -0,0 +1,189 @@
"""Support for Yale binary sensors."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
from functools import partial
import logging
from yalexs.activity import Activity, ActivityType
from yalexs.doorbell import DoorbellDetail
from yalexs.lock import LockDetail, LockDoorStatus
from yalexs.manager.const import ACTIVITY_UPDATE_INTERVAL
from yalexs.util import update_lock_detail_from_activity
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_call_later
from . import YaleConfigEntry, YaleData
from .entity import YaleDescriptionEntity
from .util import (
retrieve_ding_activity,
retrieve_doorbell_motion_activity,
retrieve_online_state,
retrieve_time_based_activity,
)
_LOGGER = logging.getLogger(__name__)
TIME_TO_RECHECK_DETECTION = timedelta(
seconds=ACTIVITY_UPDATE_INTERVAL.total_seconds() * 3
)
@dataclass(frozen=True, kw_only=True)
class YaleDoorbellBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes Yale binary_sensor entity."""
value_fn: Callable[[YaleData, DoorbellDetail | LockDetail], Activity | None]
is_time_based: bool
SENSOR_TYPE_DOOR = BinarySensorEntityDescription(
key="open",
device_class=BinarySensorDeviceClass.DOOR,
)
SENSOR_TYPES_VIDEO_DOORBELL = (
YaleDoorbellBinarySensorEntityDescription(
key="motion",
device_class=BinarySensorDeviceClass.MOTION,
value_fn=retrieve_doorbell_motion_activity,
is_time_based=True,
),
YaleDoorbellBinarySensorEntityDescription(
key="image capture",
translation_key="image_capture",
value_fn=partial(
retrieve_time_based_activity, {ActivityType.DOORBELL_IMAGE_CAPTURE}
),
is_time_based=True,
),
YaleDoorbellBinarySensorEntityDescription(
key="online",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=retrieve_online_state,
is_time_based=False,
),
)
SENSOR_TYPES_DOORBELL: tuple[YaleDoorbellBinarySensorEntityDescription, ...] = (
YaleDoorbellBinarySensorEntityDescription(
key="ding",
translation_key="ding",
device_class=BinarySensorDeviceClass.OCCUPANCY,
value_fn=retrieve_ding_activity,
is_time_based=True,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: YaleConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Yale binary sensors."""
data = config_entry.runtime_data
entities: list[BinarySensorEntity] = []
for lock in data.locks:
detail = data.get_device_detail(lock.device_id)
if detail.doorsense:
entities.append(YaleDoorBinarySensor(data, lock, SENSOR_TYPE_DOOR))
if detail.doorbell:
entities.extend(
YaleDoorbellBinarySensor(data, lock, description)
for description in SENSOR_TYPES_DOORBELL
)
for doorbell in data.doorbells:
entities.extend(
YaleDoorbellBinarySensor(data, doorbell, description)
for description in SENSOR_TYPES_DOORBELL + SENSOR_TYPES_VIDEO_DOORBELL
)
async_add_entities(entities)
class YaleDoorBinarySensor(YaleDescriptionEntity, BinarySensorEntity):
"""Representation of an Yale Door binary sensor."""
_attr_device_class = BinarySensorDeviceClass.DOOR
description: BinarySensorEntityDescription
@callback
def _update_from_data(self) -> None:
"""Get the latest state of the sensor and update activity."""
if door_activity := self._get_latest({ActivityType.DOOR_OPERATION}):
update_lock_detail_from_activity(self._detail, door_activity)
if door_activity.was_pushed:
self._detail.set_online(True)
if bridge_activity := self._get_latest({ActivityType.BRIDGE_OPERATION}):
update_lock_detail_from_activity(self._detail, bridge_activity)
self._attr_available = self._detail.bridge_is_online
self._attr_is_on = self._detail.door_state == LockDoorStatus.OPEN
class YaleDoorbellBinarySensor(YaleDescriptionEntity, BinarySensorEntity):
"""Representation of an Yale binary sensor."""
entity_description: YaleDoorbellBinarySensorEntityDescription
_check_for_off_update_listener: Callable[[], None] | None = None
@callback
def _update_from_data(self) -> None:
"""Get the latest state of the sensor."""
self._cancel_any_pending_updates()
self._attr_is_on = bool(
self.entity_description.value_fn(self._data, self._detail)
)
if self.entity_description.is_time_based:
self._attr_available = retrieve_online_state(self._data, self._detail)
self._schedule_update_to_recheck_turn_off_sensor()
else:
self._attr_available = True
@callback
def _async_scheduled_update(self, now: datetime) -> None:
"""Timer callback for sensor update."""
self._check_for_off_update_listener = None
self._update_from_data()
if not self.is_on:
self.async_write_ha_state()
def _schedule_update_to_recheck_turn_off_sensor(self) -> None:
"""Schedule an update to recheck the sensor to see if it is ready to turn off."""
# If the sensor is already off there is nothing to do
if not self.is_on:
return
self._check_for_off_update_listener = async_call_later(
self.hass, TIME_TO_RECHECK_DETECTION, self._async_scheduled_update
)
def _cancel_any_pending_updates(self) -> None:
"""Cancel any updates to recheck a sensor to see if it is ready to turn off."""
if not self._check_for_off_update_listener:
return
_LOGGER.debug("%s: canceled pending update", self.entity_id)
self._check_for_off_update_listener()
self._check_for_off_update_listener = None
async def async_will_remove_from_hass(self) -> None:
"""When removing cancel any scheduled updates."""
self._cancel_any_pending_updates()
await super().async_will_remove_from_hass()

View File

@ -0,0 +1,32 @@
"""Support for Yale buttons."""
from homeassistant.components.button import ButtonEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import YaleConfigEntry
from .entity import YaleEntityMixin
async def async_setup_entry(
hass: HomeAssistant,
config_entry: YaleConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Yale lock wake buttons."""
data = config_entry.runtime_data
async_add_entities(YaleWakeLockButton(data, lock, "wake") for lock in data.locks)
class YaleWakeLockButton(YaleEntityMixin, ButtonEntity):
"""Representation of an Yale lock wake button."""
_attr_translation_key = "wake"
async def async_press(self) -> None:
"""Wake the device."""
await self._data.async_status_async(self._device_id, self._hyper_bridge)
@callback
def _update_from_data(self) -> None:
"""Nothing to update as buttons are stateless."""

View File

@ -0,0 +1,90 @@
"""Support for Yale doorbell camera."""
from __future__ import annotations
import logging
from aiohttp import ClientSession
from yalexs.activity import ActivityType
from yalexs.doorbell import Doorbell
from yalexs.util import update_doorbell_image_from_activity
from homeassistant.components.camera import Camera
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import YaleConfigEntry, YaleData
from .const import DEFAULT_NAME, DEFAULT_TIMEOUT
from .entity import YaleEntityMixin
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: YaleConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Yale cameras."""
data = config_entry.runtime_data
# Create an aiohttp session instead of using the default one since the
# default one is likely to trigger yale's WAF if another integration
# is also using Cloudflare
session = aiohttp_client.async_create_clientsession(hass)
async_add_entities(
YaleCamera(data, doorbell, session, DEFAULT_TIMEOUT)
for doorbell in data.doorbells
)
class YaleCamera(YaleEntityMixin, Camera):
"""An implementation of an Yale security camera."""
_attr_translation_key = "camera"
_attr_motion_detection_enabled = True
_attr_brand = DEFAULT_NAME
_image_url: str | None = None
_image_content: bytes | None = None
def __init__(
self, data: YaleData, device: Doorbell, session: ClientSession, timeout: int
) -> None:
"""Initialize an Yale security camera."""
super().__init__(data, device, "camera")
self._timeout = timeout
self._session = session
self._attr_model = self._detail.model
@property
def is_recording(self) -> bool:
"""Return true if the device is recording."""
return self._device.has_subscription
async def _async_update(self):
"""Update device."""
_LOGGER.debug("async_update called %s", self._detail.device_name)
await self._data.refresh_camera_by_id(self._device_id)
self._update_from_data()
@callback
def _update_from_data(self) -> None:
"""Get the latest state of the sensor."""
if doorbell_activity := self._get_latest(
{ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_IMAGE_CAPTURE}
):
update_doorbell_image_from_activity(self._detail, doorbell_activity)
async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return bytes of camera image."""
self._update_from_data()
if self._image_url is not self._detail.image_url:
self._image_content = await self._data.async_get_doorbell_image(
self._device_id, self._session, timeout=self._timeout
)
self._image_url = self._detail.image_url
return self._image_content

View File

@ -0,0 +1,57 @@
"""Config flow for Yale integration."""
from collections.abc import Mapping
import logging
from typing import Any
import jwt
from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
from homeassistant.helpers import config_entry_oauth2_flow
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class YaleConfigFlow(config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN):
"""Handle a config flow for Yale."""
VERSION = 1
DOMAIN = DOMAIN
reauth_entry: ConfigEntry | None = None
@property
def logger(self) -> logging.Logger:
"""Return logger."""
return _LOGGER
async def async_step_reauth(self, data: Mapping[str, Any]) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
self.reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
return await self.async_step_user()
def _async_get_user_id_from_access_token(self, encoded: str) -> str:
"""Get user ID from access token."""
decoded = jwt.decode(
encoded,
"",
verify=False,
options={"verify_signature": False},
algorithms=["HS256"],
)
return decoded["userId"]
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
"""Create an entry for the flow."""
user_id = self._async_get_user_id_from_access_token(
data["token"]["access_token"]
)
if entry := self.reauth_entry:
if entry.unique_id != user_id:
return self.async_abort(reason="reauth_invalid_user")
return self.async_update_reload_and_abort(entry, data=data)
await self.async_set_unique_id(user_id)
return await super().async_oauth_create_entry(data)

View File

@ -0,0 +1,47 @@
"""Constants for Yale devices."""
from yalexs.const import Brand
from homeassistant.const import Platform
DEFAULT_TIMEOUT = 25
CONF_ACCESS_TOKEN_CACHE_FILE = "access_token_cache_file"
CONF_BRAND = "brand"
CONF_LOGIN_METHOD = "login_method"
CONF_INSTALL_ID = "install_id"
VERIFICATION_CODE_KEY = "verification_code"
DEFAULT_BRAND = Brand.YALE_HOME
MANUFACTURER = "Yale Home Inc."
DEFAULT_NAME = "Yale"
DOMAIN = "yale"
OPERATION_METHOD_AUTORELOCK = "autorelock"
OPERATION_METHOD_REMOTE = "remote"
OPERATION_METHOD_KEYPAD = "keypad"
OPERATION_METHOD_MANUAL = "manual"
OPERATION_METHOD_TAG = "tag"
OPERATION_METHOD_MOBILE_DEVICE = "mobile"
ATTR_OPERATION_AUTORELOCK = "autorelock"
ATTR_OPERATION_METHOD = "method"
ATTR_OPERATION_REMOTE = "remote"
ATTR_OPERATION_KEYPAD = "keypad"
ATTR_OPERATION_MANUAL = "manual"
ATTR_OPERATION_TAG = "tag"
LOGIN_METHODS = ["phone", "email"]
DEFAULT_LOGIN_METHOD = "email"
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CAMERA,
Platform.EVENT,
Platform.LOCK,
Platform.SENSOR,
]

View File

@ -0,0 +1,52 @@
"""Support for Yale devices."""
from __future__ import annotations
from yalexs.lock import LockDetail
from yalexs.manager.data import YaleXSData
from yalexs_ble import YaleXSBLEDiscovery
from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import discovery_flow
from .gateway import YaleGateway
YALEXS_BLE_DOMAIN = "yalexs_ble"
@callback
def _async_trigger_ble_lock_discovery(
hass: HomeAssistant, locks_with_offline_keys: list[LockDetail]
) -> None:
"""Update keys for the yalexs-ble integration if available."""
for lock_detail in locks_with_offline_keys:
discovery_flow.async_create_flow(
hass,
YALEXS_BLE_DOMAIN,
context={"source": SOURCE_INTEGRATION_DISCOVERY},
data=YaleXSBLEDiscovery(
{
"name": lock_detail.device_name,
"address": lock_detail.mac_address,
"serial": lock_detail.serial_number,
"key": lock_detail.offline_key,
"slot": lock_detail.offline_slot,
}
),
)
class YaleData(YaleXSData):
"""yale data object."""
def __init__(self, hass: HomeAssistant, yale_gateway: YaleGateway) -> None:
"""Init yale data object."""
self._hass = hass
super().__init__(yale_gateway, HomeAssistantError)
@callback
def async_offline_key_discovered(self, detail: LockDetail) -> None:
"""Handle offline key discovery."""
_async_trigger_ble_lock_discovery(self._hass, [detail])

View File

@ -0,0 +1,49 @@
"""Diagnostics support for yale."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.core import HomeAssistant
from . import YaleConfigEntry
from .const import CONF_BRAND, DEFAULT_BRAND
TO_REDACT = {
"HouseID",
"OfflineKeys",
"installUserID",
"invitations",
"key",
"pins",
"pubsubChannel",
"recentImage",
"remoteOperateSecret",
"users",
"zWaveDSK",
"contentToken",
}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: YaleConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
data = entry.runtime_data
return {
"locks": {
lock.device_id: async_redact_data(
data.get_device_detail(lock.device_id).raw, TO_REDACT
)
for lock in data.locks
},
"doorbells": {
doorbell.device_id: async_redact_data(
data.get_device_detail(doorbell.device_id).raw, TO_REDACT
)
for doorbell in data.doorbells
},
"brand": entry.data.get(CONF_BRAND, DEFAULT_BRAND),
}

View File

@ -0,0 +1,115 @@
"""Base class for Yale entity."""
from abc import abstractmethod
from yalexs.activity import Activity, ActivityType
from yalexs.doorbell import Doorbell, DoorbellDetail
from yalexs.keypad import KeypadDetail
from yalexs.lock import Lock, LockDetail
from yalexs.util import get_configuration_url
from homeassistant.const import ATTR_CONNECTIONS
from homeassistant.core import callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity, EntityDescription
from . import DOMAIN, YaleData
from .const import MANUFACTURER
DEVICE_TYPES = ["keypad", "lock", "camera", "doorbell", "door", "bell"]
class YaleEntityMixin(Entity):
"""Base implementation for Yale device."""
_attr_should_poll = False
_attr_has_entity_name = True
def __init__(
self, data: YaleData, device: Doorbell | Lock | KeypadDetail, unique_id: str
) -> None:
"""Initialize an Yale device."""
super().__init__()
self._data = data
self._stream = data.activity_stream
self._device = device
detail = self._detail
self._device_id = device.device_id
self._attr_unique_id = f"{device.device_id}_{unique_id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._device_id)},
manufacturer=MANUFACTURER,
model=detail.model,
name=device.device_name,
sw_version=detail.firmware_version,
suggested_area=_remove_device_types(device.device_name, DEVICE_TYPES),
configuration_url=get_configuration_url(data.brand),
)
if isinstance(detail, LockDetail) and (mac := detail.mac_address):
self._attr_device_info[ATTR_CONNECTIONS] = {(dr.CONNECTION_BLUETOOTH, mac)}
@property
def _detail(self) -> DoorbellDetail | LockDetail:
return self._data.get_device_detail(self._device.device_id)
@property
def _hyper_bridge(self) -> bool:
"""Check if the lock has a paired hyper bridge."""
return bool(self._detail.bridge and self._detail.bridge.hyper_bridge)
@callback
def _get_latest(self, activity_types: set[ActivityType]) -> Activity | None:
"""Get the latest activity for the device."""
return self._stream.get_latest_device_activity(self._device_id, activity_types)
@callback
def _update_from_data_and_write_state(self) -> None:
self._update_from_data()
self.async_write_ha_state()
@abstractmethod
def _update_from_data(self) -> None:
"""Update the entity state from the data object."""
async def async_added_to_hass(self) -> None:
"""Subscribe to updates."""
self.async_on_remove(
self._data.async_subscribe_device_id(
self._device_id, self._update_from_data_and_write_state
)
)
self.async_on_remove(
self._stream.async_subscribe_device_id(
self._device_id, self._update_from_data_and_write_state
)
)
self._update_from_data()
class YaleDescriptionEntity(YaleEntityMixin):
"""An Yale entity with a description."""
def __init__(
self,
data: YaleData,
device: Doorbell | Lock | KeypadDetail,
description: EntityDescription,
) -> None:
"""Initialize an Yale entity with a description."""
super().__init__(data, device, description.key)
self.entity_description = description
def _remove_device_types(name: str, device_types: list[str]) -> str:
"""Strip device types from a string.
Yale stores the name as Master Bed Lock
or Master Bed Door. We can come up with a
reasonable suggestion by removing the supported
device types from the string.
"""
lower_name = name.lower()
for device_type in device_types:
lower_name = lower_name.removesuffix(f" {device_type}")
return name[: len(lower_name)]

View File

@ -0,0 +1,104 @@
"""Support for yale events."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING
from yalexs.activity import Activity
from yalexs.doorbell import DoorbellDetail
from yalexs.lock import LockDetail
from homeassistant.components.event import (
EventDeviceClass,
EventEntity,
EventEntityDescription,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import YaleConfigEntry, YaleData
from .entity import YaleDescriptionEntity
from .util import (
retrieve_ding_activity,
retrieve_doorbell_motion_activity,
retrieve_online_state,
)
@dataclass(kw_only=True, frozen=True)
class YaleEventEntityDescription(EventEntityDescription):
"""Describe yale event entities."""
value_fn: Callable[[YaleData, DoorbellDetail | LockDetail], Activity | None]
TYPES_VIDEO_DOORBELL: tuple[YaleEventEntityDescription, ...] = (
YaleEventEntityDescription(
key="motion",
translation_key="motion",
device_class=EventDeviceClass.MOTION,
event_types=["motion"],
value_fn=retrieve_doorbell_motion_activity,
),
)
TYPES_DOORBELL: tuple[YaleEventEntityDescription, ...] = (
YaleEventEntityDescription(
key="doorbell",
translation_key="doorbell",
device_class=EventDeviceClass.DOORBELL,
event_types=["ring"],
value_fn=retrieve_ding_activity,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: YaleConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the yale event platform."""
data = config_entry.runtime_data
entities: list[YaleEventEntity] = []
for lock in data.locks:
detail = data.get_device_detail(lock.device_id)
if detail.doorbell:
entities.extend(
YaleEventEntity(data, lock, description)
for description in TYPES_DOORBELL
)
for doorbell in data.doorbells:
entities.extend(
YaleEventEntity(data, doorbell, description)
for description in TYPES_DOORBELL + TYPES_VIDEO_DOORBELL
)
async_add_entities(entities)
class YaleEventEntity(YaleDescriptionEntity, EventEntity):
"""An yale event entity."""
entity_description: YaleEventEntityDescription
_attr_has_entity_name = True
_last_activity: Activity | None = None
@callback
def _update_from_data(self) -> None:
"""Update from data."""
self._attr_available = retrieve_online_state(self._data, self._detail)
current_activity = self.entity_description.value_fn(self._data, self._detail)
if not current_activity or current_activity == self._last_activity:
return
self._last_activity = current_activity
event_types = self.entity_description.event_types
if TYPE_CHECKING:
assert event_types is not None
self._trigger_event(event_type=event_types[0])
self.async_write_ha_state()

View File

@ -0,0 +1,43 @@
"""Handle Yale connection setup and authentication."""
import logging
from pathlib import Path
from aiohttp import ClientSession
from yalexs.authenticator_common import Authentication, AuthenticationState
from yalexs.manager.gateway import Gateway
from homeassistant.helpers import config_entry_oauth2_flow
_LOGGER = logging.getLogger(__name__)
class YaleGateway(Gateway):
"""Handle the connection to Yale."""
def __init__(
self,
config_path: Path,
aiohttp_session: ClientSession,
oauth_session: config_entry_oauth2_flow.OAuth2Session,
) -> None:
"""Init the connection."""
super().__init__(config_path, aiohttp_session)
self._oauth_session = oauth_session
async def async_get_access_token(self) -> str:
"""Get access token."""
await self._oauth_session.async_ensure_token_valid()
return self._oauth_session.token["access_token"]
async def async_refresh_access_token_if_needed(self) -> None:
"""Refresh the access token if needed."""
await self._oauth_session.async_ensure_token_valid()
async def async_authenticate(self) -> Authentication:
"""Authenticate with the details provided to setup."""
await self._oauth_session.async_ensure_token_valid()
self.authentication = Authentication(
AuthenticationState.AUTHENTICATED, None, None, None
)
return self.authentication

View File

@ -0,0 +1,9 @@
{
"entity": {
"binary_sensor": {
"image_capture": {
"default": "mdi:file-image"
}
}
}
}

View File

@ -0,0 +1,147 @@
"""Support for Yale lock."""
from __future__ import annotations
from collections.abc import Callable, Coroutine
import logging
from typing import Any
from aiohttp import ClientResponseError
from yalexs.activity import ActivityType, ActivityTypes
from yalexs.lock import Lock, LockStatus
from yalexs.util import get_latest_activity, update_lock_detail_from_activity
from homeassistant.components.lock import ATTR_CHANGED_BY, LockEntity, LockEntityFeature
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
import homeassistant.util.dt as dt_util
from . import YaleConfigEntry, YaleData
from .entity import YaleEntityMixin
_LOGGER = logging.getLogger(__name__)
LOCK_JAMMED_ERR = 531
async def async_setup_entry(
hass: HomeAssistant,
config_entry: YaleConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Yale locks."""
data = config_entry.runtime_data
async_add_entities(YaleLock(data, lock) for lock in data.locks)
class YaleLock(YaleEntityMixin, RestoreEntity, LockEntity):
"""Representation of an Yale lock."""
_attr_name = None
_lock_status: LockStatus | None = None
def __init__(self, data: YaleData, device: Lock) -> None:
"""Initialize the lock."""
super().__init__(data, device, "lock")
if self._detail.unlatch_supported:
self._attr_supported_features = LockEntityFeature.OPEN
async def async_lock(self, **kwargs: Any) -> None:
"""Lock the device."""
if self._data.push_updates_connected:
await self._data.async_lock_async(self._device_id, self._hyper_bridge)
return
await self._call_lock_operation(self._data.async_lock)
async def async_open(self, **kwargs: Any) -> None:
"""Open/unlatch the device."""
if self._data.push_updates_connected:
await self._data.async_unlatch_async(self._device_id, self._hyper_bridge)
return
await self._call_lock_operation(self._data.async_unlatch)
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the device."""
if self._data.push_updates_connected:
await self._data.async_unlock_async(self._device_id, self._hyper_bridge)
return
await self._call_lock_operation(self._data.async_unlock)
async def _call_lock_operation(
self, lock_operation: Callable[[str], Coroutine[Any, Any, list[ActivityTypes]]]
) -> None:
try:
activities = await lock_operation(self._device_id)
except ClientResponseError as err:
if err.status == LOCK_JAMMED_ERR:
self._detail.lock_status = LockStatus.JAMMED
self._detail.lock_status_datetime = dt_util.utcnow()
else:
raise
else:
for lock_activity in activities:
update_lock_detail_from_activity(self._detail, lock_activity)
if self._update_lock_status_from_detail():
_LOGGER.debug(
"async_signal_device_id_update (from lock operation): %s",
self._device_id,
)
self._data.async_signal_device_id_update(self._device_id)
def _update_lock_status_from_detail(self) -> bool:
self._attr_available = self._detail.bridge_is_online
if self._lock_status != self._detail.lock_status:
self._lock_status = self._detail.lock_status
return True
return False
@callback
def _update_from_data(self) -> None:
"""Get the latest state of the sensor and update activity."""
detail = self._detail
if lock_activity := self._get_latest({ActivityType.LOCK_OPERATION}):
self._attr_changed_by = lock_activity.operated_by
lock_activity_without_operator = self._get_latest(
{ActivityType.LOCK_OPERATION_WITHOUT_OPERATOR}
)
if latest_activity := get_latest_activity(
lock_activity_without_operator, lock_activity
):
if latest_activity.was_pushed:
self._detail.set_online(True)
update_lock_detail_from_activity(detail, latest_activity)
if bridge_activity := self._get_latest({ActivityType.BRIDGE_OPERATION}):
update_lock_detail_from_activity(detail, bridge_activity)
self._update_lock_status_from_detail()
lock_status = self._lock_status
if lock_status is None or lock_status is LockStatus.UNKNOWN:
self._attr_is_locked = None
else:
self._attr_is_locked = lock_status is LockStatus.LOCKED
self._attr_is_jammed = lock_status is LockStatus.JAMMED
self._attr_is_locking = lock_status is LockStatus.LOCKING
self._attr_is_unlocking = lock_status in (
LockStatus.UNLOCKING,
LockStatus.UNLATCHING,
)
self._attr_extra_state_attributes = {ATTR_BATTERY_LEVEL: detail.battery_level}
if keypad := detail.keypad:
self._attr_extra_state_attributes["keypad_battery_level"] = (
keypad.battery_level
)
async def async_added_to_hass(self) -> None:
"""Restore ATTR_CHANGED_BY on startup since it is likely no longer in the activity log."""
await super().async_added_to_hass()
if not (last_state := await self.async_get_last_state()):
return
if ATTR_CHANGED_BY in last_state.attributes:
self._attr_changed_by = last_state.attributes[ATTR_CHANGED_BY]

View File

@ -0,0 +1,16 @@
{
"domain": "yale",
"name": "Yale",
"codeowners": ["@bdraco"],
"config_flow": true,
"dhcp": [
{
"hostname": "yale-connect-plus",
"macaddress": "00177A*"
}
],
"documentation": "https://www.home-assistant.io/integrations/yale",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==8.5.4", "yalexs-ble==2.4.3"]
}

View File

@ -0,0 +1,210 @@
"""Support for Yale sensors."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Generic, TypeVar, cast
from yalexs.activity import ActivityType, LockOperationActivity
from yalexs.doorbell import Doorbell
from yalexs.keypad import KeypadDetail
from yalexs.lock import LockDetail
from homeassistant.components.sensor import (
RestoreSensor,
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
ATTR_ENTITY_PICTURE,
PERCENTAGE,
STATE_UNAVAILABLE,
EntityCategory,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import YaleConfigEntry
from .const import (
ATTR_OPERATION_AUTORELOCK,
ATTR_OPERATION_KEYPAD,
ATTR_OPERATION_MANUAL,
ATTR_OPERATION_METHOD,
ATTR_OPERATION_REMOTE,
ATTR_OPERATION_TAG,
OPERATION_METHOD_AUTORELOCK,
OPERATION_METHOD_KEYPAD,
OPERATION_METHOD_MANUAL,
OPERATION_METHOD_MOBILE_DEVICE,
OPERATION_METHOD_REMOTE,
OPERATION_METHOD_TAG,
)
from .entity import YaleDescriptionEntity, YaleEntityMixin
def _retrieve_device_battery_state(detail: LockDetail) -> int:
"""Get the latest state of the sensor."""
return detail.battery_level
def _retrieve_linked_keypad_battery_state(detail: KeypadDetail) -> int | None:
"""Get the latest state of the sensor."""
return detail.battery_percentage
_T = TypeVar("_T", LockDetail, KeypadDetail)
@dataclass(frozen=True, kw_only=True)
class YaleSensorEntityDescription(SensorEntityDescription, Generic[_T]):
"""Mixin for required keys."""
value_fn: Callable[[_T], int | None]
SENSOR_TYPE_DEVICE_BATTERY = YaleSensorEntityDescription[LockDetail](
key="device_battery",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
value_fn=_retrieve_device_battery_state,
)
SENSOR_TYPE_KEYPAD_BATTERY = YaleSensorEntityDescription[KeypadDetail](
key="linked_keypad_battery",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
value_fn=_retrieve_linked_keypad_battery_state,
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: YaleConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Yale sensors."""
data = config_entry.runtime_data
entities: list[SensorEntity] = []
for device in data.locks:
detail = data.get_device_detail(device.device_id)
entities.append(YaleOperatorSensor(data, device, "lock_operator"))
if SENSOR_TYPE_DEVICE_BATTERY.value_fn(detail):
entities.append(
YaleBatterySensor[LockDetail](data, device, SENSOR_TYPE_DEVICE_BATTERY)
)
if keypad := detail.keypad:
entities.append(
YaleBatterySensor[KeypadDetail](
data, keypad, SENSOR_TYPE_KEYPAD_BATTERY
)
)
entities.extend(
YaleBatterySensor[Doorbell](data, device, SENSOR_TYPE_DEVICE_BATTERY)
for device in data.doorbells
if SENSOR_TYPE_DEVICE_BATTERY.value_fn(data.get_device_detail(device.device_id))
)
async_add_entities(entities)
class YaleOperatorSensor(YaleEntityMixin, RestoreSensor):
"""Representation of an Yale lock operation sensor."""
_attr_translation_key = "operator"
_operated_remote: bool | None = None
_operated_keypad: bool | None = None
_operated_manual: bool | None = None
_operated_tag: bool | None = None
_operated_autorelock: bool | None = None
@callback
def _update_from_data(self) -> None:
"""Get the latest state of the sensor and update activity."""
self._attr_available = True
if lock_activity := self._get_latest({ActivityType.LOCK_OPERATION}):
lock_activity = cast(LockOperationActivity, lock_activity)
self._attr_native_value = lock_activity.operated_by
self._operated_remote = lock_activity.operated_remote
self._operated_keypad = lock_activity.operated_keypad
self._operated_manual = lock_activity.operated_manual
self._operated_tag = lock_activity.operated_tag
self._operated_autorelock = lock_activity.operated_autorelock
self._attr_entity_picture = lock_activity.operator_thumbnail_url
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the device specific state attributes."""
attributes: dict[str, Any] = {}
if self._operated_remote is not None:
attributes[ATTR_OPERATION_REMOTE] = self._operated_remote
if self._operated_keypad is not None:
attributes[ATTR_OPERATION_KEYPAD] = self._operated_keypad
if self._operated_manual is not None:
attributes[ATTR_OPERATION_MANUAL] = self._operated_manual
if self._operated_tag is not None:
attributes[ATTR_OPERATION_TAG] = self._operated_tag
if self._operated_autorelock is not None:
attributes[ATTR_OPERATION_AUTORELOCK] = self._operated_autorelock
if self._operated_remote:
attributes[ATTR_OPERATION_METHOD] = OPERATION_METHOD_REMOTE
elif self._operated_keypad:
attributes[ATTR_OPERATION_METHOD] = OPERATION_METHOD_KEYPAD
elif self._operated_manual:
attributes[ATTR_OPERATION_METHOD] = OPERATION_METHOD_MANUAL
elif self._operated_tag:
attributes[ATTR_OPERATION_METHOD] = OPERATION_METHOD_TAG
elif self._operated_autorelock:
attributes[ATTR_OPERATION_METHOD] = OPERATION_METHOD_AUTORELOCK
else:
attributes[ATTR_OPERATION_METHOD] = OPERATION_METHOD_MOBILE_DEVICE
return attributes
async def async_added_to_hass(self) -> None:
"""Restore ATTR_CHANGED_BY on startup since it is likely no longer in the activity log."""
await super().async_added_to_hass()
last_state = await self.async_get_last_state()
last_sensor_state = await self.async_get_last_sensor_data()
if (
not last_state
or not last_sensor_state
or last_state.state == STATE_UNAVAILABLE
):
return
self._attr_native_value = last_sensor_state.native_value
last_attrs = last_state.attributes
if ATTR_ENTITY_PICTURE in last_attrs:
self._attr_entity_picture = last_attrs[ATTR_ENTITY_PICTURE]
if ATTR_OPERATION_REMOTE in last_attrs:
self._operated_remote = last_attrs[ATTR_OPERATION_REMOTE]
if ATTR_OPERATION_KEYPAD in last_attrs:
self._operated_keypad = last_attrs[ATTR_OPERATION_KEYPAD]
if ATTR_OPERATION_MANUAL in last_attrs:
self._operated_manual = last_attrs[ATTR_OPERATION_MANUAL]
if ATTR_OPERATION_TAG in last_attrs:
self._operated_tag = last_attrs[ATTR_OPERATION_TAG]
if ATTR_OPERATION_AUTORELOCK in last_attrs:
self._operated_autorelock = last_attrs[ATTR_OPERATION_AUTORELOCK]
class YaleBatterySensor(YaleDescriptionEntity, SensorEntity, Generic[_T]):
"""Representation of an Yale sensor."""
entity_description: YaleSensorEntityDescription[_T]
_attr_device_class = SensorDeviceClass.BATTERY
_attr_native_unit_of_measurement = PERCENTAGE
@callback
def _update_from_data(self) -> None:
"""Get the latest state of the sensor."""
self._attr_native_value = self.entity_description.value_fn(self._detail)
self._attr_available = self._attr_native_value is not None

View File

@ -0,0 +1,71 @@
{
"config": {
"step": {
"pick_implementation": {
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
"reauth_invalid_user": "Reauthenticate must use the same account."
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
}
},
"entity": {
"binary_sensor": {
"ding": {
"name": "Doorbell ding"
},
"image_capture": {
"name": "Image capture"
}
},
"button": {
"wake": {
"name": "Wake"
}
},
"camera": {
"camera": {
"name": "[%key:component::camera::title%]"
}
},
"sensor": {
"operator": {
"name": "Operator"
}
},
"event": {
"doorbell": {
"state_attributes": {
"event_type": {
"state": {
"ring": "Ring"
}
}
}
},
"motion": {
"state_attributes": {
"event_type": {
"state": {
"motion": "Motion"
}
}
}
}
}
}
}

View File

@ -0,0 +1,83 @@
"""Yale util functions."""
from __future__ import annotations
from datetime import datetime, timedelta
from functools import partial
import aiohttp
from yalexs.activity import ACTION_DOORBELL_CALL_MISSED, Activity, ActivityType
from yalexs.doorbell import DoorbellDetail
from yalexs.lock import LockDetail
from yalexs.manager.const import ACTIVITY_UPDATE_INTERVAL
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import aiohttp_client
from . import YaleData
TIME_TO_DECLARE_DETECTION = timedelta(seconds=ACTIVITY_UPDATE_INTERVAL.total_seconds())
@callback
def async_create_yale_clientsession(hass: HomeAssistant) -> aiohttp.ClientSession:
"""Create an aiohttp session for the yale integration."""
# Create an aiohttp session instead of using the default one since the
# default one is likely to trigger yale's WAF if another integration
# is also using Cloudflare
return aiohttp_client.async_create_clientsession(hass)
def retrieve_time_based_activity(
activities: set[ActivityType], data: YaleData, detail: DoorbellDetail | LockDetail
) -> Activity | None:
"""Get the latest state of the sensor."""
stream = data.activity_stream
if latest := stream.get_latest_device_activity(detail.device_id, activities):
return _activity_time_based(latest)
return False
_RING_ACTIVITIES = {ActivityType.DOORBELL_DING}
def retrieve_ding_activity(
data: YaleData, detail: DoorbellDetail | LockDetail
) -> Activity | None:
"""Get the ring/ding state."""
stream = data.activity_stream
latest = stream.get_latest_device_activity(detail.device_id, _RING_ACTIVITIES)
if latest is None or (
data.push_updates_connected and latest.action == ACTION_DOORBELL_CALL_MISSED
):
return None
return _activity_time_based(latest)
retrieve_doorbell_motion_activity = partial(
retrieve_time_based_activity, {ActivityType.DOORBELL_MOTION}
)
def _activity_time_based(latest: Activity) -> Activity | None:
"""Get the latest state of the sensor."""
start = latest.activity_start_time
end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
if start <= _native_datetime() <= end:
return latest
return None
def _native_datetime() -> datetime:
"""Return time in the format yale uses without timezone."""
return datetime.now()
def retrieve_online_state(data: YaleData, detail: DoorbellDetail | LockDetail) -> bool:
"""Get the latest state of the sensor."""
# The doorbell will go into standby mode when there is no motion
# for a short while. It will wake by itself when needed so we need
# to consider is available or we will not report motion or dings
if isinstance(detail, DoorbellDetail):
return detail.is_online or detail.is_standby
return detail.bridge_is_online

View File

@ -29,6 +29,7 @@ APPLICATION_CREDENTIALS = [
"twitch",
"withings",
"xbox",
"yale",
"yolink",
"youtube",
]

View File

@ -664,6 +664,7 @@ FLOWS = {
"xiaomi_aqara",
"xiaomi_ble",
"xiaomi_miio",
"yale",
"yale_smart_alarm",
"yalexs_ble",
"yamaha_musiccast",

View File

@ -12,11 +12,6 @@ DHCP: Final[list[dict[str, str | bool]]] = [
"domain": "airzone",
"macaddress": "E84F25*",
},
{
"domain": "august",
"hostname": "yale-connect-plus",
"macaddress": "00177A*",
},
{
"domain": "august",
"hostname": "connect",
@ -1094,6 +1089,11 @@ DHCP: Final[list[dict[str, str | bool]]] = [
"domain": "wiz",
"hostname": "wiz_*",
},
{
"domain": "yale",
"hostname": "yale-connect-plus",
"macaddress": "00177A*",
},
{
"domain": "yeelight",
"hostname": "yeelink-*",

View File

@ -7007,6 +7007,12 @@
"config_flow": false,
"supported_by": "august",
"name": "Yale Home"
},
"yale": {
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_push",
"name": "Yale"
}
}
},

View File

@ -2967,10 +2967,12 @@ xs1-api-client==3.0.0
yalesmartalarmclient==0.4.0
# homeassistant.components.august
# homeassistant.components.yale
# homeassistant.components.yalexs_ble
yalexs-ble==2.4.3
# homeassistant.components.august
# homeassistant.components.yale
yalexs==8.5.4
# homeassistant.components.yeelight

View File

@ -2350,10 +2350,12 @@ xmltodict==0.13.0
yalesmartalarmclient==0.4.0
# homeassistant.components.august
# homeassistant.components.yale
# homeassistant.components.yalexs_ble
yalexs-ble==2.4.3
# homeassistant.components.august
# homeassistant.components.yale
yalexs==8.5.4
# homeassistant.components.yeelight

View File

@ -0,0 +1,12 @@
"""Tests for the yale component."""
MOCK_CONFIG_ENTRY_DATA = {
"auth_implementation": "cloud",
"token": {
"access_token": "access_token",
"expires_in": 1,
"refresh_token": "refresh_token",
"expires_at": 2,
"service": "yale",
},
}

View File

@ -0,0 +1,59 @@
"""Yale tests conftest."""
from unittest.mock import patch
import pytest
from yalexs.manager.ratelimit import _RateLimitChecker
from homeassistant.components.yale.const import DOMAIN
from homeassistant.core import HomeAssistant
from .mocks import mock_client_credentials, mock_config_entry
from tests.common import MockConfigEntry, load_fixture
@pytest.fixture(name="mock_discovery", autouse=True)
def mock_discovery_fixture():
"""Mock discovery to avoid loading the whole bluetooth stack."""
with patch(
"homeassistant.components.yale.data.discovery_flow.async_create_flow"
) as mock_discovery:
yield mock_discovery
@pytest.fixture(name="disable_ratelimit_checks", autouse=True)
def disable_ratelimit_checks_fixture():
"""Disable rate limit checks."""
with patch.object(_RateLimitChecker, "register_wakeup"):
yield
@pytest.fixture(name="mock_config_entry")
def mock_config_entry_fixture(jwt: str) -> MockConfigEntry:
"""Return the default mocked config entry."""
return mock_config_entry(jwt=jwt)
@pytest.fixture(name="jwt")
def load_jwt_fixture() -> str:
"""Load Fixture data."""
return load_fixture("jwt", DOMAIN).strip("\n")
@pytest.fixture(name="reauth_jwt")
def load_reauth_jwt_fixture() -> str:
"""Load Fixture data."""
return load_fixture("reauth_jwt", DOMAIN).strip("\n")
@pytest.fixture(name="reauth_jwt_wrong_account")
def load_reauth_jwt_wrong_account_fixture() -> str:
"""Load Fixture data."""
return load_fixture("reauth_jwt_wrong_account", DOMAIN).strip("\n")
@pytest.fixture(name="client_credentials", autouse=True)
async def mock_client_credentials_fixture(hass: HomeAssistant) -> None:
"""Mock client credentials."""
await mock_client_credentials(hass)

View File

@ -0,0 +1,36 @@
[
{
"entities": {
"activity": "mockActivity2",
"house": "123",
"device": "online_with_doorsense",
"callingUser": "mockUserId2",
"otherUser": "deleted"
},
"callingUser": {
"LastName": "elven princess",
"UserID": "mockUserId2",
"FirstName": "Your favorite"
},
"otherUser": {
"LastName": "User",
"UserName": "deleteduser",
"FirstName": "Unknown",
"UserID": "deleted",
"PhoneNo": "deleted"
},
"deviceType": "lock",
"deviceName": "MockHouseTDoor",
"action": "associated_bridge_offline",
"dateTime": 1582007218000,
"info": {
"remote": true,
"DateLogActionID": "ABC+Time"
},
"deviceID": "online_with_doorsense",
"house": {
"houseName": "MockHouse",
"houseID": "123"
}
}
]

View File

@ -0,0 +1,36 @@
[
{
"entities": {
"activity": "mockActivity2",
"house": "123",
"device": "online_with_doorsense",
"callingUser": "mockUserId2",
"otherUser": "deleted"
},
"callingUser": {
"LastName": "elven princess",
"UserID": "mockUserId2",
"FirstName": "Your favorite"
},
"otherUser": {
"LastName": "User",
"UserName": "deleteduser",
"FirstName": "Unknown",
"UserID": "deleted",
"PhoneNo": "deleted"
},
"deviceType": "lock",
"deviceName": "MockHouseTDoor",
"action": "associated_bridge_online",
"dateTime": 1582007218000,
"info": {
"remote": true,
"DateLogActionID": "ABC+Time"
},
"deviceID": "online_with_doorsense",
"house": {
"houseName": "MockHouse",
"houseID": "123"
}
}
]

View File

@ -0,0 +1,58 @@
[
{
"otherUser": {
"FirstName": "Unknown",
"UserName": "deleteduser",
"LastName": "User",
"UserID": "deleted",
"PhoneNo": "deleted"
},
"dateTime": 1582663119959,
"deviceID": "K98GiDT45GUL",
"info": {
"videoUploadProgress": "in_progress",
"image": {
"resource_type": "image",
"etag": "fdsf",
"created_at": "2020-02-25T20:38:39Z",
"type": "upload",
"format": "jpg",
"version": 1582663119,
"secure_url": "https://res.cloudinary.com/updated_image.jpg",
"signature": "fdfdfd",
"url": "http://res.cloudinary.com/updated_image.jpg",
"bytes": 48545,
"placeholder": false,
"original_filename": "file",
"width": 720,
"tags": [],
"public_id": "xnsj5gphpzij9brifpf4",
"height": 576
},
"dvrID": "dvr",
"videoAvailable": false,
"hasSubscription": false
},
"callingUser": {
"LastName": "User",
"UserName": "deleteduser",
"FirstName": "Unknown",
"UserID": "deleted",
"PhoneNo": "deleted"
},
"house": {
"houseName": "K98GiDT45GUL",
"houseID": "na"
},
"action": "doorbell_motion_detected",
"deviceType": "doorbell",
"entities": {
"otherUser": "deleted",
"house": "na",
"device": "K98GiDT45GUL",
"activity": "de5585cfd4eae900bb5ba3dc",
"callingUser": "deleted"
},
"deviceName": "Front Door"
}
]

View File

@ -0,0 +1,36 @@
[
{
"entities": {
"activity": "mockActivity2",
"house": "123",
"device": "online_with_doorsense",
"callingUser": "mockUserId2",
"otherUser": "deleted"
},
"callingUser": {
"LastName": "elven princess",
"UserID": "mockUserId2",
"FirstName": "Your favorite"
},
"otherUser": {
"LastName": "User",
"UserName": "deleteduser",
"FirstName": "Unknown",
"UserID": "deleted",
"PhoneNo": "deleted"
},
"deviceType": "lock",
"deviceName": "MockHouseTDoor",
"action": "jammed",
"dateTime": 1582007218000,
"info": {
"remote": true,
"DateLogActionID": "ABC+Time"
},
"deviceID": "online_with_doorsense",
"house": {
"houseName": "MockHouse",
"houseID": "123"
}
}
]

View File

@ -0,0 +1,36 @@
[
{
"entities": {
"activity": "mockActivity2",
"house": "123",
"device": "online_with_doorsense",
"callingUser": "mockUserId2",
"otherUser": "deleted"
},
"callingUser": {
"LastName": "elven princess",
"UserID": "mockUserId2",
"FirstName": "Your favorite"
},
"otherUser": {
"LastName": "User",
"UserName": "deleteduser",
"FirstName": "Unknown",
"UserID": "deleted",
"PhoneNo": "deleted"
},
"deviceType": "lock",
"deviceName": "MockHouseTDoor",
"action": "lock",
"dateTime": 1582007218000,
"info": {
"remote": true,
"DateLogActionID": "ABC+Time"
},
"deviceID": "online_with_doorsense",
"house": {
"houseName": "MockHouse",
"houseID": "123"
}
}
]

View File

@ -0,0 +1,36 @@
[
{
"entities": {
"activity": "mockActivity2",
"house": "123",
"device": "online_with_doorsense",
"callingUser": "mockUserId2",
"otherUser": "deleted"
},
"callingUser": {
"LastName": "Relock",
"UserID": "automaticrelock",
"FirstName": "Auto"
},
"otherUser": {
"LastName": "User",
"UserName": "deleteduser",
"FirstName": "Unknown",
"UserID": "deleted",
"PhoneNo": "deleted"
},
"deviceType": "lock",
"deviceName": "MockHouseTDoor",
"action": "lock",
"dateTime": 1582007218000,
"info": {
"remote": false,
"DateLogActionID": "ABC+Time"
},
"deviceID": "online_with_doorsense",
"house": {
"houseName": "MockHouse",
"houseID": "123"
}
}
]

View File

@ -0,0 +1,36 @@
[
{
"entities": {
"activity": "mockActivity2",
"house": "123",
"device": "online_with_doorsense",
"callingUser": "mockUserId2",
"otherUser": "deleted"
},
"callingUser": {
"LastName": "elven princess",
"UserID": "mockUserId2",
"FirstName": "Your favorite"
},
"otherUser": {
"LastName": "User",
"UserName": "deleteduser",
"FirstName": "Unknown",
"UserID": "deleted",
"PhoneNo": "deleted"
},
"deviceType": "lock",
"deviceName": "MockHouseTDoor",
"action": "lock",
"dateTime": 1582007218000,
"info": {
"remote": false,
"DateLogActionID": "ABC+Time"
},
"deviceID": "online_with_doorsense",
"house": {
"houseName": "MockHouse",
"houseID": "123"
}
}
]

View File

@ -0,0 +1,37 @@
[
{
"entities": {
"activity": "mockActivity2",
"house": "123",
"device": "online_with_doorsense",
"callingUser": "mockUserId2",
"otherUser": "deleted"
},
"callingUser": {
"LastName": "elven princess",
"UserID": "mockUserId2",
"FirstName": "Your favorite"
},
"otherUser": {
"LastName": "User",
"UserName": "deleteduser",
"FirstName": "Unknown",
"UserID": "deleted",
"PhoneNo": "deleted"
},
"deviceType": "lock",
"deviceName": "MockHouseTDoor",
"action": "lock",
"dateTime": 1582007218000,
"info": {
"remote": false,
"keypad": true,
"DateLogActionID": "ABC+Time"
},
"deviceID": "online_with_doorsense",
"house": {
"houseName": "MockHouse",
"houseID": "123"
}
}
]

View File

@ -0,0 +1,39 @@
[
{
"entities": {
"activity": "mockActivity2",
"house": "123",
"device": "online_with_doorsense",
"callingUser": "mockUserId2",
"otherUser": "deleted"
},
"callingUser": {
"LastName": "elven princess",
"UserID": "mockUserId2",
"FirstName": "Your favorite"
},
"otherUser": {
"LastName": "User",
"UserName": "deleteduser",
"FirstName": "Unknown",
"UserID": "deleted",
"PhoneNo": "deleted"
},
"deviceType": "lock",
"deviceName": "MockHouseTDoor",
"action": "lock",
"dateTime": 1582007218000,
"info": {
"remote": false,
"keypad": false,
"manual": true,
"tag": false,
"DateLogActionID": "ABC+Time"
},
"deviceID": "online_with_doorsense",
"house": {
"houseName": "MockHouse",
"houseID": "123"
}
}
]

View File

@ -0,0 +1,36 @@
[
{
"entities": {
"activity": "mockActivity2",
"house": "123",
"device": "online_with_doorsense",
"callingUser": "mockUserId2",
"otherUser": "deleted"
},
"callingUser": {
"LastName": "elven princess",
"UserID": "mockUserId2",
"FirstName": "Your favorite"
},
"otherUser": {
"LastName": "User",
"UserName": "deleteduser",
"FirstName": "Unknown",
"UserID": "deleted",
"PhoneNo": "deleted"
},
"deviceType": "lock",
"deviceName": "MockHouseTDoor",
"action": "locking",
"dateTime": 1582007218000,
"info": {
"remote": true,
"DateLogActionID": "ABC+Time"
},
"deviceID": "online_with_doorsense",
"house": {
"houseName": "MockHouse",
"houseID": "123"
}
}
]

View File

@ -0,0 +1,39 @@
[
{
"entities": {
"activity": "mockActivity2",
"house": "123",
"device": "online_with_doorsense",
"callingUser": "mockUserId2",
"otherUser": "deleted"
},
"callingUser": {
"LastName": "elven princess",
"UserID": "mockUserId2",
"FirstName": "Your favorite"
},
"otherUser": {
"LastName": "User",
"UserName": "deleteduser",
"FirstName": "Unknown",
"UserID": "deleted",
"PhoneNo": "deleted"
},
"deviceType": "lock",
"deviceName": "MockHouseTDoor",
"action": "unlock",
"dateTime": 1582007218000,
"info": {
"remote": false,
"keypad": false,
"manual": true,
"tag": false,
"DateLogActionID": "ABC+Time"
},
"deviceID": "online_with_doorsense",
"house": {
"houseName": "MockHouse",
"houseID": "123"
}
}
]

View File

@ -0,0 +1,39 @@
[
{
"entities": {
"activity": "mockActivity2",
"house": "123",
"device": "online_with_doorsense",
"callingUser": "mockUserId2",
"otherUser": "deleted"
},
"callingUser": {
"LastName": "elven princess",
"UserID": "mockUserId2",
"FirstName": "Your favorite"
},
"otherUser": {
"LastName": "User",
"UserName": "deleteduser",
"FirstName": "Unknown",
"UserID": "deleted",
"PhoneNo": "deleted"
},
"deviceType": "lock",
"deviceName": "MockHouseTDoor",
"action": "unlock",
"dateTime": 1582007218000,
"info": {
"remote": false,
"keypad": false,
"manual": false,
"tag": true,
"DateLogActionID": "ABC+Time"
},
"deviceID": "online_with_doorsense",
"house": {
"houseName": "MockHouse",
"houseID": "123"
}
}
]

View File

@ -0,0 +1,36 @@
[
{
"entities": {
"activity": "mockActivity2",
"house": "123",
"device": "online_with_doorsense",
"callingUser": "mockUserId2",
"otherUser": "deleted"
},
"callingUser": {
"LastName": "elven princess",
"UserID": "mockUserId2",
"FirstName": "Your favorite"
},
"otherUser": {
"LastName": "User",
"UserName": "deleteduser",
"FirstName": "Unknown",
"UserID": "deleted",
"PhoneNo": "deleted"
},
"deviceType": "lock",
"deviceName": "MockHouseTDoor",
"action": "unlocking",
"dateTime": 1582007218000,
"info": {
"remote": true,
"DateLogActionID": "ABC+Time"
},
"deviceID": "online_with_doorsense",
"house": {
"houseName": "MockHouse",
"houseID": "123"
}
}
]

View File

@ -0,0 +1,81 @@
{
"status_timestamp": 1512811834532,
"appID": "august-iphone",
"LockID": "BBBB1F5F11114C24CCCC97571DD6AAAA",
"recentImage": {
"original_filename": "file",
"placeholder": false,
"bytes": 24476,
"height": 640,
"format": "jpg",
"width": 480,
"version": 1512892814,
"resource_type": "image",
"etag": "54966926be2e93f77d498a55f247661f",
"tags": [],
"public_id": "qqqqt4ctmxwsysylaaaa",
"url": "http://image.com/vmk16naaaa7ibuey7sar.jpg",
"created_at": "2017-12-10T08:01:35Z",
"signature": "75z47ca21b5e8ffda21d2134e478a2307c4625da",
"secure_url": "https://image.com/vmk16naaaa7ibuey7sar.jpg",
"type": "upload"
},
"settings": {
"keepEncoderRunning": true,
"videoResolution": "640x480",
"minACNoScaling": 40,
"irConfiguration": 8448272,
"directLink": true,
"overlayEnabled": true,
"notify_when_offline": true,
"micVolume": 100,
"bitrateCeiling": 512000,
"initialBitrate": 384000,
"IVAEnabled": false,
"turnOffCamera": false,
"ringSoundEnabled": true,
"JPGQuality": 70,
"motion_notifications": true,
"speakerVolume": 92,
"buttonpush_notifications": true,
"ABREnabled": true,
"debug": false,
"batteryLowThreshold": 3.1,
"batteryRun": false,
"IREnabled": true,
"batteryUseThreshold": 3.4
},
"doorbellServerURL": "https://doorbells.august.com",
"name": "Front Door",
"createdAt": "2016-11-26T22:27:11.176Z",
"installDate": "2016-11-26T22:27:11.176Z",
"serialNumber": "tBXZR0Z35E",
"dvrSubscriptionSetupDone": true,
"caps": ["reconnect"],
"doorbellID": "K98GiDT45GUL",
"HouseID": "mockhouseid1",
"telemetry": {
"signal_level": -56,
"date": "2017-12-10 08:05:12",
"battery_soc": 96,
"battery": 4.061763,
"steady_ac_in": 22.196405,
"BSSID": "88:ee:00:dd:aa:11",
"SSID": "foo_ssid",
"updated_at": "2017-12-10T08:05:13.650Z",
"temperature": 28.25,
"wifi_freq": 5745,
"load_average": "0.50 0.47 0.35 1/154 9345",
"link_quality": 54,
"battery_soh": 95,
"uptime": "16168.75 13830.49",
"ip_addr": "10.0.1.11",
"doorbell_low_battery": false,
"ac_in": 23.856874
},
"installUserID": "c3b2a94e-373e-aaaa-bbbb-36e996827777",
"status": "doorbell_call_status_online",
"firmwareVersion": "2.3.0-RC153+201711151527",
"pubsubChannel": "7c7a6672-59c8-3333-ffff-dcd98705cccc",
"updatedAt": "2017-12-10T08:05:13.650Z"
}

View File

@ -0,0 +1,78 @@
{
"status_timestamp": 1512811834532,
"appID": "august-iphone",
"LockID": "BBBB1F5F11114C24CCCC97571DD6AAAA",
"recentImage": {
"original_filename": "file",
"placeholder": false,
"bytes": 24476,
"height": 640,
"format": "jpg",
"width": 480,
"version": 1512892814,
"resource_type": "image",
"etag": "54966926be2e93f77d498a55f247661f",
"tags": [],
"public_id": "qqqqt4ctmxwsysylaaaa",
"url": "http://image.com/vmk16naaaa7ibuey7sar.jpg",
"created_at": "2017-12-10T08:01:35Z",
"signature": "75z47ca21b5e8ffda21d2134e478a2307c4625da",
"secure_url": "https://image.com/vmk16naaaa7ibuey7sar.jpg",
"type": "upload"
},
"settings": {
"keepEncoderRunning": true,
"videoResolution": "640x480",
"minACNoScaling": 40,
"irConfiguration": 8448272,
"directLink": true,
"overlayEnabled": true,
"notify_when_offline": true,
"micVolume": 100,
"bitrateCeiling": 512000,
"initialBitrate": 384000,
"IVAEnabled": false,
"turnOffCamera": false,
"ringSoundEnabled": true,
"JPGQuality": 70,
"motion_notifications": true,
"speakerVolume": 92,
"buttonpush_notifications": true,
"ABREnabled": true,
"debug": false,
"batteryLowThreshold": 3.1,
"batteryRun": false,
"IREnabled": true,
"batteryUseThreshold": 3.4
},
"doorbellServerURL": "https://doorbells.august.com",
"name": "Front Door",
"createdAt": "2016-11-26T22:27:11.176Z",
"installDate": "2016-11-26T22:27:11.176Z",
"serialNumber": "tBXZR0Z35E",
"dvrSubscriptionSetupDone": true,
"caps": ["reconnect"],
"doorbellID": "K98GiDT45GUL",
"HouseID": "3dd2accaea08",
"telemetry": {
"signal_level": -56,
"date": "2017-12-10 08:05:12",
"steady_ac_in": 22.196405,
"BSSID": "88:ee:00:dd:aa:11",
"SSID": "foo_ssid",
"updated_at": "2017-12-10T08:05:13.650Z",
"temperature": 28.25,
"wifi_freq": 5745,
"load_average": "0.50 0.47 0.35 1/154 9345",
"link_quality": 54,
"uptime": "16168.75 13830.49",
"ip_addr": "10.0.1.11",
"doorbell_low_battery": false,
"ac_in": 23.856874
},
"installUserID": "c3b2a94e-373e-aaaa-bbbb-36e996827777",
"status": "doorbell_call_status_online",
"firmwareVersion": "2.3.0-RC153+201711151527",
"pubsubChannel": "7c7a6672-59c8-3333-ffff-dcd98705cccc",
"updatedAt": "2017-12-10T08:05:13.650Z"
}

View File

@ -0,0 +1,126 @@
{
"recentImage": {
"tags": [],
"height": 576,
"public_id": "fdsfds",
"bytes": 50013,
"resource_type": "image",
"original_filename": "file",
"version": 1582242766,
"format": "jpg",
"signature": "fdsfdsf",
"created_at": "2020-02-20T23:52:46Z",
"type": "upload",
"placeholder": false,
"url": "http://res.cloudinary.com/august-com/image/upload/ccc/ccccc.jpg",
"secure_url": "https://res.cloudinary.com/august-com/image/upload/cc/cccc.jpg",
"etag": "zds",
"width": 720
},
"firmwareVersion": "3.1.0-HYDRC75+201909251139",
"doorbellServerURL": "https://doorbells.august.com",
"installUserID": "mock",
"caps": ["reconnect", "webrtc", "tcp_wakeup"],
"messagingProtocol": "pubnub",
"createdAt": "2020-02-12T03:52:28.719Z",
"invitations": [],
"appID": "august-iphone-v5",
"HouseID": "houseid1",
"doorbellID": "tmt100",
"name": "Front Door",
"settings": {
"batteryUseThreshold": 3.4,
"brightness": 50,
"batteryChargeCurrent": 60,
"overCurrentThreshold": -250,
"irLedBrightness": 40,
"videoResolution": "720x576",
"pirPulseCounter": 1,
"contrast": 50,
"micVolume": 50,
"directLink": true,
"auto_contrast_mode": 0,
"saturation": 50,
"motion_notifications": true,
"pirSensitivity": 20,
"pirBlindTime": 7,
"notify_when_offline": false,
"nightModeAlsThreshold": 10,
"minACNoScaling": 40,
"DVRRecordingTimeout": 15,
"turnOffCamera": false,
"debug": false,
"keepEncoderRunning": true,
"pirWindowTime": 0,
"bitrateCeiling": 2000000,
"backlight_comp": false,
"buttonpush_notifications": true,
"buttonpush_notifications_partners": false,
"minimumSnapshotInterval": 30,
"pirConfiguration": 272,
"batteryLowThreshold": 3.1,
"sharpness": 50,
"ABREnabled": true,
"hue": 50,
"initialBitrate": 1000000,
"ringSoundEnabled": true,
"IVAEnabled": false,
"overlayEnabled": true,
"speakerVolume": 92,
"ringRepetitions": 3,
"powerProfilePreset": -1,
"irConfiguration": 16836880,
"JPGQuality": 70,
"IREnabled": true
},
"updatedAt": "2020-02-20T23:58:21.580Z",
"serialNumber": "abc",
"installDate": "2019-02-12T03:52:28.719Z",
"dvrSubscriptionSetupDone": true,
"pubsubChannel": "mock",
"chimes": [
{
"updatedAt": "2020-02-12T03:55:38.805Z",
"_id": "cccc",
"type": 1,
"serialNumber": "ccccc",
"doorbellID": "tmt100",
"name": "Living Room",
"chimeID": "cccc",
"createdAt": "2020-02-12T03:55:38.805Z",
"firmware": "3.1.16"
}
],
"telemetry": {
"battery": 3.985,
"battery_soc": 81,
"load_average": "0.45 0.18 0.07 4/98 831",
"ip_addr": "192.168.100.174",
"BSSID": "snp",
"uptime": "96.55 70.59",
"SSID": "bob",
"updated_at": "2020-02-20T23:53:09.586Z",
"dtim_period": 0,
"wifi_freq": 2462,
"date": "2020-02-20 11:47:36",
"BSSIDManufacturer": "Ubiquiti - Ubiquiti Networks Inc.",
"battery_temp": 22,
"battery_avg_cur": -291,
"beacon_interval": 0,
"signal_level": -49,
"battery_soh": 95,
"doorbell_low_battery": false
},
"secChipCertSerial": "",
"tcpKeepAlive": {
"keepAliveUUID": "mock",
"wakeUp": {
"token": "wakemeup",
"lastUpdated": 1582242723931
}
},
"statusUpdatedAtMs": 1582243101579,
"status": "doorbell_offline",
"type": "hydra1",
"HouseName": "housename"
}

View File

@ -0,0 +1,92 @@
{
"LockName": "Front Door Lock",
"Type": 2,
"Created": "2017-12-10T03:12:09.210Z",
"Updated": "2017-12-10T03:12:09.210Z",
"LockID": "A6697750D607098BAE8D6BAA11EF8063",
"HouseID": "000000000000",
"HouseName": "My House",
"Calibrated": false,
"skuNumber": "AUG-SL02-M02-S02",
"timeZone": "America/Vancouver",
"battery": 0.88,
"SerialNumber": "X2FSW05DGA",
"LockStatus": {
"status": "locked",
"doorState": "init",
"dateTime": "2017-12-10T04:48:30.272Z",
"isLockStatusChanged": false,
"valid": true
},
"currentFirmwareVersion": "109717e9-3.0.44-3.0.30",
"homeKitEnabled": false,
"zWaveEnabled": false,
"isGalileo": false,
"Bridge": {
"_id": "aaacab87f7efxa0015884999",
"mfgBridgeID": "AAGPP102XX",
"deviceModel": "august-doorbell",
"firmwareVersion": "2.3.0-RC153+201711151527",
"operative": true
},
"keypad": {
"_id": "5bc65c24e6ef2a263e1450a8",
"serialNumber": "K1GXB0054Z",
"lockID": "92412D1B44004595B5DEB134E151A8D3",
"currentFirmwareVersion": "2.27.0",
"battery": {},
"batteryLevel": "Medium",
"batteryRaw": 170
},
"OfflineKeys": {
"created": [],
"loaded": [],
"deleted": [],
"loadedhk": [
{
"key": "kkk01d4300c1dcxxx1c330f794941222",
"slot": 256,
"UserID": "cccca94e-373e-aaaa-bbbb-333396827777",
"created": "2017-12-10T03:12:09.218Z",
"loaded": "2017-12-10T03:12:55.563Z"
}
]
},
"parametersToSet": {},
"users": {
"cccca94e-373e-aaaa-bbbb-333396827777": {
"UserType": "superuser",
"FirstName": "Foo",
"LastName": "Bar",
"identifiers": ["email:foo@bar.com", "phone:+177777777777"],
"imageInfo": {
"original": {
"width": 948,
"height": 949,
"format": "jpg",
"url": "http://www.image.com/foo.jpeg",
"secure_url": "https://www.image.com/foo.jpeg"
},
"thumbnail": {
"width": 128,
"height": 128,
"format": "jpg",
"url": "http://www.image.com/foo.jpeg",
"secure_url": "https://www.image.com/foo.jpeg"
}
}
}
},
"pubsubChannel": "3333a674-ffff-aaaa-b351-b3a4473f3333",
"ruleHash": {},
"cameras": [],
"geofenceLimits": {
"ios": {
"debounceInterval": 90,
"gpsAccuracyMultiplier": 2.5,
"maximumGeofence": 5000,
"minimumGeofence": 100,
"minGPSAccuracyRequired": 80
}
}
}

View File

@ -0,0 +1,92 @@
{
"LockName": "Front Door Lock",
"Type": 2,
"Created": "2017-12-10T03:12:09.210Z",
"Updated": "2017-12-10T03:12:09.210Z",
"LockID": "A6697750D607098BAE8D6BAA11EF8063",
"HouseID": "000000000000",
"HouseName": "My House",
"Calibrated": false,
"skuNumber": "AUG-SL02-M02-S02",
"timeZone": "America/Vancouver",
"battery": 0.88,
"SerialNumber": "X2FSW05DGA",
"LockStatus": {
"status": "locked",
"doorState": "closed",
"dateTime": "2017-12-10T04:48:30.272Z",
"isLockStatusChanged": true,
"valid": true
},
"currentFirmwareVersion": "109717e9-3.0.44-3.0.30",
"homeKitEnabled": false,
"zWaveEnabled": false,
"isGalileo": false,
"Bridge": {
"_id": "aaacab87f7efxa0015884999",
"mfgBridgeID": "AAGPP102XX",
"deviceModel": "august-doorbell",
"firmwareVersion": "2.3.0-RC153+201711151527",
"operative": true
},
"keypad": {
"_id": "5bc65c24e6ef2a263e1450a8",
"serialNumber": "K1GXB0054Z",
"lockID": "92412D1B44004595B5DEB134E151A8D3",
"currentFirmwareVersion": "2.27.0",
"battery": {},
"batteryLevel": "Low",
"batteryRaw": 128
},
"OfflineKeys": {
"created": [],
"loaded": [],
"deleted": [],
"loadedhk": [
{
"key": "kkk01d4300c1dcxxx1c330f794941222",
"slot": 256,
"UserID": "cccca94e-373e-aaaa-bbbb-333396827777",
"created": "2017-12-10T03:12:09.218Z",
"loaded": "2017-12-10T03:12:55.563Z"
}
]
},
"parametersToSet": {},
"users": {
"cccca94e-373e-aaaa-bbbb-333396827777": {
"UserType": "superuser",
"FirstName": "Foo",
"LastName": "Bar",
"identifiers": ["email:foo@bar.com", "phone:+177777777777"],
"imageInfo": {
"original": {
"width": 948,
"height": 949,
"format": "jpg",
"url": "http://www.image.com/foo.jpeg",
"secure_url": "https://www.image.com/foo.jpeg"
},
"thumbnail": {
"width": 128,
"height": 128,
"format": "jpg",
"url": "http://www.image.com/foo.jpeg",
"secure_url": "https://www.image.com/foo.jpeg"
}
}
}
},
"pubsubChannel": "3333a674-ffff-aaaa-b351-b3a4473f3333",
"ruleHash": {},
"cameras": [],
"geofenceLimits": {
"ios": {
"debounceInterval": 90,
"gpsAccuracyMultiplier": 2.5,
"maximumGeofence": 5000,
"minimumGeofence": 100,
"minGPSAccuracyRequired": 80
}
}
}

View File

@ -0,0 +1,57 @@
{
"Calibrated": false,
"Created": "2000-00-00T00:00:00.447Z",
"HouseID": "houseid",
"HouseName": "MockName",
"LockID": "ABC",
"LockName": "Test",
"LockStatus": {
"status": "unknown"
},
"OfflineKeys": {
"created": [],
"createdhk": [
{
"UserID": "mock-user-id",
"created": "2000-00-00T00:00:00.447Z",
"key": "mockkey",
"slot": 12
}
],
"deleted": [],
"loaded": []
},
"SerialNumber": "ABC",
"Type": 3,
"Updated": "2000-00-00T00:00:00.447Z",
"battery": -1,
"cameras": [],
"currentFirmwareVersion": "undefined-1.59.0-1.13.2",
"geofenceLimits": {
"ios": {
"debounceInterval": 90,
"gpsAccuracyMultiplier": 2.5,
"maximumGeofence": 5000,
"minGPSAccuracyRequired": 80,
"minimumGeofence": 100
}
},
"homeKitEnabled": false,
"isGalileo": false,
"macAddress": "a:b:c",
"parametersToSet": {},
"pubsubChannel": "mockpubsub",
"ruleHash": {},
"skuNumber": "AUG-X",
"supportsEntryCodes": false,
"users": {
"mockuserid": {
"FirstName": "MockName",
"LastName": "House",
"UserType": "superuser",
"identifiers": ["phone:+15558675309", "email:mockme@mock.org"]
}
},
"zWaveDSK": "1-2-3-4",
"zWaveEnabled": true
}

View File

@ -0,0 +1,92 @@
{
"LockName": "Front Door Lock",
"Type": 2,
"Created": "2017-12-10T03:12:09.210Z",
"Updated": "2017-12-10T03:12:09.210Z",
"LockID": "A6697750D607098BAE8D6BAA11EF8063",
"HouseID": "000000000000",
"HouseName": "My House",
"Calibrated": false,
"skuNumber": "AUG-SL02-M02-S02",
"timeZone": "America/Vancouver",
"battery": 0.88,
"SerialNumber": "X2FSW05DGA",
"LockStatus": {
"status": "locked",
"doorState": "closed",
"dateTime": "2017-12-10T04:48:30.272Z",
"isLockStatusChanged": true,
"valid": true
},
"currentFirmwareVersion": "109717e9-3.0.44-3.0.30",
"homeKitEnabled": false,
"zWaveEnabled": false,
"isGalileo": false,
"Bridge": {
"_id": "aaacab87f7efxa0015884999",
"mfgBridgeID": "AAGPP102XX",
"deviceModel": "august-doorbell",
"firmwareVersion": "2.3.0-RC153+201711151527",
"operative": true
},
"keypad": {
"_id": "5bc65c24e6ef2a263e1450a8",
"serialNumber": "K1GXB0054Z",
"lockID": "92412D1B44004595B5DEB134E151A8D3",
"currentFirmwareVersion": "2.27.0",
"battery": {},
"batteryLevel": "Medium",
"batteryRaw": 170
},
"OfflineKeys": {
"created": [],
"loaded": [],
"deleted": [],
"loadedhk": [
{
"key": "kkk01d4300c1dcxxx1c330f794941222",
"slot": 256,
"UserID": "cccca94e-373e-aaaa-bbbb-333396827777",
"created": "2017-12-10T03:12:09.218Z",
"loaded": "2017-12-10T03:12:55.563Z"
}
]
},
"parametersToSet": {},
"users": {
"cccca94e-373e-aaaa-bbbb-333396827777": {
"UserType": "superuser",
"FirstName": "Foo",
"LastName": "Bar",
"identifiers": ["email:foo@bar.com", "phone:+177777777777"],
"imageInfo": {
"original": {
"width": 948,
"height": 949,
"format": "jpg",
"url": "http://www.image.com/foo.jpeg",
"secure_url": "https://www.image.com/foo.jpeg"
},
"thumbnail": {
"width": 128,
"height": 128,
"format": "jpg",
"url": "http://www.image.com/foo.jpeg",
"secure_url": "https://www.image.com/foo.jpeg"
}
}
}
},
"pubsubChannel": "3333a674-ffff-aaaa-b351-b3a4473f3333",
"ruleHash": {},
"cameras": [],
"geofenceLimits": {
"ios": {
"debounceInterval": 90,
"gpsAccuracyMultiplier": 2.5,
"maximumGeofence": 5000,
"minimumGeofence": 100,
"minGPSAccuracyRequired": 80
}
}
}

View File

@ -0,0 +1,59 @@
{
"LockName": "Side Door",
"Type": 1001,
"Created": "2019-10-07T01:49:06.831Z",
"Updated": "2019-10-07T01:49:06.831Z",
"LockID": "BROKENID",
"HouseID": "abc",
"HouseName": "dog",
"Calibrated": false,
"timeZone": "America/Chicago",
"battery": 0.9524716174964851,
"hostLockInfo": {
"serialNumber": "YR",
"manufacturer": "yale",
"productID": 1536,
"productTypeID": 32770
},
"supportsEntryCodes": true,
"skuNumber": "AUG-MD01",
"macAddress": "MAC",
"SerialNumber": "M1FXZ00EZ9",
"LockStatus": {
"status": "unknown_error_during_connect",
"dateTime": "2020-02-22T02:48:11.741Z",
"isLockStatusChanged": true,
"valid": true,
"doorState": "closed"
},
"currentFirmwareVersion": "undefined-4.3.0-1.8.14",
"homeKitEnabled": true,
"zWaveEnabled": false,
"isGalileo": false,
"Bridge": {
"_id": "id",
"mfgBridgeID": "id",
"deviceModel": "august-connect",
"firmwareVersion": "2.2.1",
"operative": true,
"status": {
"current": "online",
"updated": "2020-02-21T15:06:47.001Z",
"lastOnline": "2020-02-21T15:06:47.001Z",
"lastOffline": "2020-02-06T17:33:21.265Z"
},
"hyperBridge": true
},
"parametersToSet": {},
"ruleHash": {},
"cameras": [],
"geofenceLimits": {
"ios": {
"debounceInterval": 90,
"gpsAccuracyMultiplier": 2.5,
"maximumGeofence": 5000,
"minimumGeofence": 100,
"minGPSAccuracyRequired": 80
}
}
}

View File

@ -0,0 +1,50 @@
{
"Bridge": {
"_id": "bridgeid",
"deviceModel": "august-connect",
"firmwareVersion": "2.2.1",
"hyperBridge": true,
"mfgBridgeID": "C5WY200WSH",
"operative": true,
"status": {
"current": "online",
"lastOffline": "2000-00-00T00:00:00.447Z",
"lastOnline": "2000-00-00T00:00:00.447Z",
"updated": "2000-00-00T00:00:00.447Z"
}
},
"Calibrated": false,
"Created": "2000-00-00T00:00:00.447Z",
"HouseID": "123",
"HouseName": "Test",
"LockID": "missing_doorsense_id",
"LockName": "Online door missing doorsense",
"LockStatus": {
"dateTime": "2017-12-10T04:48:30.272Z",
"isLockStatusChanged": false,
"status": "locked",
"valid": true
},
"SerialNumber": "XY",
"Type": 1001,
"Updated": "2000-00-00T00:00:00.447Z",
"battery": 0.922,
"currentFirmwareVersion": "undefined-4.3.0-1.8.14",
"homeKitEnabled": true,
"hostLockInfo": {
"manufacturer": "yale",
"productID": 1536,
"productTypeID": 32770,
"serialNumber": "ABC"
},
"isGalileo": false,
"macAddress": "12:22",
"pins": {
"created": [],
"loaded": []
},
"skuNumber": "AUG-MD01",
"supportsEntryCodes": true,
"timeZone": "Pacific/Hawaii",
"zWaveEnabled": false
}

View File

@ -0,0 +1,52 @@
{
"Bridge": {
"_id": "bridgeid",
"deviceModel": "august-connect",
"firmwareVersion": "2.2.1",
"hyperBridge": true,
"mfgBridgeID": "C5WY200WSH",
"operative": true,
"status": {
"current": "online",
"lastOffline": "2000-00-00T00:00:00.447Z",
"lastOnline": "2000-00-00T00:00:00.447Z",
"updated": "2000-00-00T00:00:00.447Z"
}
},
"pubsubChannel": "pubsub",
"Calibrated": false,
"Created": "2000-00-00T00:00:00.447Z",
"HouseID": "mockhouseid1",
"HouseName": "Test",
"LockID": "online_with_doorsense",
"LockName": "Online door with doorsense",
"LockStatus": {
"dateTime": "2017-12-10T04:48:30.272Z",
"doorState": "open",
"isLockStatusChanged": false,
"status": "locked",
"valid": true
},
"SerialNumber": "XY",
"Type": 1001,
"Updated": "2000-00-00T00:00:00.447Z",
"battery": 0.922,
"currentFirmwareVersion": "undefined-4.3.0-1.8.14",
"homeKitEnabled": true,
"hostLockInfo": {
"manufacturer": "yale",
"productID": 1536,
"productTypeID": 32770,
"serialNumber": "ABC"
},
"isGalileo": false,
"macAddress": "12:22",
"pins": {
"created": [],
"loaded": []
},
"skuNumber": "AUG-MD01",
"supportsEntryCodes": true,
"timeZone": "Pacific/Hawaii",
"zWaveEnabled": false
}

View File

@ -0,0 +1,100 @@
{
"LockName": "Front Door Lock",
"Type": 2,
"Created": "2017-12-10T03:12:09.210Z",
"Updated": "2017-12-10T03:12:09.210Z",
"LockID": "A6697750D607098BAE8D6BAA11EF8064",
"HouseID": "000000000000",
"HouseName": "My House",
"Calibrated": false,
"skuNumber": "AUG-SL02-M02-S02",
"timeZone": "America/Vancouver",
"battery": 0.88,
"SerialNumber": "X2FSW05DGA",
"LockStatus": {
"status": "locked",
"doorState": "closed",
"dateTime": "2017-12-10T04:48:30.272Z",
"isLockStatusChanged": true,
"valid": true
},
"currentFirmwareVersion": "109717e9-3.0.44-3.0.30",
"homeKitEnabled": false,
"zWaveEnabled": false,
"isGalileo": false,
"Bridge": {
"_id": "aaacab87f7efxa0015884999",
"mfgBridgeID": "AAGPP102XX",
"deviceModel": "august-doorbell",
"firmwareVersion": "2.3.0-RC153+201711151527",
"operative": true
},
"keypad": {
"_id": "5bc65c24e6ef2a263e1450a9",
"serialNumber": "K1GXB0054L",
"lockID": "92412D1B44004595B5DEB134E151A8D4",
"currentFirmwareVersion": "2.27.0",
"battery": {},
"batteryLevel": "Medium",
"batteryRaw": 170
},
"OfflineKeys": {
"created": [],
"loaded": [
{
"UserID": "cccca94e-373e-aaaa-bbbb-333396827777",
"slot": 1,
"key": "kkk01d4300c1dcxxx1c330f794941111",
"created": "2017-12-10T03:12:09.215Z",
"loaded": "2017-12-10T03:12:54.391Z"
}
],
"deleted": [],
"loadedhk": [
{
"key": "kkk01d4300c1dcxxx1c330f794941222",
"slot": 256,
"UserID": "cccca94e-373e-aaaa-bbbb-333396827777",
"created": "2017-12-10T03:12:09.218Z",
"loaded": "2017-12-10T03:12:55.563Z"
}
]
},
"parametersToSet": {},
"users": {
"cccca94e-373e-aaaa-bbbb-333396827777": {
"UserType": "superuser",
"FirstName": "Foo",
"LastName": "Bar",
"identifiers": ["email:foo@bar.com", "phone:+177777777777"],
"imageInfo": {
"original": {
"width": 948,
"height": 949,
"format": "jpg",
"url": "http://www.image.com/foo.jpeg",
"secure_url": "https://www.image.com/foo.jpeg"
},
"thumbnail": {
"width": 128,
"height": 128,
"format": "jpg",
"url": "http://www.image.com/foo.jpeg",
"secure_url": "https://www.image.com/foo.jpeg"
}
}
}
},
"pubsubChannel": "3333a674-ffff-aaaa-b351-b3a4473f3333",
"ruleHash": {},
"cameras": [],
"geofenceLimits": {
"ios": {
"debounceInterval": 90,
"gpsAccuracyMultiplier": 2.5,
"maximumGeofence": 5000,
"minimumGeofence": 100,
"minGPSAccuracyRequired": 80
}
}
}

View File

@ -0,0 +1,94 @@
{
"LockName": "Lock online with unlatch supported",
"Type": 17,
"Created": "2024-03-14T18:03:09.003Z",
"Updated": "2024-03-14T18:03:09.003Z",
"LockID": "online_with_unlatch",
"HouseID": "mockhouseid1",
"HouseName": "Zuhause",
"Calibrated": false,
"timeZone": "Europe/Berlin",
"battery": 0.61,
"batteryInfo": {
"level": 0.61,
"warningState": "lock_state_battery_warning_none",
"infoUpdatedDate": "2024-04-30T17:55:09.045Z",
"lastChangeDate": "2024-03-15T07:04:00.000Z",
"lastChangeVoltage": 8350,
"state": "Mittel",
"icon": "https://app-resources.aaecosystem.com/images/lock_battery_state_medium.png"
},
"hostHardwareID": "xxx",
"supportsEntryCodes": true,
"remoteOperateSecret": "xxxx",
"skuNumber": "NONE",
"macAddress": "DE:AD:BE:00:00:00",
"SerialNumber": "LPOC000000",
"LockStatus": {
"status": "locked",
"dateTime": "2024-04-30T18:41:25.673Z",
"isLockStatusChanged": false,
"valid": true,
"doorState": "init"
},
"currentFirmwareVersion": "1.0.4",
"homeKitEnabled": false,
"zWaveEnabled": false,
"isGalileo": false,
"Bridge": {
"_id": "65f33445529187c78a100000",
"mfgBridgeID": "LPOCH0004Y",
"deviceModel": "august-lock",
"firmwareVersion": "1.0.4",
"operative": true,
"status": {
"current": "online",
"lastOnline": "2024-04-30T18:41:27.971Z",
"updated": "2024-04-30T18:41:27.971Z",
"lastOffline": "2024-04-25T14:41:40.118Z"
},
"locks": [
{
"_id": "656858c182e6c7c555faf758",
"LockID": "68895DD075A1444FAD4C00B273EEEF28",
"macAddress": "DE:AD:BE:EF:0B:BC"
}
],
"hyperBridge": true
},
"OfflineKeys": {
"created": [],
"loaded": [
{
"created": "2024-03-14T18:03:09.034Z",
"key": "055281d4aa9bd7b68c7b7bb78e2f34ca",
"slot": 1,
"UserID": "b4b44424-0000-0000-0000-25c224dad337",
"loaded": "2024-03-14T18:03:33.470Z"
}
],
"deleted": []
},
"parametersToSet": {},
"users": {
"b4b44424-0000-0000-0000-25c224dad337": {
"UserType": "superuser",
"FirstName": "m10x",
"LastName": "m10x",
"identifiers": ["phone:+494444444", "email:m10x@example.com"]
}
},
"pubsubChannel": "pubsub",
"ruleHash": {},
"cameras": [],
"geofenceLimits": {
"ios": {
"debounceInterval": 90,
"gpsAccuracyMultiplier": 2.5,
"maximumGeofence": 5000,
"minimumGeofence": 100,
"minGPSAccuracyRequired": 80
}
},
"accessSchedulesAllowed": true
}

View File

@ -0,0 +1,16 @@
{
"A6697750D607098BAE8D6BAA11EF8063": {
"LockName": "Front Door Lock",
"UserType": "superuser",
"macAddress": "2E:BA:C4:14:3F:09",
"HouseID": "000000000000",
"HouseName": "A House"
},
"A6697750D607098BAE8D6BAA11EF9999": {
"LockName": "Back Door Lock",
"UserType": "user",
"macAddress": "2E:BA:C4:14:3F:88",
"HouseID": "000000000011",
"HouseName": "A House"
}
}

View File

@ -0,0 +1 @@
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpbnN0YWxsSWQiOiIiLCJyZWdpb24iOiJpcmVsYW5kLXByb2QtYXdzIiwiYXBwbGljYXRpb25JZCI6IiIsInVzZXJJZCI6ImE3NmMyNWU1LTQ5YWEtNGMxNC1jZDBjLTQ4YTY5MzFlMjA4MSIsInZJbnN0YWxsSWQiOmZhbHNlLCJ2UGFzc3dvcmQiOnRydWUsInZFbWFpbCI6dHJ1ZSwidlBob25lIjp0cnVlLCJoYXNJbnN0YWxsSWQiOmZhbHNlLCJoYXNQYXNzd29yZCI6ZmFsc2UsImhhc0VtYWlsIjpmYWxzZSwiaGFzUGhvbmUiOmZhbHNlLCJpc0xvY2tlZE91dCI6ZmFsc2UsImNhcHRjaGEiOiIiLCJlbWFpbCI6W10sInBob25lIjpbXSwiZXhwaXJlc0F0IjoiMjAyNC0xMi0xOFQxMzo1NDowNS4xMzRaIiwidGVtcG9yYXJ5QWNjb3VudENyZWF0aW9uUGFzc3dvcmRMaW5rIjoiIiwiaWF0IjoxNzI0MTYyMDQ1LCJleHAiOjE3MzQ1MzAwNDUsIm9hdXRoIjp7ImFwcF9uYW1lIjoiSG9tZSBBc3Npc3RhbnQiLCJjbGllbnRfaWQiOiJiM2NkM2YwYi1mYjk3LTRkNmMtYmVlOS1hZjdhYjA0NzU4YzciLCJyZWRpcmVjdF91cmkiOiJodHRwczovL2FjY291bnQtbGluay5uYWJ1Y2FzYS5jb20vYXV0aG9yaXplX2NhbGxiYWNrIiwicGFydG5lcl9pZCI6IjY1Nzk3NDg4MTA2NmNhNDhjOTljMDgyNiJ9fQ.BdRo-dEr-osbDQGB2XzlI-mIj4gqULtapODt-sj-eA8

View File

@ -0,0 +1,26 @@
{
"status": "kAugLockState_Locked",
"resultsFromOperationCache": false,
"retryCount": 1,
"info": {
"wlanRSSI": -54,
"lockType": "lock_version_1001",
"lockStatusChanged": false,
"serialNumber": "ABC",
"serial": "123",
"action": "lock",
"context": {
"startDate": "2020-02-19T01:59:39.516Z",
"retryCount": 1,
"transactionID": "mock"
},
"bridgeID": "mock",
"wlanSNR": 41,
"startTime": "2020-02-19T01:59:39.517Z",
"duration": 5149,
"lockID": "ABC",
"rssi": -77
},
"totalTime": 5162,
"doorState": "kAugDoorState_Open"
}

View File

@ -0,0 +1,100 @@
{
"LockName": "Front Door Lock",
"Type": 7,
"Created": "2017-12-10T03:12:09.210Z",
"Updated": "2017-12-10T03:12:09.210Z",
"LockID": "A6697750D607098BAE8D6BAA11EF8063",
"HouseID": "000000000000",
"HouseName": "My House",
"Calibrated": false,
"skuNumber": "AUG-SL02-M02-S02",
"timeZone": "America/Vancouver",
"battery": 0.88,
"SerialNumber": "X2FSW05DGA",
"LockStatus": {
"status": "locked",
"doorState": "closed",
"dateTime": "2017-12-10T04:48:30.272Z",
"isLockStatusChanged": true,
"valid": true
},
"currentFirmwareVersion": "109717e9-3.0.44-3.0.30",
"homeKitEnabled": false,
"zWaveEnabled": false,
"isGalileo": false,
"Bridge": {
"_id": "aaacab87f7efxa0015884999",
"mfgBridgeID": "AAGPP102XX",
"deviceModel": "august-doorbell",
"firmwareVersion": "2.3.0-RC153+201711151527",
"operative": true
},
"keypad": {
"_id": "5bc65c24e6ef2a263e1450a8",
"serialNumber": "K1GXB0054Z",
"lockID": "92412D1B44004595B5DEB134E151A8D3",
"currentFirmwareVersion": "2.27.0",
"battery": {},
"batteryLevel": "Medium",
"batteryRaw": 170
},
"OfflineKeys": {
"created": [],
"loaded": [
{
"UserID": "cccca94e-373e-aaaa-bbbb-333396827777",
"slot": 1,
"key": "kkk01d4300c1dcxxx1c330f794941111",
"created": "2017-12-10T03:12:09.215Z",
"loaded": "2017-12-10T03:12:54.391Z"
}
],
"deleted": [],
"loadedhk": [
{
"key": "kkk01d4300c1dcxxx1c330f794941222",
"slot": 256,
"UserID": "cccca94e-373e-aaaa-bbbb-333396827777",
"created": "2017-12-10T03:12:09.218Z",
"loaded": "2017-12-10T03:12:55.563Z"
}
]
},
"parametersToSet": {},
"users": {
"cccca94e-373e-aaaa-bbbb-333396827777": {
"UserType": "superuser",
"FirstName": "Foo",
"LastName": "Bar",
"identifiers": ["email:foo@bar.com", "phone:+177777777777"],
"imageInfo": {
"original": {
"width": 948,
"height": 949,
"format": "jpg",
"url": "http://www.image.com/foo.jpeg",
"secure_url": "https://www.image.com/foo.jpeg"
},
"thumbnail": {
"width": 128,
"height": 128,
"format": "jpg",
"url": "http://www.image.com/foo.jpeg",
"secure_url": "https://www.image.com/foo.jpeg"
}
}
}
},
"pubsubChannel": "3333a674-ffff-aaaa-b351-b3a4473f3333",
"ruleHash": {},
"cameras": [],
"geofenceLimits": {
"ios": {
"debounceInterval": 90,
"gpsAccuracyMultiplier": 2.5,
"maximumGeofence": 5000,
"minimumGeofence": 100,
"minGPSAccuracyRequired": 80
}
}
}

View File

@ -0,0 +1 @@
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpbnN0YWxsSWQiOiIiLCJyZWdpb24iOiJpcmVsYW5kLXByb2QtYXdzIiwiYXBwbGljYXRpb25JZCI6IiIsInVzZXJJZCI6ImE3NmMyNWU1LTQ5YWEtNGMxNC1jZDBjLTQ4YTY5MzFlMjA4MSIsInZJbnN0YWxsSWQiOmZhbHNlLCJ2UGFzc3dvcmQiOnRydWUsInZFbWFpbCI6dHJ1ZSwidlBob25lIjp0cnVlLCJoYXNJbnN0YWxsSWQiOmZhbHNlLCJoYXNQYXNzd29yZCI6ZmFsc2UsImhhc0VtYWlsIjpmYWxzZSwiaGFzUGhvbmUiOmZhbHNlLCJpc0xvY2tlZE91dCI6ZmFsc2UsImNhcHRjaGEiOiIiLCJlbWFpbCI6W10sInBob25lIjpbXSwiZXhwaXJlc0F0IjoiMjAyNC0xMi0xOFQxMzo1NDowNS4xMzRaIiwidGVtcG9yYXJ5QWNjb3VudENyZWF0aW9uUGFzc3dvcmRMaW5rIjoiIiwiaWF0IjoxNzI0MTYyMDQ1LCJleHAiOjI3MzQ1MzAwNDUsIm9hdXRoIjp7ImFwcF9uYW1lIjoiSG9tZSBBc3Npc3RhbnQiLCJjbGllbnRfaWQiOiJiM2NkM2YwYi1mYjk3LTRkNmMtYmVlOS1hZjdhYjA0NzU4YzciLCJyZWRpcmVjdF91cmkiOiJodHRwczovL2FjY291bnQtbGluay5uYWJ1Y2FzYS5jb20vYXV0aG9yaXplX2NhbGxiYWNrIiwicGFydG5lcl9pZCI6IjY1Nzk3NDg4MTA2NmNhNDhjOTljMDgyNiJ9fQ.DtkHscsvbTE-SyKW3RxwXFQIKMf0xJwfPZN1X3JesqA

View File

@ -0,0 +1 @@
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpbnN0YWxsSWQiOiIiLCJyZWdpb24iOiJpcmVsYW5kLXByb2QtYXdzIiwiYXBwbGljYXRpb25JZCI6IiIsInVzZXJJZCI6IjQ0NDQ0NDQ0LTQ5YWEtNGMxNC1jZDBjLTQ4YTY5MzFlMjA4MSIsInZJbnN0YWxsSWQiOmZhbHNlLCJ2UGFzc3dvcmQiOnRydWUsInZFbWFpbCI6dHJ1ZSwidlBob25lIjp0cnVlLCJoYXNJbnN0YWxsSWQiOmZhbHNlLCJoYXNQYXNzd29yZCI6ZmFsc2UsImhhc0VtYWlsIjpmYWxzZSwiaGFzUGhvbmUiOmZhbHNlLCJpc0xvY2tlZE91dCI6ZmFsc2UsImNhcHRjaGEiOiIiLCJlbWFpbCI6W10sInBob25lIjpbXSwiZXhwaXJlc0F0IjoiMjAyNC0xMi0xOFQxMzo1NDowNS4xMzRaIiwidGVtcG9yYXJ5QWNjb3VudENyZWF0aW9uUGFzc3dvcmRMaW5rIjoiIiwiaWF0IjoxNzI0MTYyMDQ1LCJleHAiOjE3MzQ1MzAwNDUsIm9hdXRoIjp7ImFwcF9uYW1lIjoiSG9tZSBBc3Npc3RhbnQiLCJjbGllbnRfaWQiOiJiM2NkM2YwYi1mYjk3LTRkNmMtYmVlOS1hZjdhYjA0NzU4YzciLCJyZWRpcmVjdF91cmkiOiJodHRwczovL2FjY291bnQtbGluay5uYWJ1Y2FzYS5jb20vYXV0aG9yaXplX2NhbGxiYWNrIiwicGFydG5lcl9pZCI6IjY1Nzk3NDg4MTA2NmNhNDhjOTljMDgyNiJ9fQ.PenDp4JUIBQZEx2BFxaCqV1-6yMuUPtmnB6jq1wpoX8

View File

@ -0,0 +1,26 @@
{
"status": "kAugLockState_Unlocked",
"resultsFromOperationCache": false,
"retryCount": 1,
"info": {
"wlanRSSI": -54,
"lockType": "lock_version_1001",
"lockStatusChanged": false,
"serialNumber": "ABC",
"serial": "123",
"action": "lock",
"context": {
"startDate": "2020-02-19T01:59:39.516Z",
"retryCount": 1,
"transactionID": "mock"
},
"bridgeID": "mock",
"wlanSNR": 41,
"startTime": "2020-02-19T01:59:39.517Z",
"duration": 5149,
"lockID": "ABC",
"rssi": -77
},
"totalTime": 5162,
"doorState": "kAugDoorState_Closed"
}

View File

@ -0,0 +1,515 @@
"""Mocks for the yale component."""
from __future__ import annotations
from collections.abc import Iterable
from contextlib import contextmanager
import json
import os
import time
from typing import Any
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
from yalexs.activity import (
ACTIVITY_ACTIONS_BRIDGE_OPERATION,
ACTIVITY_ACTIONS_DOOR_OPERATION,
ACTIVITY_ACTIONS_DOORBELL_DING,
ACTIVITY_ACTIONS_DOORBELL_MOTION,
ACTIVITY_ACTIONS_DOORBELL_VIEW,
ACTIVITY_ACTIONS_LOCK_OPERATION,
SOURCE_LOCK_OPERATE,
SOURCE_LOG,
Activity,
BridgeOperationActivity,
DoorbellDingActivity,
DoorbellMotionActivity,
DoorbellViewActivity,
DoorOperationActivity,
LockOperationActivity,
)
from yalexs.api_async import ApiAsync
from yalexs.authenticator_common import Authentication, AuthenticationState
from yalexs.const import Brand
from yalexs.doorbell import Doorbell, DoorbellDetail
from yalexs.lock import Lock, LockDetail
from yalexs.manager.ratelimit import _RateLimitChecker
from yalexs.manager.socketio import SocketIORunner
from homeassistant.components.application_credentials import (
ClientCredential,
async_import_client_credential,
)
from homeassistant.components.yale.const import DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, load_fixture
USER_ID = "a76c25e5-49aa-4c14-cd0c-48a6931e2081"
def _mock_get_config(
brand: Brand = Brand.YALE_GLOBAL, jwt: str | None = None
) -> dict[str, Any]:
"""Return a default yale config."""
return {
DOMAIN: {
"auth_implementation": "yale",
"token": {
"access_token": jwt or "access_token",
"expires_in": 1,
"refresh_token": "refresh_token",
"expires_at": time.time() + 3600,
"service": "yale",
},
}
}
def _mock_authenticator(auth_state: AuthenticationState) -> Authentication:
"""Mock an yale authenticator."""
authenticator = MagicMock()
type(authenticator).state = PropertyMock(return_value=auth_state)
return authenticator
def _timetoken() -> str:
return str(time.time_ns())[:-2]
async def mock_yale_config_entry(
hass: HomeAssistant,
) -> MockConfigEntry:
"""Mock yale config entry and client credentials."""
entry = mock_config_entry()
entry.add_to_hass(hass)
return entry
def mock_config_entry(jwt: str | None = None) -> MockConfigEntry:
"""Return the default mocked config entry."""
return MockConfigEntry(
domain=DOMAIN,
data=_mock_get_config(jwt=jwt)[DOMAIN],
options={},
unique_id=USER_ID,
)
async def mock_client_credentials(hass: HomeAssistant) -> ClientCredential:
"""Mock client credentials."""
assert await async_setup_component(hass, "application_credentials", {})
await async_import_client_credential(
hass,
DOMAIN,
ClientCredential("1", "2"),
DOMAIN,
)
@contextmanager
def patch_yale_setup():
"""Patch yale setup process."""
with (
patch("yalexs.manager.gateway.ApiAsync") as api_mock,
patch.object(_RateLimitChecker, "register_wakeup") as authenticate_mock,
patch("yalexs.manager.data.SocketIORunner") as socketio_mock,
patch.object(socketio_mock, "run"),
patch(
"homeassistant.components.yale.config_entry_oauth2_flow.async_get_config_entry_implementation"
),
):
yield api_mock, authenticate_mock, socketio_mock
async def _mock_setup_yale(
hass: HomeAssistant,
api_instance: ApiAsync,
socketio_mock: SocketIORunner,
authenticate_side_effect: MagicMock,
) -> ConfigEntry:
"""Set up yale integration."""
entry = await mock_yale_config_entry(hass)
with patch_yale_setup() as patched_setup:
api_mock, authenticate_mock, sockio_mock_ = patched_setup
authenticate_mock.side_effect = authenticate_side_effect
sockio_mock_.return_value = socketio_mock
api_mock.return_value = api_instance
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
return entry
async def _create_yale_with_devices(
hass: HomeAssistant,
devices: Iterable[LockDetail | DoorbellDetail] | None = None,
api_call_side_effects: dict[str, Any] | None = None,
activities: list[Any] | None = None,
brand: Brand = Brand.YALE_GLOBAL,
authenticate_side_effect: MagicMock | None = None,
) -> tuple[ConfigEntry, SocketIORunner]:
entry, _, socketio = await _create_yale_api_with_devices(
hass,
devices,
api_call_side_effects,
activities,
brand,
authenticate_side_effect,
)
return entry, socketio
async def _create_yale_api_with_devices(
hass: HomeAssistant,
devices: Iterable[LockDetail | DoorbellDetail] | None = None,
api_call_side_effects: dict[str, Any] | None = None,
activities: dict[str, Any] | None = None,
brand: Brand = Brand.YALE_GLOBAL,
authenticate_side_effect: MagicMock | None = None,
) -> tuple[ConfigEntry, ApiAsync, SocketIORunner]:
if api_call_side_effects is None:
api_call_side_effects = {}
if devices is None:
devices = ()
update_api_call_side_effects(api_call_side_effects, devices, activities)
api_instance = await make_mock_api(api_call_side_effects, brand)
socketio = SocketIORunner(
MagicMock(
api=api_instance, async_get_access_token=AsyncMock(return_value="token")
)
)
socketio.run = AsyncMock()
entry = await _mock_setup_yale(
hass,
api_instance,
socketio,
authenticate_side_effect=authenticate_side_effect,
)
return entry, api_instance, socketio
def update_api_call_side_effects(
api_call_side_effects: dict[str, Any],
devices: Iterable[LockDetail | DoorbellDetail],
activities: dict[str, Any] | None = None,
) -> None:
"""Update side effects dict from devices and activities."""
device_data = {"doorbells": [], "locks": []}
for device in devices or ():
if isinstance(device, LockDetail):
device_data["locks"].append(
{"base": _mock_yale_lock(device.device_id), "detail": device}
)
elif isinstance(device, DoorbellDetail):
device_data["doorbells"].append(
{
"base": _mock_yale_doorbell(
deviceid=device.device_id,
brand=device._data.get("brand", Brand.YALE_GLOBAL),
),
"detail": device,
}
)
else:
raise ValueError # noqa: TRY004
def _get_device_detail(device_type, device_id):
for device in device_data[device_type]:
if device["detail"].device_id == device_id:
return device["detail"]
raise ValueError
def _get_base_devices(device_type):
return [device["base"] for device in device_data[device_type]]
def get_lock_detail_side_effect(access_token, device_id):
return _get_device_detail("locks", device_id)
def get_doorbell_detail_side_effect(access_token, device_id):
return _get_device_detail("doorbells", device_id)
def get_operable_locks_side_effect(access_token):
return _get_base_devices("locks")
def get_doorbells_side_effect(access_token):
return _get_base_devices("doorbells")
def get_house_activities_side_effect(access_token, house_id, limit=10):
if activities is not None:
return activities
return []
def lock_return_activities_side_effect(access_token, device_id):
lock = _get_device_detail("locks", device_id)
return [
# There is a check to prevent out of order events
# so we set the doorclosed & lock event in the future
# to prevent a race condition where we reject the event
# because it happened before the dooropen & unlock event.
_mock_lock_operation_activity(lock, "lock", 2000),
_mock_door_operation_activity(lock, "doorclosed", 2000),
]
def unlock_return_activities_side_effect(access_token, device_id):
lock = _get_device_detail("locks", device_id)
return [
_mock_lock_operation_activity(lock, "unlock", 0),
_mock_door_operation_activity(lock, "dooropen", 0),
]
api_call_side_effects.setdefault("get_lock_detail", get_lock_detail_side_effect)
api_call_side_effects.setdefault(
"get_doorbell_detail", get_doorbell_detail_side_effect
)
api_call_side_effects.setdefault(
"get_operable_locks", get_operable_locks_side_effect
)
api_call_side_effects.setdefault("get_doorbells", get_doorbells_side_effect)
api_call_side_effects.setdefault(
"get_house_activities", get_house_activities_side_effect
)
api_call_side_effects.setdefault(
"lock_return_activities", lock_return_activities_side_effect
)
api_call_side_effects.setdefault(
"unlock_return_activities", unlock_return_activities_side_effect
)
api_call_side_effects.setdefault(
"async_unlatch_return_activities", unlock_return_activities_side_effect
)
async def make_mock_api(
api_call_side_effects: dict[str, Any],
brand: Brand = Brand.YALE_GLOBAL,
) -> ApiAsync:
"""Make a mock ApiAsync instance."""
api_instance = MagicMock(name="Api", brand=brand)
if api_call_side_effects["get_lock_detail"]:
type(api_instance).async_get_lock_detail = AsyncMock(
side_effect=api_call_side_effects["get_lock_detail"]
)
if api_call_side_effects["get_operable_locks"]:
type(api_instance).async_get_operable_locks = AsyncMock(
side_effect=api_call_side_effects["get_operable_locks"]
)
if api_call_side_effects["get_doorbells"]:
type(api_instance).async_get_doorbells = AsyncMock(
side_effect=api_call_side_effects["get_doorbells"]
)
if api_call_side_effects["get_doorbell_detail"]:
type(api_instance).async_get_doorbell_detail = AsyncMock(
side_effect=api_call_side_effects["get_doorbell_detail"]
)
if api_call_side_effects["get_house_activities"]:
type(api_instance).async_get_house_activities = AsyncMock(
side_effect=api_call_side_effects["get_house_activities"]
)
if api_call_side_effects["lock_return_activities"]:
type(api_instance).async_lock_return_activities = AsyncMock(
side_effect=api_call_side_effects["lock_return_activities"]
)
if api_call_side_effects["unlock_return_activities"]:
type(api_instance).async_unlock_return_activities = AsyncMock(
side_effect=api_call_side_effects["unlock_return_activities"]
)
if api_call_side_effects["async_unlatch_return_activities"]:
type(api_instance).async_unlatch_return_activities = AsyncMock(
side_effect=api_call_side_effects["async_unlatch_return_activities"]
)
api_instance.async_unlock_async = AsyncMock()
api_instance.async_lock_async = AsyncMock()
api_instance.async_status_async = AsyncMock()
api_instance.async_get_user = AsyncMock(return_value={"UserID": "abc"})
api_instance.async_unlatch_async = AsyncMock()
api_instance.async_unlatch = AsyncMock()
api_instance.async_add_websocket_subscription = AsyncMock()
return api_instance
def _mock_yale_authentication(
token_text: str, token_timestamp: float, state: AuthenticationState
) -> Authentication:
authentication = MagicMock(name="yalexs.authentication")
type(authentication).state = PropertyMock(return_value=state)
type(authentication).access_token = PropertyMock(return_value=token_text)
type(authentication).access_token_expires = PropertyMock(
return_value=token_timestamp
)
return authentication
def _mock_yale_lock(lockid: str = "mocklockid1", houseid: str = "mockhouseid1") -> Lock:
return Lock(lockid, _mock_yale_lock_data(lockid=lockid, houseid=houseid))
def _mock_yale_doorbell(
deviceid="mockdeviceid1", houseid="mockhouseid1", brand=Brand.YALE_GLOBAL
) -> Doorbell:
return Doorbell(
deviceid,
_mock_yale_doorbell_data(deviceid=deviceid, houseid=houseid, brand=brand),
)
def _mock_yale_doorbell_data(
deviceid: str = "mockdeviceid1",
houseid: str = "mockhouseid1",
brand: Brand = Brand.YALE_GLOBAL,
) -> dict[str, Any]:
return {
"_id": deviceid,
"DeviceID": deviceid,
"name": f"{deviceid} Name",
"HouseID": houseid,
"UserType": "owner",
"serialNumber": "mockserial",
"battery": 90,
"status": "standby",
"currentFirmwareVersion": "mockfirmware",
"Bridge": {
"_id": "bridgeid1",
"firmwareVersion": "mockfirm",
"operative": True,
},
"LockStatus": {"doorState": "open"},
}
def _mock_yale_lock_data(
lockid: str = "mocklockid1", houseid: str = "mockhouseid1"
) -> dict[str, Any]:
return {
"_id": lockid,
"LockID": lockid,
"LockName": f"{lockid} Name",
"HouseID": houseid,
"UserType": "owner",
"SerialNumber": "mockserial",
"battery": 90,
"currentFirmwareVersion": "mockfirmware",
"Bridge": {
"_id": "bridgeid1",
"firmwareVersion": "mockfirm",
"operative": True,
},
"LockStatus": {"doorState": "open"},
}
async def _mock_operative_yale_lock_detail(hass: HomeAssistant) -> LockDetail:
return await _mock_lock_from_fixture(hass, "get_lock.online.json")
async def _mock_lock_with_offline_key(hass: HomeAssistant) -> LockDetail:
return await _mock_lock_from_fixture(hass, "get_lock.online_with_keys.json")
async def _mock_inoperative_yale_lock_detail(hass: HomeAssistant) -> LockDetail:
return await _mock_lock_from_fixture(hass, "get_lock.offline.json")
async def _mock_activities_from_fixture(
hass: HomeAssistant, path: str
) -> list[Activity]:
json_dict = await _load_json_fixture(hass, path)
activities = []
for activity_json in json_dict:
activity = _activity_from_dict(activity_json)
if activity:
activities.append(activity)
return activities
async def _mock_lock_from_fixture(hass: HomeAssistant, path: str) -> LockDetail:
json_dict = await _load_json_fixture(hass, path)
return LockDetail(json_dict)
async def _mock_doorbell_from_fixture(hass: HomeAssistant, path: str) -> LockDetail:
json_dict = await _load_json_fixture(hass, path)
return DoorbellDetail(json_dict)
async def _load_json_fixture(hass: HomeAssistant, path: str) -> dict[str, Any]:
fixture = await hass.async_add_executor_job(
load_fixture, os.path.join("yale", path)
)
return json.loads(fixture)
async def _mock_doorsense_enabled_yale_lock_detail(hass: HomeAssistant) -> LockDetail:
return await _mock_lock_from_fixture(hass, "get_lock.online_with_doorsense.json")
async def _mock_doorsense_missing_yale_lock_detail(hass: HomeAssistant) -> LockDetail:
return await _mock_lock_from_fixture(hass, "get_lock.online_missing_doorsense.json")
async def _mock_lock_with_unlatch(hass: HomeAssistant) -> LockDetail:
return await _mock_lock_from_fixture(hass, "get_lock.online_with_unlatch.json")
def _mock_lock_operation_activity(
lock: Lock, action: str, offset: float
) -> LockOperationActivity:
return LockOperationActivity(
SOURCE_LOCK_OPERATE,
{
"dateTime": (time.time() + offset) * 1000,
"deviceID": lock.device_id,
"deviceType": "lock",
"action": action,
},
)
def _mock_door_operation_activity(
lock: Lock, action: str, offset: float
) -> DoorOperationActivity:
return DoorOperationActivity(
SOURCE_LOCK_OPERATE,
{
"dateTime": (time.time() + offset) * 1000,
"deviceID": lock.device_id,
"deviceType": "lock",
"action": action,
},
)
def _activity_from_dict(activity_dict: dict[str, Any]) -> Activity | None:
action = activity_dict.get("action")
activity_dict["dateTime"] = time.time() * 1000
if action in ACTIVITY_ACTIONS_DOORBELL_DING:
return DoorbellDingActivity(SOURCE_LOG, activity_dict)
if action in ACTIVITY_ACTIONS_DOORBELL_MOTION:
return DoorbellMotionActivity(SOURCE_LOG, activity_dict)
if action in ACTIVITY_ACTIONS_DOORBELL_VIEW:
return DoorbellViewActivity(SOURCE_LOG, activity_dict)
if action in ACTIVITY_ACTIONS_LOCK_OPERATION:
return LockOperationActivity(SOURCE_LOG, activity_dict)
if action in ACTIVITY_ACTIONS_DOOR_OPERATION:
return DoorOperationActivity(SOURCE_LOG, activity_dict)
if action in ACTIVITY_ACTIONS_BRIDGE_OPERATION:
return BridgeOperationActivity(SOURCE_LOG, activity_dict)
return None

View File

@ -0,0 +1,125 @@
# serializer version: 1
# name: test_diagnostics
dict({
'brand': 'yale_home',
'doorbells': dict({
'K98GiDT45GUL': dict({
'HouseID': '**REDACTED**',
'LockID': 'BBBB1F5F11114C24CCCC97571DD6AAAA',
'appID': 'august-iphone',
'caps': list([
'reconnect',
]),
'createdAt': '2016-11-26T22:27:11.176Z',
'doorbellID': 'K98GiDT45GUL',
'doorbellServerURL': 'https://doorbells.august.com',
'dvrSubscriptionSetupDone': True,
'firmwareVersion': '2.3.0-RC153+201711151527',
'installDate': '2016-11-26T22:27:11.176Z',
'installUserID': '**REDACTED**',
'name': 'Front Door',
'pubsubChannel': '**REDACTED**',
'recentImage': '**REDACTED**',
'serialNumber': 'tBXZR0Z35E',
'settings': dict({
'ABREnabled': True,
'IREnabled': True,
'IVAEnabled': False,
'JPGQuality': 70,
'batteryLowThreshold': 3.1,
'batteryRun': False,
'batteryUseThreshold': 3.4,
'bitrateCeiling': 512000,
'buttonpush_notifications': True,
'debug': False,
'directLink': True,
'initialBitrate': 384000,
'irConfiguration': 8448272,
'keepEncoderRunning': True,
'micVolume': 100,
'minACNoScaling': 40,
'motion_notifications': True,
'notify_when_offline': True,
'overlayEnabled': True,
'ringSoundEnabled': True,
'speakerVolume': 92,
'turnOffCamera': False,
'videoResolution': '640x480',
}),
'status': 'doorbell_call_status_online',
'status_timestamp': 1512811834532,
'telemetry': dict({
'BSSID': '88:ee:00:dd:aa:11',
'SSID': 'foo_ssid',
'ac_in': 23.856874,
'battery': 4.061763,
'battery_soc': 96,
'battery_soh': 95,
'date': '2017-12-10 08:05:12',
'doorbell_low_battery': False,
'ip_addr': '10.0.1.11',
'link_quality': 54,
'load_average': '0.50 0.47 0.35 1/154 9345',
'signal_level': -56,
'steady_ac_in': 22.196405,
'temperature': 28.25,
'updated_at': '2017-12-10T08:05:13.650Z',
'uptime': '16168.75 13830.49',
'wifi_freq': 5745,
}),
'updatedAt': '2017-12-10T08:05:13.650Z',
}),
}),
'locks': dict({
'online_with_doorsense': dict({
'Bridge': dict({
'_id': 'bridgeid',
'deviceModel': 'august-connect',
'firmwareVersion': '2.2.1',
'hyperBridge': True,
'mfgBridgeID': 'C5WY200WSH',
'operative': True,
'status': dict({
'current': 'online',
'lastOffline': '2000-00-00T00:00:00.447Z',
'lastOnline': '2000-00-00T00:00:00.447Z',
'updated': '2000-00-00T00:00:00.447Z',
}),
}),
'Calibrated': False,
'Created': '2000-00-00T00:00:00.447Z',
'HouseID': '**REDACTED**',
'HouseName': 'Test',
'LockID': 'online_with_doorsense',
'LockName': 'Online door with doorsense',
'LockStatus': dict({
'dateTime': '2017-12-10T04:48:30.272Z',
'doorState': 'open',
'isLockStatusChanged': False,
'status': 'locked',
'valid': True,
}),
'SerialNumber': 'XY',
'Type': 1001,
'Updated': '2000-00-00T00:00:00.447Z',
'battery': 0.922,
'currentFirmwareVersion': 'undefined-4.3.0-1.8.14',
'homeKitEnabled': True,
'hostLockInfo': dict({
'manufacturer': 'yale',
'productID': 1536,
'productTypeID': 32770,
'serialNumber': 'ABC',
}),
'isGalileo': False,
'macAddress': '12:22',
'pins': '**REDACTED**',
'pubsubChannel': '**REDACTED**',
'skuNumber': 'AUG-MD01',
'supportsEntryCodes': True,
'timeZone': 'Pacific/Hawaii',
'zWaveEnabled': False,
}),
}),
})
# ---

View File

@ -0,0 +1,390 @@
"""The binary_sensor tests for the yale platform."""
import datetime
from unittest.mock import patch
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_LOCK,
SERVICE_UNLOCK,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
import homeassistant.util.dt as dt_util
from .mocks import (
_create_yale_with_devices,
_mock_activities_from_fixture,
_mock_doorbell_from_fixture,
_mock_doorsense_enabled_yale_lock_detail,
_mock_lock_from_fixture,
)
from tests.common import async_fire_time_changed
async def test_doorsense(hass: HomeAssistant) -> None:
"""Test creation of a lock with doorsense and bridge."""
lock_one = await _mock_lock_from_fixture(
hass, "get_lock.online_with_doorsense.json"
)
await _create_yale_with_devices(hass, [lock_one])
binary_sensor_online_with_doorsense_name = hass.states.get(
"binary_sensor.online_with_doorsense_name_door"
)
assert binary_sensor_online_with_doorsense_name.state == STATE_ON
data = {ATTR_ENTITY_ID: "lock.online_with_doorsense_name"}
await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True)
await hass.async_block_till_done()
binary_sensor_online_with_doorsense_name = hass.states.get(
"binary_sensor.online_with_doorsense_name_door"
)
assert binary_sensor_online_with_doorsense_name.state == STATE_ON
await hass.services.async_call(LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True)
await hass.async_block_till_done()
binary_sensor_online_with_doorsense_name = hass.states.get(
"binary_sensor.online_with_doorsense_name_door"
)
assert binary_sensor_online_with_doorsense_name.state == STATE_OFF
async def test_lock_bridge_offline(hass: HomeAssistant) -> None:
"""Test creation of a lock with doorsense and bridge that goes offline."""
lock_one = await _mock_lock_from_fixture(
hass, "get_lock.online_with_doorsense.json"
)
activities = await _mock_activities_from_fixture(
hass, "get_activity.bridge_offline.json"
)
await _create_yale_with_devices(hass, [lock_one], activities=activities)
binary_sensor_online_with_doorsense_name = hass.states.get(
"binary_sensor.online_with_doorsense_name_door"
)
assert binary_sensor_online_with_doorsense_name.state == STATE_UNAVAILABLE
async def test_create_doorbell(hass: HomeAssistant) -> None:
"""Test creation of a doorbell."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json")
await _create_yale_with_devices(hass, [doorbell_one])
binary_sensor_k98gidt45gul_name_motion = hass.states.get(
"binary_sensor.k98gidt45gul_name_motion"
)
assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF
binary_sensor_k98gidt45gul_name_image_capture = hass.states.get(
"binary_sensor.k98gidt45gul_name_image_capture"
)
assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF
binary_sensor_k98gidt45gul_name_online = hass.states.get(
"binary_sensor.k98gidt45gul_name_connectivity"
)
assert binary_sensor_k98gidt45gul_name_online.state == STATE_ON
binary_sensor_k98gidt45gul_name_ding = hass.states.get(
"binary_sensor.k98gidt45gul_name_doorbell_ding"
)
assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF
binary_sensor_k98gidt45gul_name_motion = hass.states.get(
"binary_sensor.k98gidt45gul_name_motion"
)
assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF
binary_sensor_k98gidt45gul_name_image_capture = hass.states.get(
"binary_sensor.k98gidt45gul_name_image_capture"
)
assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF
async def test_create_doorbell_offline(hass: HomeAssistant) -> None:
"""Test creation of a doorbell that is offline."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.offline.json")
await _create_yale_with_devices(hass, [doorbell_one])
binary_sensor_tmt100_name_motion = hass.states.get(
"binary_sensor.tmt100_name_motion"
)
assert binary_sensor_tmt100_name_motion.state == STATE_UNAVAILABLE
binary_sensor_tmt100_name_online = hass.states.get(
"binary_sensor.tmt100_name_connectivity"
)
assert binary_sensor_tmt100_name_online.state == STATE_OFF
binary_sensor_tmt100_name_ding = hass.states.get(
"binary_sensor.tmt100_name_doorbell_ding"
)
assert binary_sensor_tmt100_name_ding.state == STATE_UNAVAILABLE
async def test_create_doorbell_with_motion(hass: HomeAssistant) -> None:
"""Test creation of a doorbell."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json")
activities = await _mock_activities_from_fixture(
hass, "get_activity.doorbell_motion.json"
)
await _create_yale_with_devices(hass, [doorbell_one], activities=activities)
binary_sensor_k98gidt45gul_name_motion = hass.states.get(
"binary_sensor.k98gidt45gul_name_motion"
)
assert binary_sensor_k98gidt45gul_name_motion.state == STATE_ON
binary_sensor_k98gidt45gul_name_online = hass.states.get(
"binary_sensor.k98gidt45gul_name_connectivity"
)
assert binary_sensor_k98gidt45gul_name_online.state == STATE_ON
binary_sensor_k98gidt45gul_name_ding = hass.states.get(
"binary_sensor.k98gidt45gul_name_doorbell_ding"
)
assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF
new_time = dt_util.utcnow() + datetime.timedelta(seconds=40)
native_time = datetime.datetime.now() + datetime.timedelta(seconds=40)
with patch(
"homeassistant.components.yale.util._native_datetime",
return_value=native_time,
):
async_fire_time_changed(hass, new_time)
await hass.async_block_till_done()
binary_sensor_k98gidt45gul_name_motion = hass.states.get(
"binary_sensor.k98gidt45gul_name_motion"
)
assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF
async def test_doorbell_update_via_socketio(hass: HomeAssistant) -> None:
"""Test creation of a doorbell that can be updated via socketio."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json")
_, socketio = await _create_yale_with_devices(hass, [doorbell_one])
assert doorbell_one.pubsub_channel == "7c7a6672-59c8-3333-ffff-dcd98705cccc"
binary_sensor_k98gidt45gul_name_motion = hass.states.get(
"binary_sensor.k98gidt45gul_name_motion"
)
assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF
binary_sensor_k98gidt45gul_name_ding = hass.states.get(
"binary_sensor.k98gidt45gul_name_doorbell_ding"
)
assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF
listener = list(socketio._listeners)[0]
listener(
doorbell_one.device_id,
dt_util.utcnow(),
{
"status": "imagecapture",
"data": {
"result": {
"created_at": "2021-03-16T01:07:08.817Z",
"secure_url": (
"https://dyu7azbnaoi74.cloudfront.net/zip/images/zip.jpeg"
),
},
},
},
)
await hass.async_block_till_done()
binary_sensor_k98gidt45gul_name_image_capture = hass.states.get(
"binary_sensor.k98gidt45gul_name_image_capture"
)
assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_ON
listener(
doorbell_one.device_id,
dt_util.utcnow(),
{
"status": "doorbell_motion_detected",
"data": {
"event": "doorbell_motion_detected",
"image": {
"height": 640,
"width": 480,
"format": "jpg",
"created_at": "2021-03-16T02:36:26.886Z",
"bytes": 14061,
"secure_url": (
"https://dyu7azbnaoi74.cloudfront.net/images/1f8.jpeg"
),
"url": "https://dyu7azbnaoi74.cloudfront.net/images/1f8.jpeg",
"etag": "09e839331c4ea59eef28081f2caa0e90",
},
"doorbellName": "Front Door",
"callID": None,
"origin": "mars-api",
"mutableContent": True,
},
},
)
await hass.async_block_till_done()
binary_sensor_k98gidt45gul_name_motion = hass.states.get(
"binary_sensor.k98gidt45gul_name_motion"
)
assert binary_sensor_k98gidt45gul_name_motion.state == STATE_ON
binary_sensor_k98gidt45gul_name_ding = hass.states.get(
"binary_sensor.k98gidt45gul_name_doorbell_ding"
)
assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF
new_time = dt_util.utcnow() + datetime.timedelta(seconds=40)
native_time = datetime.datetime.now() + datetime.timedelta(seconds=40)
with patch(
"homeassistant.components.yale.util._native_datetime",
return_value=native_time,
):
async_fire_time_changed(hass, new_time)
await hass.async_block_till_done()
binary_sensor_k98gidt45gul_name_image_capture = hass.states.get(
"binary_sensor.k98gidt45gul_name_image_capture"
)
assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF
listener(
doorbell_one.device_id,
dt_util.utcnow(),
{
"status": "buttonpush",
},
)
await hass.async_block_till_done()
binary_sensor_k98gidt45gul_name_ding = hass.states.get(
"binary_sensor.k98gidt45gul_name_doorbell_ding"
)
assert binary_sensor_k98gidt45gul_name_ding.state == STATE_ON
new_time = dt_util.utcnow() + datetime.timedelta(seconds=40)
native_time = datetime.datetime.now() + datetime.timedelta(seconds=40)
with patch(
"homeassistant.components.yale.util._native_datetime",
return_value=native_time,
):
async_fire_time_changed(hass, new_time)
await hass.async_block_till_done()
binary_sensor_k98gidt45gul_name_ding = hass.states.get(
"binary_sensor.k98gidt45gul_name_doorbell_ding"
)
assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF
async def test_doorbell_device_registry(
hass: HomeAssistant, device_registry: dr.DeviceRegistry
) -> None:
"""Test creation of a lock with doorsense and bridge ands up in the registry."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.offline.json")
await _create_yale_with_devices(hass, [doorbell_one])
reg_device = device_registry.async_get_device(identifiers={("yale", "tmt100")})
assert reg_device.model == "hydra1"
assert reg_device.name == "tmt100 Name"
assert reg_device.manufacturer == "Yale Home Inc."
assert reg_device.sw_version == "3.1.0-HYDRC75+201909251139"
async def test_door_sense_update_via_socketio(hass: HomeAssistant) -> None:
"""Test creation of a lock with doorsense and bridge."""
lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass)
assert lock_one.pubsub_channel == "pubsub"
activities = await _mock_activities_from_fixture(hass, "get_activity.lock.json")
config_entry, socketio = await _create_yale_with_devices(
hass, [lock_one], activities=activities
)
binary_sensor_online_with_doorsense_name = hass.states.get(
"binary_sensor.online_with_doorsense_name_door"
)
assert binary_sensor_online_with_doorsense_name.state == STATE_ON
listener = list(socketio._listeners)[0]
listener(
lock_one.device_id,
dt_util.utcnow(),
{"status": "kAugLockState_Unlocking", "doorState": "closed"},
)
await hass.async_block_till_done()
binary_sensor_online_with_doorsense_name = hass.states.get(
"binary_sensor.online_with_doorsense_name_door"
)
assert binary_sensor_online_with_doorsense_name.state == STATE_OFF
listener(
lock_one.device_id,
dt_util.utcnow(),
{"status": "kAugLockState_Locking", "doorState": "open"},
)
await hass.async_block_till_done()
binary_sensor_online_with_doorsense_name = hass.states.get(
"binary_sensor.online_with_doorsense_name_door"
)
assert binary_sensor_online_with_doorsense_name.state == STATE_ON
async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30))
await hass.async_block_till_done()
binary_sensor_online_with_doorsense_name = hass.states.get(
"binary_sensor.online_with_doorsense_name_door"
)
assert binary_sensor_online_with_doorsense_name.state == STATE_ON
socketio.connected = True
async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30))
await hass.async_block_till_done()
binary_sensor_online_with_doorsense_name = hass.states.get(
"binary_sensor.online_with_doorsense_name_door"
)
assert binary_sensor_online_with_doorsense_name.state == STATE_ON
# Ensure socketio status is always preserved
async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=2))
await hass.async_block_till_done()
binary_sensor_online_with_doorsense_name = hass.states.get(
"binary_sensor.online_with_doorsense_name_door"
)
assert binary_sensor_online_with_doorsense_name.state == STATE_ON
listener(
lock_one.device_id,
dt_util.utcnow(),
{"status": "kAugLockState_Unlocking", "doorState": "open"},
)
await hass.async_block_till_done()
binary_sensor_online_with_doorsense_name = hass.states.get(
"binary_sensor.online_with_doorsense_name_door"
)
assert binary_sensor_online_with_doorsense_name.state == STATE_ON
async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=4))
await hass.async_block_till_done()
binary_sensor_online_with_doorsense_name = hass.states.get(
"binary_sensor.online_with_doorsense_name_door"
)
assert binary_sensor_online_with_doorsense_name.state == STATE_ON
await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()
async def test_create_lock_with_doorbell(hass: HomeAssistant) -> None:
"""Test creation of a lock with a doorbell."""
lock_one = await _mock_lock_from_fixture(hass, "lock_with_doorbell.online.json")
await _create_yale_with_devices(hass, [lock_one])
ding_sensor = hass.states.get(
"binary_sensor.a6697750d607098bae8d6baa11ef8063_name_doorbell_ding"
)
assert ding_sensor.state == STATE_OFF

View File

@ -0,0 +1,24 @@
"""The button tests for the yale platform."""
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from .mocks import _create_yale_api_with_devices, _mock_lock_from_fixture
async def test_wake_lock(hass: HomeAssistant) -> None:
"""Test creation of a lock and wake it."""
lock_one = await _mock_lock_from_fixture(
hass, "get_lock.online_with_doorsense.json"
)
_, api_instance, _ = await _create_yale_api_with_devices(hass, [lock_one])
entity_id = "button.online_with_doorsense_name_wake"
binary_sensor_online_with_doorsense_name = hass.states.get(entity_id)
assert binary_sensor_online_with_doorsense_name is not None
api_instance.async_status_async.reset_mock()
await hass.services.async_call(
BUTTON_DOMAIN, SERVICE_PRESS, {ATTR_ENTITY_ID: entity_id}, blocking=True
)
await hass.async_block_till_done()
api_instance.async_status_async.assert_called_once()

View File

@ -0,0 +1,93 @@
"""The camera tests for the yale platform."""
from http import HTTPStatus
from unittest.mock import patch
from yalexs.const import Brand
from yalexs.doorbell import ContentTokenExpired
from homeassistant.const import STATE_IDLE
from homeassistant.core import HomeAssistant
from .mocks import _create_yale_with_devices, _mock_doorbell_from_fixture
from tests.typing import ClientSessionGenerator
async def test_create_doorbell(
hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator
) -> None:
"""Test creation of a doorbell."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json")
with patch.object(
doorbell_one, "async_get_doorbell_image", create=False, return_value="image"
):
await _create_yale_with_devices(hass, [doorbell_one], brand=Brand.YALE_GLOBAL)
camera_k98gidt45gul_name_camera = hass.states.get(
"camera.k98gidt45gul_name_camera"
)
assert camera_k98gidt45gul_name_camera.state == STATE_IDLE
url = hass.states.get("camera.k98gidt45gul_name_camera").attributes[
"entity_picture"
]
client = await hass_client_no_auth()
resp = await client.get(url)
assert resp.status == HTTPStatus.OK
body = await resp.text()
assert body == "image"
async def test_doorbell_refresh_content_token_recover(
hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator
) -> None:
"""Test camera image content token expired."""
doorbell_two = await _mock_doorbell_from_fixture(hass, "get_doorbell.json")
with patch.object(
doorbell_two,
"async_get_doorbell_image",
create=False,
side_effect=[ContentTokenExpired, "image"],
):
await _create_yale_with_devices(
hass,
[doorbell_two],
brand=Brand.YALE_GLOBAL,
)
url = hass.states.get("camera.k98gidt45gul_name_camera").attributes[
"entity_picture"
]
client = await hass_client_no_auth()
resp = await client.get(url)
assert resp.status == HTTPStatus.OK
body = await resp.text()
assert body == "image"
async def test_doorbell_refresh_content_token_fail(
hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator
) -> None:
"""Test camera image content token expired."""
doorbell_two = await _mock_doorbell_from_fixture(hass, "get_doorbell.json")
with patch.object(
doorbell_two,
"async_get_doorbell_image",
create=False,
side_effect=ContentTokenExpired,
):
await _create_yale_with_devices(
hass,
[doorbell_two],
brand=Brand.YALE_GLOBAL,
)
url = hass.states.get("camera.k98gidt45gul_name_camera").attributes[
"entity_picture"
]
client = await hass_client_no_auth()
resp = await client.get(url)
assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR

View File

@ -0,0 +1,207 @@
"""Test the yale config flow."""
from collections.abc import Generator
from unittest.mock import Mock, patch
import pytest
from homeassistant import config_entries
from homeassistant.components.yale.application_credentials import (
OAUTH2_AUTHORIZE,
OAUTH2_TOKEN,
)
from homeassistant.components.yale.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import config_entry_oauth2_flow
from .mocks import USER_ID
from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMocker
from tests.typing import ClientSessionGenerator
CLIENT_ID = "1"
@pytest.fixture
def mock_setup_entry() -> Generator[Mock, None, None]:
"""Patch setup entry."""
with patch(
"homeassistant.components.yale.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry
@pytest.mark.usefixtures("client_credentials")
@pytest.mark.usefixtures("current_request_with_host")
async def test_full_flow(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
jwt: str,
mock_setup_entry: Mock,
) -> None:
"""Check full flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
state = config_entry_oauth2_flow._encode_jwt(
hass,
{
"flow_id": result["flow_id"],
"redirect_uri": "https://example.com/auth/external/callback",
},
)
assert result["url"] == (
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
"&redirect_uri=https://example.com/auth/external/callback"
f"&state={state}"
)
client = await hass_client_no_auth()
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
assert resp.status == 200
assert resp.headers["content-type"] == "text/html; charset=utf-8"
aioclient_mock.clear_requests()
aioclient_mock.post(
OAUTH2_TOKEN,
json={
"access_token": jwt,
"scope": "any",
"expires_in": 86399,
"refresh_token": "mock-refresh-token",
"user_id": "mock-user-id",
"expires_at": 1697753347,
},
)
await hass.config_entries.flow.async_configure(result["flow_id"])
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert len(mock_setup_entry.mock_calls) == 1
entry = hass.config_entries.async_entries(DOMAIN)[0]
assert entry.unique_id == USER_ID
@pytest.mark.usefixtures("client_credentials")
@pytest.mark.usefixtures("current_request_with_host")
async def test_reauth(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
mock_config_entry: MockConfigEntry,
reauth_jwt: str,
mock_setup_entry: Mock,
) -> None:
"""Test the reauthentication case updates the existing config entry."""
mock_config_entry.add_to_hass(hass)
mock_config_entry.async_start_reauth(hass)
await hass.async_block_till_done()
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
result = flows[0]
assert result["step_id"] == "auth"
state = config_entry_oauth2_flow._encode_jwt(
hass,
{
"flow_id": result["flow_id"],
"redirect_uri": "https://example.com/auth/external/callback",
},
)
client = await hass_client_no_auth()
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
assert resp.status == 200
assert resp.headers["content-type"] == "text/html; charset=utf-8"
aioclient_mock.post(
OAUTH2_TOKEN,
json={
"access_token": reauth_jwt,
"expires_in": 86399,
"refresh_token": "mock-refresh-token",
"user_id": USER_ID,
"token_type": "Bearer",
"expires_at": 1697753347,
},
)
result = await hass.config_entries.flow.async_configure(result["flow_id"])
await hass.async_block_till_done()
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
assert mock_config_entry.unique_id == USER_ID
assert "token" in mock_config_entry.data
# Verify access token is refreshed
assert mock_config_entry.data["token"]["access_token"] == reauth_jwt
@pytest.mark.usefixtures("client_credentials")
@pytest.mark.usefixtures("current_request_with_host")
async def test_reauth_wrong_account(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
mock_config_entry: MockConfigEntry,
reauth_jwt_wrong_account: str,
jwt: str,
mock_setup_entry: Mock,
) -> None:
"""Test the reauthentication aborts, if user tries to reauthenticate with another account."""
assert mock_config_entry.data["token"]["access_token"] == jwt
mock_config_entry.add_to_hass(hass)
mock_config_entry.async_start_reauth(hass)
await hass.async_block_till_done()
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
result = flows[0]
assert result["step_id"] == "auth"
state = config_entry_oauth2_flow._encode_jwt(
hass,
{
"flow_id": result["flow_id"],
"redirect_uri": "https://example.com/auth/external/callback",
},
)
client = await hass_client_no_auth()
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
assert resp.status == 200
assert resp.headers["content-type"] == "text/html; charset=utf-8"
aioclient_mock.post(
OAUTH2_TOKEN,
json={
"access_token": reauth_jwt_wrong_account,
"expires_in": 86399,
"refresh_token": "mock-refresh-token",
"token_type": "Bearer",
"expires_at": 1697753347,
},
)
result = await hass.config_entries.flow.async_configure(result["flow_id"])
await hass.async_block_till_done()
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_invalid_user"
assert mock_config_entry.unique_id == USER_ID
assert "token" in mock_config_entry.data
# Verify access token is like before
assert mock_config_entry.data["token"]["access_token"] == jwt

View File

@ -0,0 +1,31 @@
"""Test yale diagnostics."""
from syrupy import SnapshotAssertion
from homeassistant.core import HomeAssistant
from .mocks import (
_create_yale_api_with_devices,
_mock_doorbell_from_fixture,
_mock_lock_from_fixture,
)
from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator
async def test_diagnostics(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
snapshot: SnapshotAssertion,
) -> None:
"""Test generating diagnostics for a config entry."""
lock_one = await _mock_lock_from_fixture(
hass, "get_lock.online_with_doorsense.json"
)
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json")
entry, _, _ = await _create_yale_api_with_devices(hass, [lock_one, doorbell_one])
diag = await get_diagnostics_for_config_entry(hass, hass_client, entry)
assert diag == snapshot

View File

@ -0,0 +1,174 @@
"""The event tests for the yale."""
import datetime
from unittest.mock import patch
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
import homeassistant.util.dt as dt_util
from .mocks import (
_create_yale_with_devices,
_mock_activities_from_fixture,
_mock_doorbell_from_fixture,
_mock_lock_from_fixture,
)
from tests.common import async_fire_time_changed
async def test_create_doorbell(hass: HomeAssistant) -> None:
"""Test creation of a doorbell."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json")
await _create_yale_with_devices(hass, [doorbell_one])
motion_state = hass.states.get("event.k98gidt45gul_name_motion")
assert motion_state is not None
assert motion_state.state == STATE_UNKNOWN
doorbell_state = hass.states.get("event.k98gidt45gul_name_doorbell")
assert doorbell_state is not None
assert doorbell_state.state == STATE_UNKNOWN
async def test_create_doorbell_offline(hass: HomeAssistant) -> None:
"""Test creation of a doorbell that is offline."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.offline.json")
await _create_yale_with_devices(hass, [doorbell_one])
motion_state = hass.states.get("event.tmt100_name_motion")
assert motion_state is not None
assert motion_state.state == STATE_UNAVAILABLE
doorbell_state = hass.states.get("event.tmt100_name_doorbell")
assert doorbell_state is not None
assert doorbell_state.state == STATE_UNAVAILABLE
async def test_create_doorbell_with_motion(hass: HomeAssistant) -> None:
"""Test creation of a doorbell."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json")
activities = await _mock_activities_from_fixture(
hass, "get_activity.doorbell_motion.json"
)
await _create_yale_with_devices(hass, [doorbell_one], activities=activities)
motion_state = hass.states.get("event.k98gidt45gul_name_motion")
assert motion_state is not None
assert motion_state.state != STATE_UNKNOWN
isotime = motion_state.state
doorbell_state = hass.states.get("event.k98gidt45gul_name_doorbell")
assert doorbell_state is not None
assert doorbell_state.state == STATE_UNKNOWN
new_time = dt_util.utcnow() + datetime.timedelta(seconds=40)
native_time = datetime.datetime.now() + datetime.timedelta(seconds=40)
with patch(
"homeassistant.components.yale.util._native_datetime",
return_value=native_time,
):
async_fire_time_changed(hass, new_time)
await hass.async_block_till_done()
motion_state = hass.states.get("event.k98gidt45gul_name_motion")
assert motion_state.state == isotime
async def test_doorbell_update_via_socketio(hass: HomeAssistant) -> None:
"""Test creation of a doorbell that can be updated via socketio."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json")
_, socketio = await _create_yale_with_devices(hass, [doorbell_one])
assert doorbell_one.pubsub_channel == "7c7a6672-59c8-3333-ffff-dcd98705cccc"
motion_state = hass.states.get("event.k98gidt45gul_name_motion")
assert motion_state is not None
assert motion_state.state == STATE_UNKNOWN
doorbell_state = hass.states.get("event.k98gidt45gul_name_doorbell")
assert doorbell_state is not None
assert doorbell_state.state == STATE_UNKNOWN
listener = list(socketio._listeners)[0]
listener(
doorbell_one.device_id,
dt_util.utcnow(),
{
"status": "doorbell_motion_detected",
"data": {
"event": "doorbell_motion_detected",
"image": {
"height": 640,
"width": 480,
"format": "jpg",
"created_at": "2021-03-16T02:36:26.886Z",
"bytes": 14061,
"secure_url": (
"https://dyu7azbnaoi74.cloudfront.net/images/1f8.jpeg"
),
"url": "https://dyu7azbnaoi74.cloudfront.net/images/1f8.jpeg",
"etag": "09e839331c4ea59eef28081f2caa0e90",
},
"doorbellName": "Front Door",
"callID": None,
"origin": "mars-api",
"mutableContent": True,
},
},
)
await hass.async_block_till_done()
motion_state = hass.states.get("event.k98gidt45gul_name_motion")
assert motion_state is not None
assert motion_state.state != STATE_UNKNOWN
isotime = motion_state.state
new_time = dt_util.utcnow() + datetime.timedelta(seconds=40)
native_time = datetime.datetime.now() + datetime.timedelta(seconds=40)
with patch(
"homeassistant.components.yale.util._native_datetime",
return_value=native_time,
):
async_fire_time_changed(hass, new_time)
await hass.async_block_till_done()
motion_state = hass.states.get("event.k98gidt45gul_name_motion")
assert motion_state is not None
assert motion_state.state != STATE_UNKNOWN
listener(
doorbell_one.device_id,
dt_util.utcnow(),
{
"status": "buttonpush",
},
)
await hass.async_block_till_done()
doorbell_state = hass.states.get("event.k98gidt45gul_name_doorbell")
assert doorbell_state is not None
assert doorbell_state.state != STATE_UNKNOWN
isotime = motion_state.state
new_time = dt_util.utcnow() + datetime.timedelta(seconds=40)
native_time = datetime.datetime.now() + datetime.timedelta(seconds=40)
with patch(
"homeassistant.components.yale.util._native_datetime",
return_value=native_time,
):
async_fire_time_changed(hass, new_time)
await hass.async_block_till_done()
doorbell_state = hass.states.get("event.k98gidt45gul_name_doorbell")
assert doorbell_state is not None
assert doorbell_state.state != STATE_UNKNOWN
assert motion_state.state == isotime
async def test_create_lock_with_doorbell(hass: HomeAssistant) -> None:
"""Test creation of a lock with a doorbell."""
lock_one = await _mock_lock_from_fixture(hass, "lock_with_doorbell.online.json")
await _create_yale_with_devices(hass, [lock_one])
doorbell_state = hass.states.get(
"event.a6697750d607098bae8d6baa11ef8063_name_doorbell"
)
assert doorbell_state is not None
assert doorbell_state.state == STATE_UNKNOWN

View File

@ -0,0 +1,238 @@
"""The tests for the yale platform."""
from unittest.mock import Mock
from aiohttp import ClientResponseError
import pytest
from yalexs.exceptions import InvalidAuth, YaleApiError
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.components.yale.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_LOCK,
SERVICE_OPEN,
SERVICE_UNLOCK,
STATE_LOCKED,
STATE_ON,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from .mocks import (
_create_yale_with_devices,
_mock_doorsense_enabled_yale_lock_detail,
_mock_doorsense_missing_yale_lock_detail,
_mock_inoperative_yale_lock_detail,
_mock_lock_with_offline_key,
_mock_operative_yale_lock_detail,
)
from tests.typing import WebSocketGenerator
async def test_yale_api_is_failing(hass: HomeAssistant) -> None:
"""Config entry state is SETUP_RETRY when yale api is failing."""
config_entry, socketio = await _create_yale_with_devices(
hass,
authenticate_side_effect=YaleApiError(
"offline", ClientResponseError(None, None, status=500)
),
)
assert config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_yale_is_offline(hass: HomeAssistant) -> None:
"""Config entry state is SETUP_RETRY when yale is offline."""
config_entry, socketio = await _create_yale_with_devices(
hass, authenticate_side_effect=TimeoutError
)
assert config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_yale_late_auth_failure(hass: HomeAssistant) -> None:
"""Test we can detect a late auth failure."""
config_entry, socketio = await _create_yale_with_devices(
hass,
authenticate_side_effect=InvalidAuth(
"authfailed", ClientResponseError(None, None, status=401)
),
)
assert config_entry.state is ConfigEntryState.SETUP_ERROR
flows = hass.config_entries.flow.async_progress()
assert flows[0]["step_id"] == "pick_implementation"
async def test_unlock_throws_yale_api_http_error(hass: HomeAssistant) -> None:
"""Test unlock throws correct error on http error."""
mocked_lock_detail = await _mock_operative_yale_lock_detail(hass)
aiohttp_client_response_exception = ClientResponseError(None, None, status=400)
def _unlock_return_activities_side_effect(access_token, device_id):
raise YaleApiError(
"This should bubble up as its user consumable",
aiohttp_client_response_exception,
)
await _create_yale_with_devices(
hass,
[mocked_lock_detail],
api_call_side_effects={
"unlock_return_activities": _unlock_return_activities_side_effect
},
)
last_err = None
data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"}
try:
await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True)
except HomeAssistantError as err:
last_err = err
assert str(last_err) == (
"A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user"
" consumable"
)
async def test_lock_throws_yale_api_http_error(hass: HomeAssistant) -> None:
"""Test lock throws correct error on http error."""
mocked_lock_detail = await _mock_operative_yale_lock_detail(hass)
aiohttp_client_response_exception = ClientResponseError(None, None, status=400)
def _lock_return_activities_side_effect(access_token, device_id):
raise YaleApiError(
"This should bubble up as its user consumable",
aiohttp_client_response_exception,
)
await _create_yale_with_devices(
hass,
[mocked_lock_detail],
api_call_side_effects={
"lock_return_activities": _lock_return_activities_side_effect
},
)
last_err = None
data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"}
try:
await hass.services.async_call(LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True)
except HomeAssistantError as err:
last_err = err
assert str(last_err) == (
"A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user"
" consumable"
)
async def test_open_throws_hass_service_not_supported_error(
hass: HomeAssistant,
) -> None:
"""Test open throws correct error on entity does not support this service error."""
mocked_lock_detail = await _mock_operative_yale_lock_detail(hass)
await _create_yale_with_devices(hass, [mocked_lock_detail])
data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"}
with pytest.raises(HomeAssistantError):
await hass.services.async_call(LOCK_DOMAIN, SERVICE_OPEN, data, blocking=True)
async def test_inoperative_locks_are_filtered_out(hass: HomeAssistant) -> None:
"""Ensure inoperative locks do not get setup."""
yale_operative_lock = await _mock_operative_yale_lock_detail(hass)
yale_inoperative_lock = await _mock_inoperative_yale_lock_detail(hass)
await _create_yale_with_devices(hass, [yale_operative_lock, yale_inoperative_lock])
lock_abc_name = hass.states.get("lock.abc_name")
assert lock_abc_name is None
lock_a6697750d607098bae8d6baa11ef8063_name = hass.states.get(
"lock.a6697750d607098bae8d6baa11ef8063_name"
)
assert lock_a6697750d607098bae8d6baa11ef8063_name.state == STATE_LOCKED
async def test_lock_has_doorsense(hass: HomeAssistant) -> None:
"""Check to see if a lock has doorsense."""
doorsenselock = await _mock_doorsense_enabled_yale_lock_detail(hass)
nodoorsenselock = await _mock_doorsense_missing_yale_lock_detail(hass)
await _create_yale_with_devices(hass, [doorsenselock, nodoorsenselock])
binary_sensor_online_with_doorsense_name_open = hass.states.get(
"binary_sensor.online_with_doorsense_name_door"
)
assert binary_sensor_online_with_doorsense_name_open.state == STATE_ON
binary_sensor_missing_doorsense_id_name_open = hass.states.get(
"binary_sensor.missing_with_doorsense_name_door"
)
assert binary_sensor_missing_doorsense_id_name_open is None
async def test_load_unload(hass: HomeAssistant) -> None:
"""Config entry can be unloaded."""
yale_operative_lock = await _mock_operative_yale_lock_detail(hass)
yale_inoperative_lock = await _mock_inoperative_yale_lock_detail(hass)
config_entry, socketio = await _create_yale_with_devices(
hass, [yale_operative_lock, yale_inoperative_lock]
)
assert config_entry.state is ConfigEntryState.LOADED
await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()
async def test_load_triggers_ble_discovery(
hass: HomeAssistant, mock_discovery: Mock
) -> None:
"""Test that loading a lock that supports offline ble operation passes the keys to yalexe_ble."""
yale_lock_with_key = await _mock_lock_with_offline_key(hass)
yale_lock_without_key = await _mock_operative_yale_lock_detail(hass)
config_entry, socketio = await _create_yale_with_devices(
hass, [yale_lock_with_key, yale_lock_without_key]
)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
assert len(mock_discovery.mock_calls) == 1
assert mock_discovery.mock_calls[0].kwargs["data"] == {
"name": "Front Door Lock",
"address": None,
"serial": "X2FSW05DGA",
"key": "kkk01d4300c1dcxxx1c330f794941111",
"slot": 1,
}
async def test_device_remove_devices(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test we can only remove a device that no longer exists."""
assert await async_setup_component(hass, "config", {})
yale_operative_lock = await _mock_operative_yale_lock_detail(hass)
config_entry, socketio = await _create_yale_with_devices(
hass, [yale_operative_lock]
)
entity = entity_registry.entities["lock.a6697750d607098bae8d6baa11ef8063_name"]
device_entry = device_registry.async_get(entity.device_id)
client = await hass_ws_client(hass)
response = await client.remove_device(device_entry.id, config_entry.entry_id)
assert not response["success"]
dead_device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(DOMAIN, "remove-device-id")},
)
response = await client.remove_device(dead_device_entry.id, config_entry.entry_id)
assert response["success"]

View File

@ -0,0 +1,501 @@
"""The lock tests for the yale platform."""
import datetime
from aiohttp import ClientResponseError
from freezegun.api import FrozenDateTimeFactory
import pytest
from yalexs.manager.activity import INITIAL_LOCK_RESYNC_TIME
from homeassistant.components.lock import (
DOMAIN as LOCK_DOMAIN,
STATE_JAMMED,
STATE_LOCKING,
STATE_UNLOCKING,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_LOCK,
SERVICE_OPEN,
SERVICE_UNLOCK,
STATE_LOCKED,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
STATE_UNLOCKED,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
import homeassistant.util.dt as dt_util
from .mocks import (
_create_yale_with_devices,
_mock_activities_from_fixture,
_mock_doorsense_enabled_yale_lock_detail,
_mock_lock_from_fixture,
_mock_lock_with_unlatch,
_mock_operative_yale_lock_detail,
)
from tests.common import async_fire_time_changed
async def test_lock_device_registry(
hass: HomeAssistant, device_registry: dr.DeviceRegistry
) -> None:
"""Test creation of a lock with doorsense and bridge ands up in the registry."""
lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass)
await _create_yale_with_devices(hass, [lock_one])
reg_device = device_registry.async_get_device(
identifiers={("yale", "online_with_doorsense")}
)
assert reg_device.model == "AUG-MD01"
assert reg_device.sw_version == "undefined-4.3.0-1.8.14"
assert reg_device.name == "online_with_doorsense Name"
assert reg_device.manufacturer == "Yale Home Inc."
async def test_lock_changed_by(hass: HomeAssistant) -> None:
"""Test creation of a lock with doorsense and bridge."""
lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass)
activities = await _mock_activities_from_fixture(hass, "get_activity.lock.json")
await _create_yale_with_devices(hass, [lock_one], activities=activities)
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_LOCKED
assert (
lock_online_with_doorsense_name.attributes.get("changed_by")
== "Your favorite elven princess"
)
async def test_state_locking(hass: HomeAssistant) -> None:
"""Test creation of a lock with doorsense and bridge that is locking."""
lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass)
activities = await _mock_activities_from_fixture(hass, "get_activity.locking.json")
await _create_yale_with_devices(hass, [lock_one], activities=activities)
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_LOCKING
async def test_state_unlocking(hass: HomeAssistant) -> None:
"""Test creation of a lock with doorsense and bridge that is unlocking."""
lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass)
activities = await _mock_activities_from_fixture(
hass, "get_activity.unlocking.json"
)
await _create_yale_with_devices(hass, [lock_one], activities=activities)
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_UNLOCKING
async def test_state_jammed(hass: HomeAssistant) -> None:
"""Test creation of a lock with doorsense and bridge that is jammed."""
lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass)
activities = await _mock_activities_from_fixture(hass, "get_activity.jammed.json")
await _create_yale_with_devices(hass, [lock_one], activities=activities)
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_JAMMED
async def test_one_lock_operation(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test creation of a lock with doorsense and bridge."""
lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass)
await _create_yale_with_devices(hass, [lock_one])
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_LOCKED
assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92
assert (
lock_online_with_doorsense_name.attributes.get("friendly_name")
== "online_with_doorsense Name"
)
data = {ATTR_ENTITY_ID: "lock.online_with_doorsense_name"}
await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True)
await hass.async_block_till_done()
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_UNLOCKED
assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92
assert (
lock_online_with_doorsense_name.attributes.get("friendly_name")
== "online_with_doorsense Name"
)
await hass.services.async_call(LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True)
await hass.async_block_till_done()
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_LOCKED
# No activity means it will be unavailable until the activity feed has data
lock_operator_sensor = entity_registry.async_get(
"sensor.online_with_doorsense_name_operator"
)
assert lock_operator_sensor
assert (
hass.states.get("sensor.online_with_doorsense_name_operator").state
== STATE_UNKNOWN
)
async def test_open_lock_operation(hass: HomeAssistant) -> None:
"""Test open lock operation using the open service."""
lock_with_unlatch = await _mock_lock_with_unlatch(hass)
await _create_yale_with_devices(hass, [lock_with_unlatch])
lock_online_with_unlatch_name = hass.states.get("lock.online_with_unlatch_name")
assert lock_online_with_unlatch_name.state == STATE_LOCKED
data = {ATTR_ENTITY_ID: "lock.online_with_unlatch_name"}
await hass.services.async_call(LOCK_DOMAIN, SERVICE_OPEN, data, blocking=True)
await hass.async_block_till_done()
lock_online_with_unlatch_name = hass.states.get("lock.online_with_unlatch_name")
assert lock_online_with_unlatch_name.state == STATE_UNLOCKED
async def test_open_lock_operation_socketio_connected(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test open lock operation using the open service when socketio is connected."""
lock_with_unlatch = await _mock_lock_with_unlatch(hass)
assert lock_with_unlatch.pubsub_channel == "pubsub"
_, socketio = await _create_yale_with_devices(hass, [lock_with_unlatch])
socketio.connected = True
lock_online_with_unlatch_name = hass.states.get("lock.online_with_unlatch_name")
assert lock_online_with_unlatch_name.state == STATE_LOCKED
data = {ATTR_ENTITY_ID: "lock.online_with_unlatch_name"}
await hass.services.async_call(LOCK_DOMAIN, SERVICE_OPEN, data, blocking=True)
await hass.async_block_till_done()
listener = list(socketio._listeners)[0]
listener(
lock_with_unlatch.device_id,
dt_util.utcnow() + datetime.timedelta(seconds=2),
{
"status": "kAugLockState_Unlocked",
},
)
await hass.async_block_till_done()
await hass.async_block_till_done()
lock_online_with_unlatch_name = hass.states.get("lock.online_with_unlatch_name")
assert lock_online_with_unlatch_name.state == STATE_UNLOCKED
await hass.async_block_till_done()
async def test_one_lock_operation_socketio_connected(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test lock and unlock operations are async when socketio is connected."""
lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass)
assert lock_one.pubsub_channel == "pubsub"
_, socketio = await _create_yale_with_devices(hass, [lock_one])
socketio.connected = True
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_LOCKED
assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92
assert (
lock_online_with_doorsense_name.attributes.get("friendly_name")
== "online_with_doorsense Name"
)
data = {ATTR_ENTITY_ID: "lock.online_with_doorsense_name"}
await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True)
await hass.async_block_till_done()
listener = list(socketio._listeners)[0]
listener(
lock_one.device_id,
dt_util.utcnow() + datetime.timedelta(seconds=1),
{
"status": "kAugLockState_Unlocked",
},
)
await hass.async_block_till_done()
await hass.async_block_till_done()
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_UNLOCKED
assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92
assert (
lock_online_with_doorsense_name.attributes.get("friendly_name")
== "online_with_doorsense Name"
)
await hass.services.async_call(LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True)
await hass.async_block_till_done()
listener(
lock_one.device_id,
dt_util.utcnow() + datetime.timedelta(seconds=2),
{
"status": "kAugLockState_Locked",
},
)
await hass.async_block_till_done()
await hass.async_block_till_done()
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_LOCKED
# No activity means it will be unavailable until the activity feed has data
lock_operator_sensor = entity_registry.async_get(
"sensor.online_with_doorsense_name_operator"
)
assert lock_operator_sensor
assert (
hass.states.get("sensor.online_with_doorsense_name_operator").state
== STATE_UNKNOWN
)
freezer.tick(INITIAL_LOCK_RESYNC_TIME)
listener(
lock_one.device_id,
dt_util.utcnow() + datetime.timedelta(seconds=2),
{
"status": "kAugLockState_Unlocked",
},
)
await hass.async_block_till_done()
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_UNLOCKED
async def test_lock_jammed(hass: HomeAssistant) -> None:
"""Test lock gets jammed on unlock."""
def _unlock_return_activities_side_effect(access_token, device_id):
raise ClientResponseError(None, None, status=531)
lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass)
await _create_yale_with_devices(
hass,
[lock_one],
api_call_side_effects={
"unlock_return_activities": _unlock_return_activities_side_effect
},
)
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_LOCKED
assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92
assert (
lock_online_with_doorsense_name.attributes.get("friendly_name")
== "online_with_doorsense Name"
)
data = {ATTR_ENTITY_ID: "lock.online_with_doorsense_name"}
await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True)
await hass.async_block_till_done()
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_JAMMED
async def test_lock_throws_exception_on_unknown_status_code(
hass: HomeAssistant,
) -> None:
"""Test lock throws exception."""
def _unlock_return_activities_side_effect(access_token, device_id):
raise ClientResponseError(None, None, status=500)
lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass)
await _create_yale_with_devices(
hass,
[lock_one],
api_call_side_effects={
"unlock_return_activities": _unlock_return_activities_side_effect
},
)
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_LOCKED
assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92
assert (
lock_online_with_doorsense_name.attributes.get("friendly_name")
== "online_with_doorsense Name"
)
data = {ATTR_ENTITY_ID: "lock.online_with_doorsense_name"}
with pytest.raises(ClientResponseError):
await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True)
async def test_one_lock_unknown_state(hass: HomeAssistant) -> None:
"""Test creation of a lock with doorsense and bridge."""
lock_one = await _mock_lock_from_fixture(
hass,
"get_lock.online.unknown_state.json",
)
await _create_yale_with_devices(hass, [lock_one])
lock_brokenid_name = hass.states.get("lock.brokenid_name")
assert lock_brokenid_name.state == STATE_UNKNOWN
async def test_lock_bridge_offline(hass: HomeAssistant) -> None:
"""Test creation of a lock with doorsense and bridge that goes offline."""
lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass)
activities = await _mock_activities_from_fixture(
hass, "get_activity.bridge_offline.json"
)
await _create_yale_with_devices(hass, [lock_one], activities=activities)
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_UNAVAILABLE
async def test_lock_bridge_online(hass: HomeAssistant) -> None:
"""Test creation of a lock with doorsense and bridge that goes offline."""
lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass)
activities = await _mock_activities_from_fixture(
hass, "get_activity.bridge_online.json"
)
await _create_yale_with_devices(hass, [lock_one], activities=activities)
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_LOCKED
async def test_lock_update_via_socketio(hass: HomeAssistant) -> None:
"""Test creation of a lock with doorsense and bridge."""
lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass)
assert lock_one.pubsub_channel == "pubsub"
activities = await _mock_activities_from_fixture(hass, "get_activity.lock.json")
config_entry, socketio = await _create_yale_with_devices(
hass, [lock_one], activities=activities
)
socketio.connected = True
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_LOCKED
listener = list(socketio._listeners)[0]
listener(
lock_one.device_id,
dt_util.utcnow(),
{
"status": "kAugLockState_Unlocking",
},
)
await hass.async_block_till_done()
await hass.async_block_till_done()
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_UNLOCKING
listener(
lock_one.device_id,
dt_util.utcnow(),
{
"status": "kAugLockState_Locking",
},
)
await hass.async_block_till_done()
await hass.async_block_till_done()
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_LOCKING
async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30))
await hass.async_block_till_done()
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_LOCKING
socketio.connected = True
async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30))
await hass.async_block_till_done()
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_LOCKING
# Ensure socketio status is always preserved
async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=2))
await hass.async_block_till_done()
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_LOCKING
listener(
lock_one.device_id,
dt_util.utcnow() + datetime.timedelta(seconds=2),
{
"status": "kAugLockState_Unlocking",
},
)
await hass.async_block_till_done()
await hass.async_block_till_done()
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_UNLOCKING
async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=4))
await hass.async_block_till_done()
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_UNLOCKING
await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()
async def test_open_throws_hass_service_not_supported_error(
hass: HomeAssistant,
) -> None:
"""Test open throws correct error on entity does not support this service error."""
mocked_lock_detail = await _mock_operative_yale_lock_detail(hass)
await _create_yale_with_devices(hass, [mocked_lock_detail])
data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"}
with pytest.raises(HomeAssistantError, match="does not support this service"):
await hass.services.async_call(LOCK_DOMAIN, SERVICE_OPEN, data, blocking=True)

View File

@ -0,0 +1,362 @@
"""The sensor tests for the yale platform."""
from typing import Any
from homeassistant import core as ha
from homeassistant.const import (
ATTR_ENTITY_PICTURE,
ATTR_UNIT_OF_MEASUREMENT,
PERCENTAGE,
STATE_UNKNOWN,
)
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.helpers import entity_registry as er
from .mocks import (
_create_yale_with_devices,
_mock_activities_from_fixture,
_mock_doorbell_from_fixture,
_mock_doorsense_enabled_yale_lock_detail,
_mock_lock_from_fixture,
)
from tests.common import mock_restore_cache_with_extra_data
async def test_create_doorbell(hass: HomeAssistant) -> None:
"""Test creation of a doorbell."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json")
await _create_yale_with_devices(hass, [doorbell_one])
sensor_k98gidt45gul_name_battery = hass.states.get(
"sensor.k98gidt45gul_name_battery"
)
assert sensor_k98gidt45gul_name_battery.state == "96"
assert (
sensor_k98gidt45gul_name_battery.attributes["unit_of_measurement"] == PERCENTAGE
)
async def test_create_doorbell_offline(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test creation of a doorbell that is offline."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.offline.json")
await _create_yale_with_devices(hass, [doorbell_one])
sensor_tmt100_name_battery = hass.states.get("sensor.tmt100_name_battery")
assert sensor_tmt100_name_battery.state == "81"
assert sensor_tmt100_name_battery.attributes["unit_of_measurement"] == PERCENTAGE
entry = entity_registry.async_get("sensor.tmt100_name_battery")
assert entry
assert entry.unique_id == "tmt100_device_battery"
async def test_create_doorbell_hardwired(hass: HomeAssistant) -> None:
"""Test creation of a doorbell that is hardwired without a battery."""
doorbell_one = await _mock_doorbell_from_fixture(
hass, "get_doorbell.nobattery.json"
)
await _create_yale_with_devices(hass, [doorbell_one])
sensor_tmt100_name_battery = hass.states.get("sensor.tmt100_name_battery")
assert sensor_tmt100_name_battery is None
async def test_create_lock_with_linked_keypad(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test creation of a lock with a linked keypad that both have a battery."""
lock_one = await _mock_lock_from_fixture(hass, "get_lock.doorsense_init.json")
await _create_yale_with_devices(hass, [lock_one])
sensor_a6697750d607098bae8d6baa11ef8063_name_battery = hass.states.get(
"sensor.a6697750d607098bae8d6baa11ef8063_name_battery"
)
assert sensor_a6697750d607098bae8d6baa11ef8063_name_battery.state == "88"
assert (
sensor_a6697750d607098bae8d6baa11ef8063_name_battery.attributes[
"unit_of_measurement"
]
== PERCENTAGE
)
entry = entity_registry.async_get(
"sensor.a6697750d607098bae8d6baa11ef8063_name_battery"
)
assert entry
assert entry.unique_id == "A6697750D607098BAE8D6BAA11EF8063_device_battery"
state = hass.states.get("sensor.front_door_lock_keypad_battery")
assert state.state == "62"
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE
entry = entity_registry.async_get("sensor.front_door_lock_keypad_battery")
assert entry
assert entry.unique_id == "5bc65c24e6ef2a263e1450a8_linked_keypad_battery"
async def test_create_lock_with_low_battery_linked_keypad(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test creation of a lock with a linked keypad that both have a battery."""
lock_one = await _mock_lock_from_fixture(hass, "get_lock.low_keypad_battery.json")
await _create_yale_with_devices(hass, [lock_one])
sensor_a6697750d607098bae8d6baa11ef8063_name_battery = hass.states.get(
"sensor.a6697750d607098bae8d6baa11ef8063_name_battery"
)
assert sensor_a6697750d607098bae8d6baa11ef8063_name_battery.state == "88"
assert (
sensor_a6697750d607098bae8d6baa11ef8063_name_battery.attributes[
"unit_of_measurement"
]
== PERCENTAGE
)
entry = entity_registry.async_get(
"sensor.a6697750d607098bae8d6baa11ef8063_name_battery"
)
assert entry
assert entry.unique_id == "A6697750D607098BAE8D6BAA11EF8063_device_battery"
state = hass.states.get("sensor.front_door_lock_keypad_battery")
assert state.state == "10"
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE
entry = entity_registry.async_get("sensor.front_door_lock_keypad_battery")
assert entry
assert entry.unique_id == "5bc65c24e6ef2a263e1450a8_linked_keypad_battery"
# No activity means it will be unavailable until someone unlocks/locks it
lock_operator_sensor = entity_registry.async_get(
"sensor.a6697750d607098bae8d6baa11ef8063_name_operator"
)
assert (
lock_operator_sensor.unique_id
== "A6697750D607098BAE8D6BAA11EF8063_lock_operator"
)
assert (
hass.states.get("sensor.a6697750d607098bae8d6baa11ef8063_name_operator").state
== STATE_UNKNOWN
)
async def test_lock_operator_bluetooth(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test operation of a lock with doorsense and bridge."""
lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass)
activities = await _mock_activities_from_fixture(
hass, "get_activity.lock_from_bluetooth.json"
)
await _create_yale_with_devices(hass, [lock_one], activities=activities)
lock_operator_sensor = entity_registry.async_get(
"sensor.online_with_doorsense_name_operator"
)
assert lock_operator_sensor
state = hass.states.get("sensor.online_with_doorsense_name_operator")
assert state.state == "Your favorite elven princess"
assert state.attributes["manual"] is False
assert state.attributes["tag"] is False
assert state.attributes["remote"] is False
assert state.attributes["keypad"] is False
assert state.attributes["autorelock"] is False
assert state.attributes["method"] == "mobile"
async def test_lock_operator_keypad(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test operation of a lock with doorsense and bridge."""
lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass)
activities = await _mock_activities_from_fixture(
hass, "get_activity.lock_from_keypad.json"
)
await _create_yale_with_devices(hass, [lock_one], activities=activities)
lock_operator_sensor = entity_registry.async_get(
"sensor.online_with_doorsense_name_operator"
)
assert lock_operator_sensor
state = hass.states.get("sensor.online_with_doorsense_name_operator")
assert state.state == "Your favorite elven princess"
assert state.attributes["manual"] is False
assert state.attributes["tag"] is False
assert state.attributes["remote"] is False
assert state.attributes["keypad"] is True
assert state.attributes["autorelock"] is False
assert state.attributes["method"] == "keypad"
async def test_lock_operator_remote(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test operation of a lock with doorsense and bridge."""
lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass)
activities = await _mock_activities_from_fixture(hass, "get_activity.lock.json")
await _create_yale_with_devices(hass, [lock_one], activities=activities)
lock_operator_sensor = entity_registry.async_get(
"sensor.online_with_doorsense_name_operator"
)
assert lock_operator_sensor
state = hass.states.get("sensor.online_with_doorsense_name_operator")
assert state.state == "Your favorite elven princess"
assert state.attributes["manual"] is False
assert state.attributes["tag"] is False
assert state.attributes["remote"] is True
assert state.attributes["keypad"] is False
assert state.attributes["autorelock"] is False
assert state.attributes["method"] == "remote"
async def test_lock_operator_manual(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test operation of a lock with doorsense and bridge."""
lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass)
activities = await _mock_activities_from_fixture(
hass, "get_activity.lock_from_manual.json"
)
await _create_yale_with_devices(hass, [lock_one], activities=activities)
lock_operator_sensor = entity_registry.async_get(
"sensor.online_with_doorsense_name_operator"
)
assert lock_operator_sensor
state = hass.states.get("sensor.online_with_doorsense_name_operator")
assert state.state == "Your favorite elven princess"
assert state.attributes["manual"] is True
assert state.attributes["tag"] is False
assert state.attributes["remote"] is False
assert state.attributes["keypad"] is False
assert state.attributes["autorelock"] is False
assert state.attributes["method"] == "manual"
async def test_lock_operator_autorelock(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test operation of a lock with doorsense and bridge."""
lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass)
activities = await _mock_activities_from_fixture(
hass, "get_activity.lock_from_autorelock.json"
)
await _create_yale_with_devices(hass, [lock_one], activities=activities)
lock_operator_sensor = entity_registry.async_get(
"sensor.online_with_doorsense_name_operator"
)
assert lock_operator_sensor
state = hass.states.get("sensor.online_with_doorsense_name_operator")
assert state.state == "Auto Relock"
assert state.attributes["manual"] is False
assert state.attributes["tag"] is False
assert state.attributes["remote"] is False
assert state.attributes["keypad"] is False
assert state.attributes["autorelock"] is True
assert state.attributes["method"] == "autorelock"
async def test_unlock_operator_manual(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test operation of a lock manually."""
lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass)
activities = await _mock_activities_from_fixture(
hass, "get_activity.unlock_from_manual.json"
)
await _create_yale_with_devices(hass, [lock_one], activities=activities)
lock_operator_sensor = entity_registry.async_get(
"sensor.online_with_doorsense_name_operator"
)
assert lock_operator_sensor
state = hass.states.get("sensor.online_with_doorsense_name_operator")
assert state.state == "Your favorite elven princess"
assert state.attributes["manual"] is True
assert state.attributes["tag"] is False
assert state.attributes["remote"] is False
assert state.attributes["keypad"] is False
assert state.attributes["autorelock"] is False
assert state.attributes["method"] == "manual"
async def test_unlock_operator_tag(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test operation of a lock with a tag."""
lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass)
activities = await _mock_activities_from_fixture(
hass, "get_activity.unlock_from_tag.json"
)
await _create_yale_with_devices(hass, [lock_one], activities=activities)
lock_operator_sensor = entity_registry.async_get(
"sensor.online_with_doorsense_name_operator"
)
assert lock_operator_sensor
state = hass.states.get("sensor.online_with_doorsense_name_operator")
assert state.state == "Your favorite elven princess"
assert state.attributes["manual"] is False
assert state.attributes["tag"] is True
assert state.attributes["remote"] is False
assert state.attributes["keypad"] is False
assert state.attributes["autorelock"] is False
assert state.attributes["method"] == "tag"
async def test_restored_state(
hass: HomeAssistant, hass_storage: dict[str, Any]
) -> None:
"""Test restored state."""
entity_id = "sensor.online_with_doorsense_name_operator"
lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass)
fake_state = ha.State(
entity_id,
state="Tag Unlock",
attributes={
"method": "tag",
"manual": False,
"remote": False,
"keypad": False,
"tag": True,
"autorelock": False,
ATTR_ENTITY_PICTURE: "image.png",
},
)
# Home assistant is not running yet
hass.set_state(CoreState.not_running)
mock_restore_cache_with_extra_data(
hass,
[
(
fake_state,
{"native_value": "Tag Unlock", "native_unit_of_measurement": None},
)
],
)
await _create_yale_with_devices(hass, [lock_one])
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == "Tag Unlock"
assert state.attributes["method"] == "tag"
assert state.attributes[ATTR_ENTITY_PICTURE] == "image.png"