From bfe3ef09800df2cbca8e4f3a249f983879b60808 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Jul 2021 18:48:15 -1000 Subject: [PATCH] Update august to support locking, unlocking, jammed (#52814) --- homeassistant/components/august/lock.py | 22 ++- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/august/test_lock.py | 129 ++++++++++++++++-- .../fixtures/august/get_activity.jammed.json | 34 +++++ .../fixtures/august/get_activity.locking.json | 34 +++++ .../august/get_activity.unlocking.json | 34 +++++ 8 files changed, 245 insertions(+), 14 deletions(-) create mode 100644 tests/fixtures/august/get_activity.jammed.json create mode 100644 tests/fixtures/august/get_activity.locking.json create mode 100644 tests/fixtures/august/get_activity.unlocking.json diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index e74eded3557..5f4fe85bc71 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -1,6 +1,7 @@ """Support for August lock.""" import logging +from aiohttp import ClientResponseError from yalexs.activity import SOURCE_PUBNUB, ActivityType from yalexs.lock import LockStatus from yalexs.util import update_lock_detail_from_activity @@ -9,12 +10,15 @@ from homeassistant.components.lock import ATTR_CHANGED_BY, LockEntity from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.core import callback from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.util.dt as dt_util from .const import DATA_AUGUST, DOMAIN from .entity import AugustEntityMixin _LOGGER = logging.getLogger(__name__) +LOCK_JAMMED_ERR = 531 + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up August locks.""" @@ -44,9 +48,17 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): await self._call_lock_operation(self._data.async_unlock) async def _call_lock_operation(self, lock_operation): - activities = await lock_operation(self._device_id) - for lock_activity in activities: - update_lock_detail_from_activity(self._detail, lock_activity) + 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( @@ -91,6 +103,10 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): else: self._attr_is_locked = self._lock_status is LockStatus.LOCKED + self._attr_is_jammed = self._lock_status is LockStatus.JAMMED + self._attr_is_locking = self._lock_status is LockStatus.LOCKING + self._attr_is_unlocking = self._lock_status is LockStatus.UNLOCKING + self._attr_extra_state_attributes = { ATTR_BATTERY_LEVEL: self._detail.battery_level } diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index e966338f287..9f8b435b714 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.1.11"], + "requirements": ["yalexs==1.1.12"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index fb87a9d615d..a0b2fa1d554 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2401,7 +2401,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.3 # homeassistant.components.august -yalexs==1.1.11 +yalexs==1.1.12 # homeassistant.components.yeelight yeelight==0.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 42197c6f659..36cfed2598e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1313,7 +1313,7 @@ xknx==0.18.8 xmltodict==0.12.0 # homeassistant.components.august -yalexs==1.1.11 +yalexs==1.1.12 # homeassistant.components.yeelight yeelight==0.6.3 diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index 5b3c163780f..a0b44d4fb79 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -2,9 +2,16 @@ import datetime from unittest.mock import Mock +from aiohttp import ClientResponseError +import pytest from yalexs.pubnub_async import AugustPubNub -from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN +from homeassistant.components.lock import ( + DOMAIN as LOCK_DOMAIN, + STATE_JAMMED, + STATE_LOCKING, + STATE_UNLOCKING, +) from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_LOCK, @@ -59,6 +66,44 @@ async def test_lock_changed_by(hass): ) +async def test_state_locking(hass): + """Test creation of a lock with doorsense and bridge that is locking.""" + lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) + + activities = await _mock_activities_from_fixture(hass, "get_activity.locking.json") + await _create_august_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): + """Test creation of a lock with doorsense and bridge that is unlocking.""" + lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) + + activities = await _mock_activities_from_fixture( + hass, "get_activity.unlocking.json" + ) + await _create_august_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): + """Test creation of a lock with doorsense and bridge that is jammed.""" + lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) + + activities = await _mock_activities_from_fixture(hass, "get_activity.jammed.json") + await _create_august_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): """Test creation of a lock with doorsense and bridge.""" lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) @@ -109,6 +154,74 @@ async def test_one_lock_operation(hass): ) +async def test_lock_jammed(hass): + """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_august_lock_detail(hass) + await _create_august_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"} + assert 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): + """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_august_lock_detail(hass) + await _create_august_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): + assert await hass.services.async_call( + LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True + ) + await hass.async_block_till_done() + + async def test_one_lock_unknown_state(hass): """Test creation of a lock with doorsense and bridge.""" lock_one = await _mock_lock_from_fixture( @@ -178,7 +291,7 @@ async def test_lock_update_via_pubnub(hass): 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.state == STATE_UNLOCKING pubnub.message( pubnub, @@ -193,24 +306,24 @@ async def test_lock_update_via_pubnub(hass): 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 + 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_LOCKED + assert lock_online_with_doorsense_name.state == STATE_LOCKING pubnub.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_LOCKED + assert lock_online_with_doorsense_name.state == STATE_LOCKING # Ensure pubnub 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_LOCKED + assert lock_online_with_doorsense_name.state == STATE_LOCKING pubnub.message( pubnub, @@ -224,12 +337,12 @@ async def test_lock_update_via_pubnub(hass): ) 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.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_UNLOCKED + 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() diff --git a/tests/fixtures/august/get_activity.jammed.json b/tests/fixtures/august/get_activity.jammed.json new file mode 100644 index 00000000000..be5b9dfa4eb --- /dev/null +++ b/tests/fixtures/august/get_activity.jammed.json @@ -0,0 +1,34 @@ +[{ + "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" + } +}] diff --git a/tests/fixtures/august/get_activity.locking.json b/tests/fixtures/august/get_activity.locking.json new file mode 100644 index 00000000000..c1f07e47312 --- /dev/null +++ b/tests/fixtures/august/get_activity.locking.json @@ -0,0 +1,34 @@ +[{ + "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" + } +}] diff --git a/tests/fixtures/august/get_activity.unlocking.json b/tests/fixtures/august/get_activity.unlocking.json new file mode 100644 index 00000000000..788a69164aa --- /dev/null +++ b/tests/fixtures/august/get_activity.unlocking.json @@ -0,0 +1,34 @@ +[{ + "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" + } +}]