mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 17:27:52 +00:00
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:
parent
edad766fd3
commit
03ead27f6c
@ -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
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
@ -4,10 +4,6 @@
|
||||
"codeowners": ["@bdraco"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{
|
||||
"hostname": "yale-connect-plus",
|
||||
"macaddress": "00177A*"
|
||||
},
|
||||
{
|
||||
"hostname": "connect",
|
||||
"macaddress": "D86162*"
|
||||
|
81
homeassistant/components/yale/__init__.py
Normal file
81
homeassistant/components/yale/__init__.py
Normal 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])
|
||||
)
|
15
homeassistant/components/yale/application_credentials.py
Normal file
15
homeassistant/components/yale/application_credentials.py
Normal 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,
|
||||
)
|
189
homeassistant/components/yale/binary_sensor.py
Normal file
189
homeassistant/components/yale/binary_sensor.py
Normal 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()
|
32
homeassistant/components/yale/button.py
Normal file
32
homeassistant/components/yale/button.py
Normal 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."""
|
90
homeassistant/components/yale/camera.py
Normal file
90
homeassistant/components/yale/camera.py
Normal 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
|
57
homeassistant/components/yale/config_flow.py
Normal file
57
homeassistant/components/yale/config_flow.py
Normal 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)
|
47
homeassistant/components/yale/const.py
Normal file
47
homeassistant/components/yale/const.py
Normal 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,
|
||||
]
|
52
homeassistant/components/yale/data.py
Normal file
52
homeassistant/components/yale/data.py
Normal 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])
|
49
homeassistant/components/yale/diagnostics.py
Normal file
49
homeassistant/components/yale/diagnostics.py
Normal 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),
|
||||
}
|
115
homeassistant/components/yale/entity.py
Normal file
115
homeassistant/components/yale/entity.py
Normal 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)]
|
104
homeassistant/components/yale/event.py
Normal file
104
homeassistant/components/yale/event.py
Normal 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()
|
43
homeassistant/components/yale/gateway.py
Normal file
43
homeassistant/components/yale/gateway.py
Normal 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
|
9
homeassistant/components/yale/icons.json
Normal file
9
homeassistant/components/yale/icons.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"image_capture": {
|
||||
"default": "mdi:file-image"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
147
homeassistant/components/yale/lock.py
Normal file
147
homeassistant/components/yale/lock.py
Normal 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]
|
16
homeassistant/components/yale/manifest.json
Normal file
16
homeassistant/components/yale/manifest.json
Normal 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"]
|
||||
}
|
210
homeassistant/components/yale/sensor.py
Normal file
210
homeassistant/components/yale/sensor.py
Normal 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
|
71
homeassistant/components/yale/strings.json
Normal file
71
homeassistant/components/yale/strings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
83
homeassistant/components/yale/util.py
Normal file
83
homeassistant/components/yale/util.py
Normal 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
|
@ -29,6 +29,7 @@ APPLICATION_CREDENTIALS = [
|
||||
"twitch",
|
||||
"withings",
|
||||
"xbox",
|
||||
"yale",
|
||||
"yolink",
|
||||
"youtube",
|
||||
]
|
||||
|
@ -664,6 +664,7 @@ FLOWS = {
|
||||
"xiaomi_aqara",
|
||||
"xiaomi_ble",
|
||||
"xiaomi_miio",
|
||||
"yale",
|
||||
"yale_smart_alarm",
|
||||
"yalexs_ble",
|
||||
"yamaha_musiccast",
|
||||
|
@ -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-*",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
12
tests/components/yale/__init__.py
Normal file
12
tests/components/yale/__init__.py
Normal 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",
|
||||
},
|
||||
}
|
59
tests/components/yale/conftest.py
Normal file
59
tests/components/yale/conftest.py
Normal 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)
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
@ -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"
|
||||
}
|
||||
]
|
36
tests/components/yale/fixtures/get_activity.jammed.json
Normal file
36
tests/components/yale/fixtures/get_activity.jammed.json
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
36
tests/components/yale/fixtures/get_activity.lock.json
Normal file
36
tests/components/yale/fixtures/get_activity.lock.json
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
36
tests/components/yale/fixtures/get_activity.locking.json
Normal file
36
tests/components/yale/fixtures/get_activity.locking.json
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
36
tests/components/yale/fixtures/get_activity.unlocking.json
Normal file
36
tests/components/yale/fixtures/get_activity.unlocking.json
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
81
tests/components/yale/fixtures/get_doorbell.json
Normal file
81
tests/components/yale/fixtures/get_doorbell.json
Normal 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"
|
||||
}
|
78
tests/components/yale/fixtures/get_doorbell.nobattery.json
Normal file
78
tests/components/yale/fixtures/get_doorbell.nobattery.json
Normal 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"
|
||||
}
|
126
tests/components/yale/fixtures/get_doorbell.offline.json
Normal file
126
tests/components/yale/fixtures/get_doorbell.offline.json
Normal 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"
|
||||
}
|
92
tests/components/yale/fixtures/get_lock.doorsense_init.json
Normal file
92
tests/components/yale/fixtures/get_lock.doorsense_init.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
57
tests/components/yale/fixtures/get_lock.offline.json
Normal file
57
tests/components/yale/fixtures/get_lock.offline.json
Normal 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
|
||||
}
|
92
tests/components/yale/fixtures/get_lock.online.json
Normal file
92
tests/components/yale/fixtures/get_lock.online.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
100
tests/components/yale/fixtures/get_lock.online_with_keys.json
Normal file
100
tests/components/yale/fixtures/get_lock.online_with_keys.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
16
tests/components/yale/fixtures/get_locks.json
Normal file
16
tests/components/yale/fixtures/get_locks.json
Normal 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"
|
||||
}
|
||||
}
|
1
tests/components/yale/fixtures/jwt
Normal file
1
tests/components/yale/fixtures/jwt
Normal file
@ -0,0 +1 @@
|
||||
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpbnN0YWxsSWQiOiIiLCJyZWdpb24iOiJpcmVsYW5kLXByb2QtYXdzIiwiYXBwbGljYXRpb25JZCI6IiIsInVzZXJJZCI6ImE3NmMyNWU1LTQ5YWEtNGMxNC1jZDBjLTQ4YTY5MzFlMjA4MSIsInZJbnN0YWxsSWQiOmZhbHNlLCJ2UGFzc3dvcmQiOnRydWUsInZFbWFpbCI6dHJ1ZSwidlBob25lIjp0cnVlLCJoYXNJbnN0YWxsSWQiOmZhbHNlLCJoYXNQYXNzd29yZCI6ZmFsc2UsImhhc0VtYWlsIjpmYWxzZSwiaGFzUGhvbmUiOmZhbHNlLCJpc0xvY2tlZE91dCI6ZmFsc2UsImNhcHRjaGEiOiIiLCJlbWFpbCI6W10sInBob25lIjpbXSwiZXhwaXJlc0F0IjoiMjAyNC0xMi0xOFQxMzo1NDowNS4xMzRaIiwidGVtcG9yYXJ5QWNjb3VudENyZWF0aW9uUGFzc3dvcmRMaW5rIjoiIiwiaWF0IjoxNzI0MTYyMDQ1LCJleHAiOjE3MzQ1MzAwNDUsIm9hdXRoIjp7ImFwcF9uYW1lIjoiSG9tZSBBc3Npc3RhbnQiLCJjbGllbnRfaWQiOiJiM2NkM2YwYi1mYjk3LTRkNmMtYmVlOS1hZjdhYjA0NzU4YzciLCJyZWRpcmVjdF91cmkiOiJodHRwczovL2FjY291bnQtbGluay5uYWJ1Y2FzYS5jb20vYXV0aG9yaXplX2NhbGxiYWNrIiwicGFydG5lcl9pZCI6IjY1Nzk3NDg4MTA2NmNhNDhjOTljMDgyNiJ9fQ.BdRo-dEr-osbDQGB2XzlI-mIj4gqULtapODt-sj-eA8
|
26
tests/components/yale/fixtures/lock_open.json
Normal file
26
tests/components/yale/fixtures/lock_open.json
Normal 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"
|
||||
}
|
100
tests/components/yale/fixtures/lock_with_doorbell.online.json
Normal file
100
tests/components/yale/fixtures/lock_with_doorbell.online.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
1
tests/components/yale/fixtures/reauth_jwt
Normal file
1
tests/components/yale/fixtures/reauth_jwt
Normal file
@ -0,0 +1 @@
|
||||
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpbnN0YWxsSWQiOiIiLCJyZWdpb24iOiJpcmVsYW5kLXByb2QtYXdzIiwiYXBwbGljYXRpb25JZCI6IiIsInVzZXJJZCI6ImE3NmMyNWU1LTQ5YWEtNGMxNC1jZDBjLTQ4YTY5MzFlMjA4MSIsInZJbnN0YWxsSWQiOmZhbHNlLCJ2UGFzc3dvcmQiOnRydWUsInZFbWFpbCI6dHJ1ZSwidlBob25lIjp0cnVlLCJoYXNJbnN0YWxsSWQiOmZhbHNlLCJoYXNQYXNzd29yZCI6ZmFsc2UsImhhc0VtYWlsIjpmYWxzZSwiaGFzUGhvbmUiOmZhbHNlLCJpc0xvY2tlZE91dCI6ZmFsc2UsImNhcHRjaGEiOiIiLCJlbWFpbCI6W10sInBob25lIjpbXSwiZXhwaXJlc0F0IjoiMjAyNC0xMi0xOFQxMzo1NDowNS4xMzRaIiwidGVtcG9yYXJ5QWNjb3VudENyZWF0aW9uUGFzc3dvcmRMaW5rIjoiIiwiaWF0IjoxNzI0MTYyMDQ1LCJleHAiOjI3MzQ1MzAwNDUsIm9hdXRoIjp7ImFwcF9uYW1lIjoiSG9tZSBBc3Npc3RhbnQiLCJjbGllbnRfaWQiOiJiM2NkM2YwYi1mYjk3LTRkNmMtYmVlOS1hZjdhYjA0NzU4YzciLCJyZWRpcmVjdF91cmkiOiJodHRwczovL2FjY291bnQtbGluay5uYWJ1Y2FzYS5jb20vYXV0aG9yaXplX2NhbGxiYWNrIiwicGFydG5lcl9pZCI6IjY1Nzk3NDg4MTA2NmNhNDhjOTljMDgyNiJ9fQ.DtkHscsvbTE-SyKW3RxwXFQIKMf0xJwfPZN1X3JesqA
|
1
tests/components/yale/fixtures/reauth_jwt_wrong_account
Normal file
1
tests/components/yale/fixtures/reauth_jwt_wrong_account
Normal file
@ -0,0 +1 @@
|
||||
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpbnN0YWxsSWQiOiIiLCJyZWdpb24iOiJpcmVsYW5kLXByb2QtYXdzIiwiYXBwbGljYXRpb25JZCI6IiIsInVzZXJJZCI6IjQ0NDQ0NDQ0LTQ5YWEtNGMxNC1jZDBjLTQ4YTY5MzFlMjA4MSIsInZJbnN0YWxsSWQiOmZhbHNlLCJ2UGFzc3dvcmQiOnRydWUsInZFbWFpbCI6dHJ1ZSwidlBob25lIjp0cnVlLCJoYXNJbnN0YWxsSWQiOmZhbHNlLCJoYXNQYXNzd29yZCI6ZmFsc2UsImhhc0VtYWlsIjpmYWxzZSwiaGFzUGhvbmUiOmZhbHNlLCJpc0xvY2tlZE91dCI6ZmFsc2UsImNhcHRjaGEiOiIiLCJlbWFpbCI6W10sInBob25lIjpbXSwiZXhwaXJlc0F0IjoiMjAyNC0xMi0xOFQxMzo1NDowNS4xMzRaIiwidGVtcG9yYXJ5QWNjb3VudENyZWF0aW9uUGFzc3dvcmRMaW5rIjoiIiwiaWF0IjoxNzI0MTYyMDQ1LCJleHAiOjE3MzQ1MzAwNDUsIm9hdXRoIjp7ImFwcF9uYW1lIjoiSG9tZSBBc3Npc3RhbnQiLCJjbGllbnRfaWQiOiJiM2NkM2YwYi1mYjk3LTRkNmMtYmVlOS1hZjdhYjA0NzU4YzciLCJyZWRpcmVjdF91cmkiOiJodHRwczovL2FjY291bnQtbGluay5uYWJ1Y2FzYS5jb20vYXV0aG9yaXplX2NhbGxiYWNrIiwicGFydG5lcl9pZCI6IjY1Nzk3NDg4MTA2NmNhNDhjOTljMDgyNiJ9fQ.PenDp4JUIBQZEx2BFxaCqV1-6yMuUPtmnB6jq1wpoX8
|
26
tests/components/yale/fixtures/unlock_closed.json
Normal file
26
tests/components/yale/fixtures/unlock_closed.json
Normal 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"
|
||||
}
|
515
tests/components/yale/mocks.py
Normal file
515
tests/components/yale/mocks.py
Normal 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
|
125
tests/components/yale/snapshots/test_diagnostics.ambr
Normal file
125
tests/components/yale/snapshots/test_diagnostics.ambr
Normal 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,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
# ---
|
390
tests/components/yale/test_binary_sensor.py
Normal file
390
tests/components/yale/test_binary_sensor.py
Normal 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
|
24
tests/components/yale/test_button.py
Normal file
24
tests/components/yale/test_button.py
Normal 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()
|
93
tests/components/yale/test_camera.py
Normal file
93
tests/components/yale/test_camera.py
Normal 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
|
207
tests/components/yale/test_config_flow.py
Normal file
207
tests/components/yale/test_config_flow.py
Normal 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
|
31
tests/components/yale/test_diagnostics.py
Normal file
31
tests/components/yale/test_diagnostics.py
Normal 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
|
174
tests/components/yale/test_event.py
Normal file
174
tests/components/yale/test_event.py
Normal 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
|
238
tests/components/yale/test_init.py
Normal file
238
tests/components/yale/test_init.py
Normal 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"]
|
501
tests/components/yale/test_lock.py
Normal file
501
tests/components/yale/test_lock.py
Normal 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)
|
362
tests/components/yale/test_sensor.py
Normal file
362
tests/components/yale/test_sensor.py
Normal 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"
|
Loading…
x
Reference in New Issue
Block a user