Address yale review comments (#124810)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
J. Nick Koston 2024-08-28 09:00:52 -10:00 committed by GitHub
parent 2900fa733d
commit 70488ffd15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 267 additions and 283 deletions

View file

@ -26,7 +26,7 @@ from .util import async_create_yale_clientsession
type YaleConfigEntry = ConfigEntry[YaleData] type YaleConfigEntry = ConfigEntry[YaleData]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: YaleConfigEntry) -> bool:
"""Set up yale from a config entry.""" """Set up yale from a config entry."""
session = async_create_yale_clientsession(hass) session = async_create_yale_clientsession(hass)
implementation = ( implementation = (

View file

@ -109,12 +109,11 @@ async def async_setup_entry(
for description in SENSOR_TYPES_DOORBELL for description in SENSOR_TYPES_DOORBELL
) )
for doorbell in data.doorbells: entities.extend(
entities.extend( YaleDoorbellBinarySensor(data, doorbell, description)
YaleDoorbellBinarySensor(data, doorbell, description) for description in SENSOR_TYPES_DOORBELL + SENSOR_TYPES_VIDEO_DOORBELL
for description in SENSOR_TYPES_DOORBELL + SENSOR_TYPES_VIDEO_DOORBELL for doorbell in data.doorbells
) )
async_add_entities(entities) async_add_entities(entities)

View file

@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import YaleConfigEntry from . import YaleConfigEntry
from .entity import YaleEntityMixin from .entity import YaleEntity
async def async_setup_entry( async def async_setup_entry(
@ -18,7 +18,7 @@ async def async_setup_entry(
async_add_entities(YaleWakeLockButton(data, lock, "wake") for lock in data.locks) async_add_entities(YaleWakeLockButton(data, lock, "wake") for lock in data.locks)
class YaleWakeLockButton(YaleEntityMixin, ButtonEntity): class YaleWakeLockButton(YaleEntity, ButtonEntity):
"""Representation of an Yale lock wake button.""" """Representation of an Yale lock wake button."""
_attr_translation_key = "wake" _attr_translation_key = "wake"

View file

@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import YaleConfigEntry, YaleData from . import YaleConfigEntry, YaleData
from .const import DEFAULT_NAME, DEFAULT_TIMEOUT from .const import DEFAULT_NAME, DEFAULT_TIMEOUT
from .entity import YaleEntityMixin from .entity import YaleEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -38,7 +38,7 @@ async def async_setup_entry(
) )
class YaleCamera(YaleEntityMixin, Camera): class YaleCamera(YaleEntity, Camera):
"""An implementation of an Yale security camera.""" """An implementation of an Yale security camera."""
_attr_translation_key = "camera" _attr_translation_key = "camera"

View file

@ -26,7 +26,9 @@ class YaleConfigFlow(config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=
"""Return logger.""" """Return logger."""
return _LOGGER return _LOGGER
async def async_step_reauth(self, data: Mapping[str, Any]) -> ConfigFlowResult: async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth.""" """Handle configuration by re-auth."""
self.reauth_entry = self.hass.config_entries.async_get_entry( self.reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"] self.context["entry_id"]
@ -54,4 +56,5 @@ class YaleConfigFlow(config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=
return self.async_abort(reason="reauth_invalid_user") return self.async_abort(reason="reauth_invalid_user")
return self.async_update_reload_and_abort(entry, data=data) return self.async_update_reload_and_abort(entry, data=data)
await self.async_set_unique_id(user_id) await self.async_set_unique_id(user_id)
self._abort_if_unique_id_configured()
return await super().async_oauth_create_entry(data) return await super().async_oauth_create_entry(data)

View file

@ -1,7 +1,5 @@
"""Constants for Yale devices.""" """Constants for Yale devices."""
from yalexs.const import Brand
from homeassistant.const import Platform from homeassistant.const import Platform
DEFAULT_TIMEOUT = 25 DEFAULT_TIMEOUT = 25
@ -13,8 +11,6 @@ CONF_INSTALL_ID = "install_id"
VERIFICATION_CODE_KEY = "verification_code" VERIFICATION_CODE_KEY = "verification_code"
DEFAULT_BRAND = Brand.YALE_HOME
MANUFACTURER = "Yale Home Inc." MANUFACTURER = "Yale Home Inc."
DEFAULT_NAME = "Yale" DEFAULT_NAME = "Yale"

View file

@ -4,11 +4,12 @@ from __future__ import annotations
from typing import Any from typing import Any
from yalexs.const import Brand
from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import YaleConfigEntry from . import YaleConfigEntry
from .const import CONF_BRAND, DEFAULT_BRAND
TO_REDACT = { TO_REDACT = {
"HouseID", "HouseID",
@ -45,5 +46,5 @@ async def async_get_config_entry_diagnostics(
) )
for doorbell in data.doorbells for doorbell in data.doorbells
}, },
"brand": entry.data.get(CONF_BRAND, DEFAULT_BRAND), "brand": Brand.YALE_GLOBAL.value,
} }

View file

@ -20,7 +20,7 @@ from .const import MANUFACTURER
DEVICE_TYPES = ["keypad", "lock", "camera", "doorbell", "door", "bell"] DEVICE_TYPES = ["keypad", "lock", "camera", "doorbell", "door", "bell"]
class YaleEntityMixin(Entity): class YaleEntity(Entity):
"""Base implementation for Yale device.""" """Base implementation for Yale device."""
_attr_should_poll = False _attr_should_poll = False
@ -87,7 +87,7 @@ class YaleEntityMixin(Entity):
self._update_from_data() self._update_from_data()
class YaleDescriptionEntity(YaleEntityMixin): class YaleDescriptionEntity(YaleEntity):
"""An Yale entity with a description.""" """An Yale entity with a description."""
def __init__( def __init__(

View file

@ -63,22 +63,17 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up the yale event platform.""" """Set up the yale event platform."""
data = config_entry.runtime_data data = config_entry.runtime_data
entities: list[YaleEventEntity] = [] entities: list[YaleEventEntity] = [
YaleEventEntity(data, lock, description)
for lock in data.locks: for description in TYPES_DOORBELL
detail = data.get_device_detail(lock.device_id) for lock in data.locks
if detail.doorbell: if (detail := data.get_device_detail(lock.device_id)) and detail.doorbell
entities.extend( ]
YaleEventEntity(data, lock, description) entities.extend(
for description in TYPES_DOORBELL YaleEventEntity(data, doorbell, description)
) for description in TYPES_DOORBELL + TYPES_VIDEO_DOORBELL
for doorbell in data.doorbells
for doorbell in data.doorbells: )
entities.extend(
YaleEventEntity(data, doorbell, description)
for description in TYPES_DOORBELL + TYPES_VIDEO_DOORBELL
)
async_add_entities(entities) async_add_entities(entities)
@ -86,7 +81,6 @@ class YaleEventEntity(YaleDescriptionEntity, EventEntity):
"""An yale event entity.""" """An yale event entity."""
entity_description: YaleEventEntityDescription entity_description: YaleEventEntityDescription
_attr_has_entity_name = True
_last_activity: Activity | None = None _last_activity: Activity | None = None
@callback @callback

View file

@ -19,7 +19,7 @@ from homeassistant.helpers.restore_state import RestoreEntity
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from . import YaleConfigEntry, YaleData from . import YaleConfigEntry, YaleData
from .entity import YaleEntityMixin from .entity import YaleEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -36,7 +36,7 @@ async def async_setup_entry(
async_add_entities(YaleLock(data, lock) for lock in data.locks) async_add_entities(YaleLock(data, lock) for lock in data.locks)
class YaleLock(YaleEntityMixin, RestoreEntity, LockEntity): class YaleLock(YaleEntity, RestoreEntity, LockEntity):
"""Representation of an Yale lock.""" """Representation of an Yale lock."""
_attr_name = None _attr_name = None

View file

@ -11,6 +11,6 @@
], ],
"documentation": "https://www.home-assistant.io/integrations/yale", "documentation": "https://www.home-assistant.io/integrations/yale",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"], "loggers": ["socketio", "engineio", "yalexs"],
"requirements": ["yalexs==8.5.4", "yalexs-ble==2.4.3"] "requirements": ["yalexs==8.5.4", "yalexs-ble==2.4.3"]
} }

View file

@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Generic, TypeVar, cast from typing import Any, cast
from yalexs.activity import ActivityType, LockOperationActivity from yalexs.activity import ActivityType, LockOperationActivity
from yalexs.doorbell import Doorbell from yalexs.doorbell import Doorbell
@ -42,7 +42,7 @@ from .const import (
OPERATION_METHOD_REMOTE, OPERATION_METHOD_REMOTE,
OPERATION_METHOD_TAG, OPERATION_METHOD_TAG,
) )
from .entity import YaleDescriptionEntity, YaleEntityMixin from .entity import YaleDescriptionEntity, YaleEntity
def _retrieve_device_battery_state(detail: LockDetail) -> int: def _retrieve_device_battery_state(detail: LockDetail) -> int:
@ -55,14 +55,13 @@ def _retrieve_linked_keypad_battery_state(detail: KeypadDetail) -> int | None:
return detail.battery_percentage return detail.battery_percentage
_T = TypeVar("_T", LockDetail, KeypadDetail)
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class YaleSensorEntityDescription(SensorEntityDescription, Generic[_T]): class YaleSensorEntityDescription[T: LockDetail | KeypadDetail](
SensorEntityDescription
):
"""Mixin for required keys.""" """Mixin for required keys."""
value_fn: Callable[[_T], int | None] value_fn: Callable[[T], int | None]
SENSOR_TYPE_DEVICE_BATTERY = YaleSensorEntityDescription[LockDetail]( SENSOR_TYPE_DEVICE_BATTERY = YaleSensorEntityDescription[LockDetail](
@ -112,7 +111,7 @@ async def async_setup_entry(
async_add_entities(entities) async_add_entities(entities)
class YaleOperatorSensor(YaleEntityMixin, RestoreSensor): class YaleOperatorSensor(YaleEntity, RestoreSensor):
"""Representation of an Yale lock operation sensor.""" """Representation of an Yale lock operation sensor."""
_attr_translation_key = "operator" _attr_translation_key = "operator"
@ -196,10 +195,12 @@ class YaleOperatorSensor(YaleEntityMixin, RestoreSensor):
self._operated_autorelock = last_attrs[ATTR_OPERATION_AUTORELOCK] self._operated_autorelock = last_attrs[ATTR_OPERATION_AUTORELOCK]
class YaleBatterySensor(YaleDescriptionEntity, SensorEntity, Generic[_T]): class YaleBatterySensor[T: LockDetail | KeypadDetail](
YaleDescriptionEntity, SensorEntity
):
"""Representation of an Yale sensor.""" """Representation of an Yale sensor."""
entity_description: YaleSensorEntityDescription[_T] entity_description: YaleSensorEntityDescription[T]
_attr_device_class = SensorDeviceClass.BATTERY _attr_device_class = SensorDeviceClass.BATTERY
_attr_native_unit_of_measurement = PERCENTAGE _attr_native_unit_of_measurement = PERCENTAGE

View file

@ -63,16 +63,11 @@ def _activity_time_based(latest: Activity) -> Activity | None:
"""Get the latest state of the sensor.""" """Get the latest state of the sensor."""
start = latest.activity_start_time start = latest.activity_start_time
end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
if start <= _native_datetime() <= end: if start <= datetime.now() <= end:
return latest return latest
return None 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: def retrieve_online_state(data: YaleData, detail: DoorbellDetail | LockDetail) -> bool:
"""Get the latest state of the sensor.""" """Get the latest state of the sensor."""
# The doorbell will go into standby mode when there is no motion # The doorbell will go into standby mode when there is no motion

View file

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

View file

@ -0,0 +1,33 @@
# serializer version: 1
# name: test_doorbell_device_registry
DeviceRegistryEntrySnapshot({
'area_id': 'tmt100_name',
'config_entries': <ANY>,
'configuration_url': 'https://account.aaecosystem.com',
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'yale',
'tmt100',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Yale Home Inc.',
'model': 'hydra1',
'model_id': None,
'name': 'tmt100 Name',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': 'tmt100 Name',
'sw_version': '3.1.0-HYDRC75+201909251139',
'via_device_id': None,
})
# ---

View file

@ -1,7 +1,7 @@
# serializer version: 1 # serializer version: 1
# name: test_diagnostics # name: test_diagnostics
dict({ dict({
'brand': 'yale_home', 'brand': 'yale_global',
'doorbells': dict({ 'doorbells': dict({
'K98GiDT45GUL': dict({ 'K98GiDT45GUL': dict({
'HouseID': '**REDACTED**', 'HouseID': '**REDACTED**',

View file

@ -1,7 +1,9 @@
"""The binary_sensor tests for the yale platform.""" """The binary_sensor tests for the yale platform."""
import datetime import datetime
from unittest.mock import patch
from freezegun.api import FrozenDateTimeFactory
from syrupy import SnapshotAssertion
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.const import ( from homeassistant.const import (
@ -33,28 +35,19 @@ async def test_doorsense(hass: HomeAssistant) -> None:
hass, "get_lock.online_with_doorsense.json" hass, "get_lock.online_with_doorsense.json"
) )
await _create_yale_with_devices(hass, [lock_one]) await _create_yale_with_devices(hass, [lock_one])
states = hass.states
binary_sensor_online_with_doorsense_name = hass.states.get( assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON
"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"} data = {ATTR_ENTITY_ID: "lock.online_with_doorsense_name"}
await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True) 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( assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON
"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.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( assert (
"binary_sensor.online_with_doorsense_name_door" states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_OFF
) )
assert binary_sensor_online_with_doorsense_name.state == STATE_OFF
async def test_lock_bridge_offline(hass: HomeAssistant) -> None: async def test_lock_bridge_offline(hass: HomeAssistant) -> None:
@ -66,112 +59,78 @@ async def test_lock_bridge_offline(hass: HomeAssistant) -> None:
hass, "get_activity.bridge_offline.json" hass, "get_activity.bridge_offline.json"
) )
await _create_yale_with_devices(hass, [lock_one], activities=activities) await _create_yale_with_devices(hass, [lock_one], activities=activities)
states = hass.states
binary_sensor_online_with_doorsense_name = hass.states.get( assert (
"binary_sensor.online_with_doorsense_name_door" states.get("binary_sensor.online_with_doorsense_name_door").state
== STATE_UNAVAILABLE
) )
assert binary_sensor_online_with_doorsense_name.state == STATE_UNAVAILABLE
async def test_create_doorbell(hass: HomeAssistant) -> None: async def test_create_doorbell(hass: HomeAssistant) -> None:
"""Test creation of a doorbell.""" """Test creation of a doorbell."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json")
await _create_yale_with_devices(hass, [doorbell_one]) await _create_yale_with_devices(hass, [doorbell_one])
states = hass.states
binary_sensor_k98gidt45gul_name_motion = hass.states.get( assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_OFF
"binary_sensor.k98gidt45gul_name_motion" assert (
states.get("binary_sensor.k98gidt45gul_name_image_capture").state == STATE_OFF
) )
assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF assert states.get("binary_sensor.k98gidt45gul_name_connectivity").state == STATE_ON
binary_sensor_k98gidt45gul_name_image_capture = hass.states.get( assert (
"binary_sensor.k98gidt45gul_name_image_capture" states.get("binary_sensor.k98gidt45gul_name_doorbell_ding").state == STATE_OFF
) )
assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_OFF
binary_sensor_k98gidt45gul_name_online = hass.states.get( assert (
"binary_sensor.k98gidt45gul_name_connectivity" states.get("binary_sensor.k98gidt45gul_name_image_capture").state == STATE_OFF
) )
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: async def test_create_doorbell_offline(hass: HomeAssistant) -> None:
"""Test creation of a doorbell that is offline.""" """Test creation of a doorbell that is offline."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.offline.json") doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.offline.json")
await _create_yale_with_devices(hass, [doorbell_one]) await _create_yale_with_devices(hass, [doorbell_one])
states = hass.states
binary_sensor_tmt100_name_motion = hass.states.get( assert states.get("binary_sensor.tmt100_name_motion").state == STATE_UNAVAILABLE
"binary_sensor.tmt100_name_motion" assert states.get("binary_sensor.tmt100_name_connectivity").state == STATE_OFF
assert (
states.get("binary_sensor.tmt100_name_doorbell_ding").state == STATE_UNAVAILABLE
) )
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: async def test_create_doorbell_with_motion(
hass: HomeAssistant, freezer: FrozenDateTimeFactory
) -> None:
"""Test creation of a doorbell.""" """Test creation of a doorbell."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json")
activities = await _mock_activities_from_fixture( activities = await _mock_activities_from_fixture(
hass, "get_activity.doorbell_motion.json" hass, "get_activity.doorbell_motion.json"
) )
await _create_yale_with_devices(hass, [doorbell_one], activities=activities) await _create_yale_with_devices(hass, [doorbell_one], activities=activities)
states = hass.states
binary_sensor_k98gidt45gul_name_motion = hass.states.get( assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_ON
"binary_sensor.k98gidt45gul_name_motion" assert states.get("binary_sensor.k98gidt45gul_name_connectivity").state == STATE_ON
assert (
states.get("binary_sensor.k98gidt45gul_name_doorbell_ding").state == STATE_OFF
) )
assert binary_sensor_k98gidt45gul_name_motion.state == STATE_ON freezer.tick(40)
binary_sensor_k98gidt45gul_name_online = hass.states.get( async_fire_time_changed(hass)
"binary_sensor.k98gidt45gul_name_connectivity" await hass.async_block_till_done()
) assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_OFF
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: async def test_doorbell_update_via_socketio(
hass: HomeAssistant, freezer: FrozenDateTimeFactory
) -> None:
"""Test creation of a doorbell that can be updated via socketio.""" """Test creation of a doorbell that can be updated via socketio."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json")
_, socketio = await _create_yale_with_devices(hass, [doorbell_one]) _, socketio = await _create_yale_with_devices(hass, [doorbell_one])
assert doorbell_one.pubsub_channel == "7c7a6672-59c8-3333-ffff-dcd98705cccc" assert doorbell_one.pubsub_channel == "7c7a6672-59c8-3333-ffff-dcd98705cccc"
states = hass.states
binary_sensor_k98gidt45gul_name_motion = hass.states.get( assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_OFF
"binary_sensor.k98gidt45gul_name_motion" assert (
states.get("binary_sensor.k98gidt45gul_name_doorbell_ding").state == STATE_OFF
) )
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 = list(socketio._listeners)[0]
listener( listener(
@ -192,10 +151,7 @@ async def test_doorbell_update_via_socketio(hass: HomeAssistant) -> None:
await hass.async_block_till_done() await hass.async_block_till_done()
binary_sensor_k98gidt45gul_name_image_capture = hass.states.get( assert states.get("binary_sensor.k98gidt45gul_name_image_capture").state == STATE_ON
"binary_sensor.k98gidt45gul_name_image_capture"
)
assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_ON
listener( listener(
doorbell_one.device_id, doorbell_one.device_id,
@ -226,29 +182,18 @@ async def test_doorbell_update_via_socketio(hass: HomeAssistant) -> None:
await hass.async_block_till_done() await hass.async_block_till_done()
binary_sensor_k98gidt45gul_name_motion = hass.states.get( assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_ON
"binary_sensor.k98gidt45gul_name_motion" assert (
states.get("binary_sensor.k98gidt45gul_name_doorbell_ding").state == STATE_OFF
) )
assert binary_sensor_k98gidt45gul_name_motion.state == STATE_ON
binary_sensor_k98gidt45gul_name_ding = hass.states.get( freezer.tick(40)
"binary_sensor.k98gidt45gul_name_doorbell_ding" async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (
states.get("binary_sensor.k98gidt45gul_name_image_capture").state == STATE_OFF
) )
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( listener(
doorbell_one.device_id, doorbell_one.device_id,
@ -260,37 +205,28 @@ async def test_doorbell_update_via_socketio(hass: HomeAssistant) -> None:
await hass.async_block_till_done() await hass.async_block_till_done()
binary_sensor_k98gidt45gul_name_ding = hass.states.get( assert states.get("binary_sensor.k98gidt45gul_name_doorbell_ding").state == STATE_ON
"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( freezer.tick(40)
"binary_sensor.k98gidt45gul_name_doorbell_ding" async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (
states.get("binary_sensor.k98gidt45gul_name_doorbell_ding").state == STATE_OFF
) )
assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF
async def test_doorbell_device_registry( async def test_doorbell_device_registry(
hass: HomeAssistant, device_registry: dr.DeviceRegistry hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
snapshot: SnapshotAssertion,
) -> None: ) -> None:
"""Test creation of a lock with doorsense and bridge ands up in the registry.""" """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") doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.offline.json")
await _create_yale_with_devices(hass, [doorbell_one]) await _create_yale_with_devices(hass, [doorbell_one])
reg_device = device_registry.async_get_device(identifiers={("yale", "tmt100")}) reg_device = device_registry.async_get_device(identifiers={("yale", "tmt100")})
assert reg_device.model == "hydra1" assert reg_device == snapshot
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: async def test_door_sense_update_via_socketio(hass: HomeAssistant) -> None:
@ -302,11 +238,8 @@ async def test_door_sense_update_via_socketio(hass: HomeAssistant) -> None:
config_entry, socketio = await _create_yale_with_devices( config_entry, socketio = await _create_yale_with_devices(
hass, [lock_one], activities=activities hass, [lock_one], activities=activities
) )
states = hass.states
binary_sensor_online_with_doorsense_name = hass.states.get( assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON
"binary_sensor.online_with_doorsense_name_door"
)
assert binary_sensor_online_with_doorsense_name.state == STATE_ON
listener = list(socketio._listeners)[0] listener = list(socketio._listeners)[0]
listener( listener(
@ -316,10 +249,10 @@ async def test_door_sense_update_via_socketio(hass: HomeAssistant) -> None:
) )
await hass.async_block_till_done() await hass.async_block_till_done()
binary_sensor_online_with_doorsense_name = hass.states.get(
"binary_sensor.online_with_doorsense_name_door" assert (
states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_OFF
) )
assert binary_sensor_online_with_doorsense_name.state == STATE_OFF
listener( listener(
lock_one.device_id, lock_one.device_id,
@ -328,33 +261,22 @@ async def test_door_sense_update_via_socketio(hass: HomeAssistant) -> None:
) )
await hass.async_block_till_done() await hass.async_block_till_done()
binary_sensor_online_with_doorsense_name = hass.states.get(
"binary_sensor.online_with_doorsense_name_door" assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON
)
assert binary_sensor_online_with_doorsense_name.state == STATE_ON
async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30)) async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30))
await hass.async_block_till_done() await hass.async_block_till_done()
binary_sensor_online_with_doorsense_name = hass.states.get( assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON
"binary_sensor.online_with_doorsense_name_door"
)
assert binary_sensor_online_with_doorsense_name.state == STATE_ON
socketio.connected = True socketio.connected = True
async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30)) async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30))
await hass.async_block_till_done() await hass.async_block_till_done()
binary_sensor_online_with_doorsense_name = hass.states.get( assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON
"binary_sensor.online_with_doorsense_name_door"
)
assert binary_sensor_online_with_doorsense_name.state == STATE_ON
# Ensure socketio status is always preserved # Ensure socketio status is always preserved
async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=2)) async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=2))
await hass.async_block_till_done() await hass.async_block_till_done()
binary_sensor_online_with_doorsense_name = hass.states.get( assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON
"binary_sensor.online_with_doorsense_name_door"
)
assert binary_sensor_online_with_doorsense_name.state == STATE_ON
listener( listener(
lock_one.device_id, lock_one.device_id,
@ -363,17 +285,11 @@ async def test_door_sense_update_via_socketio(hass: HomeAssistant) -> None:
) )
await hass.async_block_till_done() await hass.async_block_till_done()
binary_sensor_online_with_doorsense_name = hass.states.get( assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON
"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)) async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=4))
await hass.async_block_till_done() await hass.async_block_till_done()
binary_sensor_online_with_doorsense_name = hass.states.get( assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON
"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.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -383,8 +299,10 @@ async def test_create_lock_with_doorbell(hass: HomeAssistant) -> None:
"""Test creation of a lock with a doorbell.""" """Test creation of a lock with a doorbell."""
lock_one = await _mock_lock_from_fixture(hass, "lock_with_doorbell.online.json") lock_one = await _mock_lock_from_fixture(hass, "lock_with_doorbell.online.json")
await _create_yale_with_devices(hass, [lock_one]) await _create_yale_with_devices(hass, [lock_one])
states = hass.states
ding_sensor = hass.states.get( assert (
"binary_sensor.a6697750d607098bae8d6baa11ef8063_name_doorbell_ding" states.get(
"binary_sensor.a6697750d607098bae8d6baa11ef8063_name_doorbell_ding"
).state
== STATE_OFF
) )
assert ding_sensor.state == STATE_OFF

View file

@ -1,16 +1,16 @@
"""Test the yale config flow.""" """Test the yale config flow."""
from collections.abc import Generator from collections.abc import Generator
from unittest.mock import Mock, patch from unittest.mock import ANY, Mock, patch
import pytest import pytest
from homeassistant import config_entries
from homeassistant.components.yale.application_credentials import ( from homeassistant.components.yale.application_credentials import (
OAUTH2_AUTHORIZE, OAUTH2_AUTHORIZE,
OAUTH2_TOKEN, OAUTH2_TOKEN,
) )
from homeassistant.components.yale.const import DOMAIN from homeassistant.components.yale.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers import config_entry_oauth2_flow
@ -44,7 +44,7 @@ async def test_full_flow(
) -> None: ) -> None:
"""Check full flow.""" """Check full flow."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
) )
state = config_entry_oauth2_flow._encode_jwt( state = config_entry_oauth2_flow._encode_jwt(
hass, hass,
@ -78,13 +78,81 @@ async def test_full_flow(
}, },
) )
await hass.config_entries.flow.async_configure(result["flow_id"]) result2 = await hass.config_entries.flow.async_configure(result["flow_id"])
assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
entry = hass.config_entries.async_entries(DOMAIN)[0] entry = hass.config_entries.async_entries(DOMAIN)[0]
assert entry.unique_id == USER_ID assert entry.unique_id == USER_ID
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["result"].unique_id == USER_ID
assert entry.data == {
"auth_implementation": "yale",
"token": {
"access_token": jwt,
"expires_at": ANY,
"expires_in": ANY,
"refresh_token": "mock-refresh-token",
"scope": "any",
"user_id": "mock-user-id",
},
}
@pytest.mark.usefixtures("client_credentials")
@pytest.mark.usefixtures("current_request_with_host")
async def test_full_flow_already_exists(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
jwt: str,
mock_setup_entry: Mock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Check full flow for a user that already exists."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": 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,
},
)
result2 = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result2["type"] is FlowResultType.ABORT
assert result2["reason"] == "already_configured"
@pytest.mark.usefixtures("client_credentials") @pytest.mark.usefixtures("client_credentials")
@pytest.mark.usefixtures("current_request_with_host") @pytest.mark.usefixtures("current_request_with_host")

View file

@ -1,7 +1,6 @@
"""The event tests for the yale.""" """The event tests for the yale."""
import datetime from freezegun.api import FrozenDateTimeFactory
from unittest.mock import patch
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -42,7 +41,9 @@ async def test_create_doorbell_offline(hass: HomeAssistant) -> None:
assert doorbell_state.state == STATE_UNAVAILABLE assert doorbell_state.state == STATE_UNAVAILABLE
async def test_create_doorbell_with_motion(hass: HomeAssistant) -> None: async def test_create_doorbell_with_motion(
hass: HomeAssistant, freezer: FrozenDateTimeFactory
) -> None:
"""Test creation of a doorbell.""" """Test creation of a doorbell."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json")
activities = await _mock_activities_from_fixture( activities = await _mock_activities_from_fixture(
@ -58,19 +59,16 @@ async def test_create_doorbell_with_motion(hass: HomeAssistant) -> None:
assert doorbell_state is not None assert doorbell_state is not None
assert doorbell_state.state == STATE_UNKNOWN assert doorbell_state.state == STATE_UNKNOWN
new_time = dt_util.utcnow() + datetime.timedelta(seconds=40) freezer.tick(40)
native_time = datetime.datetime.now() + datetime.timedelta(seconds=40) async_fire_time_changed(hass)
with patch( await hass.async_block_till_done()
"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") motion_state = hass.states.get("event.k98gidt45gul_name_motion")
assert motion_state.state == isotime assert motion_state.state == isotime
async def test_doorbell_update_via_socketio(hass: HomeAssistant) -> None: async def test_doorbell_update_via_socketio(
hass: HomeAssistant, freezer: FrozenDateTimeFactory
) -> None:
"""Test creation of a doorbell that can be updated via socketio.""" """Test creation of a doorbell that can be updated via socketio."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json")
@ -119,14 +117,9 @@ async def test_doorbell_update_via_socketio(hass: HomeAssistant) -> None:
assert motion_state.state != STATE_UNKNOWN assert motion_state.state != STATE_UNKNOWN
isotime = motion_state.state isotime = motion_state.state
new_time = dt_util.utcnow() + datetime.timedelta(seconds=40) freezer.tick(40)
native_time = datetime.datetime.now() + datetime.timedelta(seconds=40) async_fire_time_changed(hass)
with patch( await hass.async_block_till_done()
"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") motion_state = hass.states.get("event.k98gidt45gul_name_motion")
assert motion_state is not None assert motion_state is not None
@ -147,14 +140,9 @@ async def test_doorbell_update_via_socketio(hass: HomeAssistant) -> None:
assert doorbell_state.state != STATE_UNKNOWN assert doorbell_state.state != STATE_UNKNOWN
isotime = motion_state.state isotime = motion_state.state
new_time = dt_util.utcnow() + datetime.timedelta(seconds=40) freezer.tick(40)
native_time = datetime.datetime.now() + datetime.timedelta(seconds=40) async_fire_time_changed(hass)
with patch( await hass.async_block_till_done()
"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") doorbell_state = hass.states.get("event.k98gidt45gul_name_doorbell")
assert doorbell_state is not None assert doorbell_state is not None

View file

@ -89,16 +89,15 @@ async def test_unlock_throws_yale_api_http_error(hass: HomeAssistant) -> None:
"unlock_return_activities": _unlock_return_activities_side_effect "unlock_return_activities": _unlock_return_activities_side_effect
}, },
) )
last_err = None
data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"} data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"}
try: with pytest.raises(
HomeAssistantError,
match=(
"A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user"
" consumable"
),
):
await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True) 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: async def test_lock_throws_yale_api_http_error(hass: HomeAssistant) -> None:
@ -119,16 +118,15 @@ async def test_lock_throws_yale_api_http_error(hass: HomeAssistant) -> None:
"lock_return_activities": _lock_return_activities_side_effect "lock_return_activities": _lock_return_activities_side_effect
}, },
) )
last_err = None
data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"} data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"}
try: with pytest.raises(
HomeAssistantError,
match=(
"A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user"
" consumable"
),
):
await hass.services.async_call(LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True) 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( async def test_open_throws_hass_service_not_supported_error(
@ -185,6 +183,7 @@ async def test_load_unload(hass: HomeAssistant) -> None:
await hass.config_entries.async_unload(config_entry.entry_id) await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.NOT_LOADED
async def test_load_triggers_ble_discovery( async def test_load_triggers_ble_discovery(