From 70488ffd15a7fa10b2348c4843adc6e914e2233c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Aug 2024 09:00:52 -1000 Subject: [PATCH] Address yale review comments (#124810) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/yale/__init__.py | 2 +- .../components/yale/binary_sensor.py | 11 +- homeassistant/components/yale/button.py | 4 +- homeassistant/components/yale/camera.py | 4 +- homeassistant/components/yale/config_flow.py | 5 +- homeassistant/components/yale/const.py | 4 - homeassistant/components/yale/diagnostics.py | 5 +- homeassistant/components/yale/entity.py | 4 +- homeassistant/components/yale/event.py | 28 +- homeassistant/components/yale/lock.py | 4 +- homeassistant/components/yale/manifest.json | 2 +- homeassistant/components/yale/sensor.py | 21 +- homeassistant/components/yale/util.py | 7 +- tests/components/yale/__init__.py | 11 - .../yale/snapshots/test_binary_sensor.ambr | 33 +++ .../yale/snapshots/test_diagnostics.ambr | 2 +- tests/components/yale/test_binary_sensor.py | 252 ++++++------------ tests/components/yale/test_config_flow.py | 76 +++++- tests/components/yale/test_event.py | 44 ++- tests/components/yale/test_init.py | 31 ++- 20 files changed, 267 insertions(+), 283 deletions(-) create mode 100644 tests/components/yale/snapshots/test_binary_sensor.ambr diff --git a/homeassistant/components/yale/__init__.py b/homeassistant/components/yale/__init__.py index f7a4a6e0f4d..1cbd9c87b57 100644 --- a/homeassistant/components/yale/__init__.py +++ b/homeassistant/components/yale/__init__.py @@ -26,7 +26,7 @@ from .util import async_create_yale_clientsession 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.""" session = async_create_yale_clientsession(hass) implementation = ( diff --git a/homeassistant/components/yale/binary_sensor.py b/homeassistant/components/yale/binary_sensor.py index cbc0b48b177..dbb00ad7d42 100644 --- a/homeassistant/components/yale/binary_sensor.py +++ b/homeassistant/components/yale/binary_sensor.py @@ -109,12 +109,11 @@ async def async_setup_entry( 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 - ) - + entities.extend( + YaleDoorbellBinarySensor(data, doorbell, description) + for description in SENSOR_TYPES_DOORBELL + SENSOR_TYPES_VIDEO_DOORBELL + for doorbell in data.doorbells + ) async_add_entities(entities) diff --git a/homeassistant/components/yale/button.py b/homeassistant/components/yale/button.py index de0cff4f0c8..b04ad638f0c 100644 --- a/homeassistant/components/yale/button.py +++ b/homeassistant/components/yale/button.py @@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import YaleConfigEntry -from .entity import YaleEntityMixin +from .entity import YaleEntity 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) -class YaleWakeLockButton(YaleEntityMixin, ButtonEntity): +class YaleWakeLockButton(YaleEntity, ButtonEntity): """Representation of an Yale lock wake button.""" _attr_translation_key = "wake" diff --git a/homeassistant/components/yale/camera.py b/homeassistant/components/yale/camera.py index 500239d7f3a..217e8f5f6fd 100644 --- a/homeassistant/components/yale/camera.py +++ b/homeassistant/components/yale/camera.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import YaleConfigEntry, YaleData from .const import DEFAULT_NAME, DEFAULT_TIMEOUT -from .entity import YaleEntityMixin +from .entity import YaleEntity _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.""" _attr_translation_key = "camera" diff --git a/homeassistant/components/yale/config_flow.py b/homeassistant/components/yale/config_flow.py index cdd44754103..6cbc9543ea4 100644 --- a/homeassistant/components/yale/config_flow.py +++ b/homeassistant/components/yale/config_flow.py @@ -26,7 +26,9 @@ class YaleConfigFlow(config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain= """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.""" self.reauth_entry = self.hass.config_entries.async_get_entry( 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_update_reload_and_abort(entry, data=data) await self.async_set_unique_id(user_id) + self._abort_if_unique_id_configured() return await super().async_oauth_create_entry(data) diff --git a/homeassistant/components/yale/const.py b/homeassistant/components/yale/const.py index 630d15f7230..3da4fb1dfb4 100644 --- a/homeassistant/components/yale/const.py +++ b/homeassistant/components/yale/const.py @@ -1,7 +1,5 @@ """Constants for Yale devices.""" -from yalexs.const import Brand - from homeassistant.const import Platform DEFAULT_TIMEOUT = 25 @@ -13,8 +11,6 @@ CONF_INSTALL_ID = "install_id" VERIFICATION_CODE_KEY = "verification_code" -DEFAULT_BRAND = Brand.YALE_HOME - MANUFACTURER = "Yale Home Inc." DEFAULT_NAME = "Yale" diff --git a/homeassistant/components/yale/diagnostics.py b/homeassistant/components/yale/diagnostics.py index ef8d837b82e..7e7f6179e7a 100644 --- a/homeassistant/components/yale/diagnostics.py +++ b/homeassistant/components/yale/diagnostics.py @@ -4,11 +4,12 @@ from __future__ import annotations from typing import Any +from yalexs.const import Brand + 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", @@ -45,5 +46,5 @@ async def async_get_config_entry_diagnostics( ) for doorbell in data.doorbells }, - "brand": entry.data.get(CONF_BRAND, DEFAULT_BRAND), + "brand": Brand.YALE_GLOBAL.value, } diff --git a/homeassistant/components/yale/entity.py b/homeassistant/components/yale/entity.py index 7105fda861c..152070c0be3 100644 --- a/homeassistant/components/yale/entity.py +++ b/homeassistant/components/yale/entity.py @@ -20,7 +20,7 @@ from .const import MANUFACTURER DEVICE_TYPES = ["keypad", "lock", "camera", "doorbell", "door", "bell"] -class YaleEntityMixin(Entity): +class YaleEntity(Entity): """Base implementation for Yale device.""" _attr_should_poll = False @@ -87,7 +87,7 @@ class YaleEntityMixin(Entity): self._update_from_data() -class YaleDescriptionEntity(YaleEntityMixin): +class YaleDescriptionEntity(YaleEntity): """An Yale entity with a description.""" def __init__( diff --git a/homeassistant/components/yale/event.py b/homeassistant/components/yale/event.py index 7014c5dafbf..935ba7376f8 100644 --- a/homeassistant/components/yale/event.py +++ b/homeassistant/components/yale/event.py @@ -63,22 +63,17 @@ async def async_setup_entry( ) -> 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 - ) - + entities: list[YaleEventEntity] = [ + YaleEventEntity(data, lock, description) + for description in TYPES_DOORBELL + for lock in data.locks + if (detail := data.get_device_detail(lock.device_id)) and detail.doorbell + ] + entities.extend( + YaleEventEntity(data, doorbell, description) + for description in TYPES_DOORBELL + TYPES_VIDEO_DOORBELL + for doorbell in data.doorbells + ) async_add_entities(entities) @@ -86,7 +81,6 @@ class YaleEventEntity(YaleDescriptionEntity, EventEntity): """An yale event entity.""" entity_description: YaleEventEntityDescription - _attr_has_entity_name = True _last_activity: Activity | None = None @callback diff --git a/homeassistant/components/yale/lock.py b/homeassistant/components/yale/lock.py index 36d865bf527..b911c92ba0f 100644 --- a/homeassistant/components/yale/lock.py +++ b/homeassistant/components/yale/lock.py @@ -19,7 +19,7 @@ from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.dt as dt_util from . import YaleConfigEntry, YaleData -from .entity import YaleEntityMixin +from .entity import YaleEntity _LOGGER = logging.getLogger(__name__) @@ -36,7 +36,7 @@ async def async_setup_entry( 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.""" _attr_name = None diff --git a/homeassistant/components/yale/manifest.json b/homeassistant/components/yale/manifest.json index 2dc84758610..d6da9ba3993 100644 --- a/homeassistant/components/yale/manifest.json +++ b/homeassistant/components/yale/manifest.json @@ -11,6 +11,6 @@ ], "documentation": "https://www.home-assistant.io/integrations/yale", "iot_class": "cloud_push", - "loggers": ["pubnub", "yalexs"], + "loggers": ["socketio", "engineio", "yalexs"], "requirements": ["yalexs==8.5.4", "yalexs-ble==2.4.3"] } diff --git a/homeassistant/components/yale/sensor.py b/homeassistant/components/yale/sensor.py index f1931c112cb..bb3d4317277 100644 --- a/homeassistant/components/yale/sensor.py +++ b/homeassistant/components/yale/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Generic, TypeVar, cast +from typing import Any, cast from yalexs.activity import ActivityType, LockOperationActivity from yalexs.doorbell import Doorbell @@ -42,7 +42,7 @@ from .const import ( OPERATION_METHOD_REMOTE, OPERATION_METHOD_TAG, ) -from .entity import YaleDescriptionEntity, YaleEntityMixin +from .entity import YaleDescriptionEntity, YaleEntity 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 -_T = TypeVar("_T", LockDetail, KeypadDetail) - - @dataclass(frozen=True, kw_only=True) -class YaleSensorEntityDescription(SensorEntityDescription, Generic[_T]): +class YaleSensorEntityDescription[T: LockDetail | KeypadDetail]( + SensorEntityDescription +): """Mixin for required keys.""" - value_fn: Callable[[_T], int | None] + value_fn: Callable[[T], int | None] SENSOR_TYPE_DEVICE_BATTERY = YaleSensorEntityDescription[LockDetail]( @@ -112,7 +111,7 @@ async def async_setup_entry( async_add_entities(entities) -class YaleOperatorSensor(YaleEntityMixin, RestoreSensor): +class YaleOperatorSensor(YaleEntity, RestoreSensor): """Representation of an Yale lock operation sensor.""" _attr_translation_key = "operator" @@ -196,10 +195,12 @@ class YaleOperatorSensor(YaleEntityMixin, RestoreSensor): 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.""" - entity_description: YaleSensorEntityDescription[_T] + entity_description: YaleSensorEntityDescription[T] _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE diff --git a/homeassistant/components/yale/util.py b/homeassistant/components/yale/util.py index d8bdaab4a66..3462c576fd9 100644 --- a/homeassistant/components/yale/util.py +++ b/homeassistant/components/yale/util.py @@ -63,16 +63,11 @@ 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: + if start <= datetime.now() <= 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 diff --git a/tests/components/yale/__init__.py b/tests/components/yale/__init__.py index f0604940686..7f72d348042 100644 --- a/tests/components/yale/__init__.py +++ b/tests/components/yale/__init__.py @@ -1,12 +1 @@ """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", - }, -} diff --git a/tests/components/yale/snapshots/test_binary_sensor.ambr b/tests/components/yale/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..e294cb7c76c --- /dev/null +++ b/tests/components/yale/snapshots/test_binary_sensor.ambr @@ -0,0 +1,33 @@ +# serializer version: 1 +# name: test_doorbell_device_registry + DeviceRegistryEntrySnapshot({ + 'area_id': 'tmt100_name', + 'config_entries': , + 'configuration_url': 'https://account.aaecosystem.com', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + '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': , + 'serial_number': None, + 'suggested_area': 'tmt100 Name', + 'sw_version': '3.1.0-HYDRC75+201909251139', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/yale/snapshots/test_diagnostics.ambr b/tests/components/yale/snapshots/test_diagnostics.ambr index fd31bc0ec91..c3d8d8e2aaa 100644 --- a/tests/components/yale/snapshots/test_diagnostics.ambr +++ b/tests/components/yale/snapshots/test_diagnostics.ambr @@ -1,7 +1,7 @@ # serializer version: 1 # name: test_diagnostics dict({ - 'brand': 'yale_home', + 'brand': 'yale_global', 'doorbells': dict({ 'K98GiDT45GUL': dict({ 'HouseID': '**REDACTED**', diff --git a/tests/components/yale/test_binary_sensor.py b/tests/components/yale/test_binary_sensor.py index ad4d4155e5b..811c845e359 100644 --- a/tests/components/yale/test_binary_sensor.py +++ b/tests/components/yale/test_binary_sensor.py @@ -1,7 +1,9 @@ """The binary_sensor tests for the yale platform.""" 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.const import ( @@ -33,28 +35,19 @@ async def test_doorsense(hass: HomeAssistant) -> None: 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 + states = hass.states + assert states.get("binary_sensor.online_with_doorsense_name_door").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 + assert states.get("binary_sensor.online_with_doorsense_name_door").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 ( + 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: @@ -66,112 +59,78 @@ async def test_lock_bridge_offline(hass: HomeAssistant) -> None: 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" + states = hass.states + assert ( + 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: """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" + states = hass.states + assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_OFF + assert ( + states.get("binary_sensor.k98gidt45gul_name_image_capture").state == STATE_OFF ) - 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 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_image_capture.state == STATE_OFF - binary_sensor_k98gidt45gul_name_online = hass.states.get( - "binary_sensor.k98gidt45gul_name_connectivity" + assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_OFF + assert ( + 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: """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" + states = hass.states + assert states.get("binary_sensor.tmt100_name_motion").state == STATE_UNAVAILABLE + 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.""" 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" + states = hass.states + assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_ON + 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 - 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 + freezer.tick(40) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert states.get("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.""" 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" + states = hass.states + assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_OFF + 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( @@ -192,10 +151,7 @@ async def test_doorbell_update_via_socketio(hass: HomeAssistant) -> None: 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 + assert states.get("binary_sensor.k98gidt45gul_name_image_capture").state == STATE_ON listener( doorbell_one.device_id, @@ -226,29 +182,18 @@ async def test_doorbell_update_via_socketio(hass: HomeAssistant) -> None: await hass.async_block_till_done() - binary_sensor_k98gidt45gul_name_motion = hass.states.get( - "binary_sensor.k98gidt45gul_name_motion" + assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_ON + 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( - "binary_sensor.k98gidt45gul_name_doorbell_ding" + freezer.tick(40) + 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( doorbell_one.device_id, @@ -260,37 +205,28 @@ async def test_doorbell_update_via_socketio(hass: HomeAssistant) -> None: 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() + assert states.get("binary_sensor.k98gidt45gul_name_doorbell_ding").state == STATE_ON - binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_doorbell_ding" + freezer.tick(40) + 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( - hass: HomeAssistant, device_registry: dr.DeviceRegistry + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, ) -> 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" + assert reg_device == snapshot 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( 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 + states = hass.states + assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON listener = list(socketio._listeners)[0] listener( @@ -316,10 +249,10 @@ async def test_door_sense_update_via_socketio(hass: HomeAssistant) -> None: ) 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( 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() - 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 + + assert states.get("binary_sensor.online_with_doorsense_name_door").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 + assert states.get("binary_sensor.online_with_doorsense_name_door").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 + assert states.get("binary_sensor.online_with_doorsense_name_door").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 + assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON listener( 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() - 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 + assert states.get("binary_sensor.online_with_doorsense_name_door").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 + assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON await hass.config_entries.async_unload(config_entry.entry_id) 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.""" 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" + states = hass.states + assert ( + states.get( + "binary_sensor.a6697750d607098bae8d6baa11ef8063_name_doorbell_ding" + ).state + == STATE_OFF ) - assert ding_sensor.state == STATE_OFF diff --git a/tests/components/yale/test_config_flow.py b/tests/components/yale/test_config_flow.py index a62aa2d38f9..163f8240553 100644 --- a/tests/components/yale/test_config_flow.py +++ b/tests/components/yale/test_config_flow.py @@ -1,16 +1,16 @@ """Test the yale config flow.""" from collections.abc import Generator -from unittest.mock import Mock, patch +from unittest.mock import ANY, 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.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow @@ -44,7 +44,7 @@ async def test_full_flow( ) -> None: """Check full flow.""" 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( 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(mock_setup_entry.mock_calls) == 1 entry = hass.config_entries.async_entries(DOMAIN)[0] 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("current_request_with_host") diff --git a/tests/components/yale/test_event.py b/tests/components/yale/test_event.py index f2f205289ff..7aeb9d8f12b 100644 --- a/tests/components/yale/test_event.py +++ b/tests/components/yale/test_event.py @@ -1,7 +1,6 @@ """The event tests for the yale.""" -import datetime -from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant @@ -42,7 +41,9 @@ async def test_create_doorbell_offline(hass: HomeAssistant) -> None: 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.""" doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") 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.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() + freezer.tick(40) + async_fire_time_changed(hass) + 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: +async def test_doorbell_update_via_socketio( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test creation of a doorbell that can be updated via socketio.""" 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 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() + freezer.tick(40) + async_fire_time_changed(hass) + await hass.async_block_till_done() motion_state = hass.states.get("event.k98gidt45gul_name_motion") 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 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() + freezer.tick(40) + async_fire_time_changed(hass) + await hass.async_block_till_done() doorbell_state = hass.states.get("event.k98gidt45gul_name_doorbell") assert doorbell_state is not None diff --git a/tests/components/yale/test_init.py b/tests/components/yale/test_init.py index c9cb4be5882..4f0a853710c 100644 --- a/tests/components/yale/test_init.py +++ b/tests/components/yale/test_init.py @@ -89,16 +89,15 @@ async def test_unlock_throws_yale_api_http_error(hass: HomeAssistant) -> None: "unlock_return_activities": _unlock_return_activities_side_effect }, ) - last_err = None 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) - 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: @@ -119,16 +118,15 @@ async def test_lock_throws_yale_api_http_error(hass: HomeAssistant) -> None: "lock_return_activities": _lock_return_activities_side_effect }, ) - last_err = None 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) - 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( @@ -185,6 +183,7 @@ async def test_load_unload(hass: HomeAssistant) -> None: await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_load_triggers_ble_discovery(