diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 5d27efa285e..67e177d11d9 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -225,6 +225,13 @@ class AugustData: self._door_state_by_id = {} self._activities_by_id = {} + # We check the locks right away so we can + # remove inoperative ones + self._update_locks_status() + self._update_locks_detail() + + self._filter_inoperative_locks() + @property def house_ids(self): """Return a list of house_ids.""" @@ -352,6 +359,14 @@ class AugustData: self._lock_last_status_update_time_utc_by_id[lock_id] = update_start_time_utc return True + def lock_has_doorsense(self, lock_id): + """Determine if a lock has doorsense installed and can tell when the door is open or closed.""" + # We do not update here since this is not expected + # to change until restart + if self._lock_detail_by_id[lock_id] is None: + return False + return self._lock_detail_by_id[lock_id].doorsense + async def async_get_lock_status(self, lock_id): """Return status if the door is locked or unlocked. @@ -497,6 +512,33 @@ class AugustData: device_id, ) + def _filter_inoperative_locks(self): + # Remove non-operative locks as there must + # be a bridge (August Connect) for them to + # be usable + operative_locks = [] + for lock in self._locks: + lock_detail = self._lock_detail_by_id.get(lock.device_id) + if lock_detail is None: + _LOGGER.info( + "The lock %s could not be setup because the system could not fetch details about the lock.", + lock.device_name, + ) + elif lock_detail.bridge is None: + _LOGGER.info( + "The lock %s could not be setup because it does not have a bridge (Connect).", + lock.device_name, + ) + elif not lock_detail.bridge.operative: + _LOGGER.info( + "The lock %s could not be setup because the bridge (Connect) is not operative.", + lock.device_name, + ) + else: + operative_locks.append(lock) + + self._locks = operative_locks + def _call_api_operation_that_requires_bridge( device_name, operation_name, func, *args, **kwargs diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 4a4f0ece39b..aed1995d592 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -12,7 +12,7 @@ from . import DATA_AUGUST _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=10) +SCAN_INTERVAL = timedelta(seconds=5) async def _async_retrieve_door_state(data, lock): @@ -51,11 +51,15 @@ async def _async_activity_time_based_state(data, doorbell, activity_types): if latest is not None: start = latest.activity_start_time - end = latest.activity_end_time + timedelta(seconds=30) + end = latest.activity_end_time + timedelta(seconds=45) return start <= datetime.now() <= end return None +SENSOR_NAME = 0 +SENSOR_DEVICE_CLASS = 1 +SENSOR_STATE_PROVIDER = 2 + # sensor_type: [name, device_class, async_state_provider] SENSOR_TYPES_DOOR = {"door_open": ["Open", "door", _async_retrieve_door_state]} @@ -73,18 +77,17 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= for door in data.locks: for sensor_type in SENSOR_TYPES_DOOR: - async_state_provider = SENSOR_TYPES_DOOR[sensor_type][2] - if await async_state_provider(data, door) is LockDoorStatus.UNKNOWN: + if not data.lock_has_doorsense(door.device_id): _LOGGER.debug( "Not adding sensor class %s for lock %s ", - SENSOR_TYPES_DOOR[sensor_type][1], + SENSOR_TYPES_DOOR[sensor_type][SENSOR_DEVICE_CLASS], door.device_name, ) continue _LOGGER.debug( "Adding sensor class %s for %s", - SENSOR_TYPES_DOOR[sensor_type][1], + SENSOR_TYPES_DOOR[sensor_type][SENSOR_DEVICE_CLASS], door.device_name, ) devices.append(AugustDoorBinarySensor(data, sensor_type, door)) @@ -93,7 +96,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= for sensor_type in SENSOR_TYPES_DOORBELL: _LOGGER.debug( "Adding doorbell sensor class %s for %s", - SENSOR_TYPES_DOORBELL[sensor_type][1], + SENSOR_TYPES_DOORBELL[sensor_type][SENSOR_DEVICE_CLASS], doorbell.device_name, ) devices.append(AugustDoorbellBinarySensor(data, sensor_type, doorbell)) @@ -125,22 +128,25 @@ class AugustDoorBinarySensor(BinarySensorDevice): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return SENSOR_TYPES_DOOR[self._sensor_type][1] + return SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_DEVICE_CLASS] @property def name(self): """Return the name of the binary sensor.""" return "{} {}".format( - self._door.device_name, SENSOR_TYPES_DOOR[self._sensor_type][0] + self._door.device_name, SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_NAME] ) async def async_update(self): """Get the latest state of the sensor and update activity.""" - async_state_provider = SENSOR_TYPES_DOOR[self._sensor_type][2] - self._state = await async_state_provider(self._data, self._door) - self._available = self._state is not None - - self._state = self._state == LockDoorStatus.OPEN + async_state_provider = SENSOR_TYPES_DOOR[self._sensor_type][ + SENSOR_STATE_PROVIDER + ] + lock_door_state = await async_state_provider(self._data, self._door) + self._available = ( + lock_door_state is not None and lock_door_state != LockDoorStatus.UNKNOWN + ) + self._state = lock_door_state == LockDoorStatus.OPEN door_activity = await self._data.async_get_latest_device_activity( self._door.device_id, ActivityType.DOOR_OPERATION @@ -193,7 +199,8 @@ class AugustDoorBinarySensor(BinarySensorDevice): def unique_id(self) -> str: """Get the unique of the door open binary sensor.""" return "{:s}_{:s}".format( - self._door.device_id, SENSOR_TYPES_DOOR[self._sensor_type][0].lower() + self._door.device_id, + SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_NAME].lower(), ) @@ -221,25 +228,31 @@ class AugustDoorbellBinarySensor(BinarySensorDevice): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return SENSOR_TYPES_DOORBELL[self._sensor_type][1] + return SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_DEVICE_CLASS] @property def name(self): """Return the name of the binary sensor.""" return "{} {}".format( - self._doorbell.device_name, SENSOR_TYPES_DOORBELL[self._sensor_type][0] + self._doorbell.device_name, + SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME], ) async def async_update(self): """Get the latest state of the sensor.""" - async_state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][2] + async_state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][ + SENSOR_STATE_PROVIDER + ] self._state = await async_state_provider(self._data, self._doorbell) - self._available = self._doorbell.is_online + # The doorbell will go into standby mode when there is no motion + # for a short while. It will wake by itself when needed so we need + # to consider is available or we will not report motion or dings + self._available = self._doorbell.is_online or self._doorbell.status == "standby" @property def unique_id(self) -> str: """Get the unique id of the doorbell sensor.""" return "{:s}_{:s}".format( self._doorbell.device_id, - SENSOR_TYPES_DOORBELL[self._sensor_type][0].lower(), + SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME].lower(), ) diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 1e64ef59944..9d5df1192a7 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -67,7 +67,9 @@ class AugustLock(LockDevice): async def async_update(self): """Get the latest state of the sensor and update activity.""" self._lock_status = await self._data.async_get_lock_status(self._lock.device_id) - self._available = self._lock_status is not None + self._available = ( + self._lock_status is not None and self._lock_status != LockStatus.UNKNOWN + ) self._lock_detail = await self._data.async_get_lock_detail(self._lock.device_id) lock_activity = await self._data.async_get_latest_device_activity( diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index 0266f6fb5bf..9be8f697b8b 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, PropertyMock from august.activity import Activity from august.api import Api from august.exceptions import AugustApiHTTPError -from august.lock import Lock +from august.lock import Lock, LockDetail from homeassistant.components.august import AugustData from homeassistant.components.august.binary_sensor import AugustDoorBinarySensor @@ -13,15 +13,7 @@ from homeassistant.components.august.lock import AugustLock from homeassistant.util import dt -class MockAugustApi(Api): - """A mock for py-august Api class.""" - - def _call_api(self, *args, **kwargs): - """Mock the time activity started.""" - raise AugustApiHTTPError("This should bubble up as its user consumable") - - -class MockAugustApiFailing(MockAugustApi): +class MockAugustApiFailing(Api): """A mock for py-august Api class that always has an AugustApiHTTPError.""" def _call_api(self, *args, **kwargs): @@ -94,7 +86,7 @@ class MockAugustComponentData(AugustData): self, last_lock_status_update_timestamp=1, last_door_state_update_timestamp=1, - api=MockAugustApi(), + api=MockAugustApiFailing(), access_token="mocked_access_token", locks=[], doorbells=[], @@ -158,8 +150,46 @@ def _mock_august_authentication(token_text, token_timestamp): return authentication -def _mock_august_lock(): - return Lock( - "mockdeviceid1", - {"LockName": "Mocked Lock 1", "HouseID": "mockhouseid1", "UserType": "owner"}, - ) +def _mock_august_lock(lockid="mocklockid1", houseid="mockhouseid1"): + return Lock(lockid, _mock_august_lock_data(lockid=lockid, houseid=houseid)) + + +def _mock_august_lock_data(lockid="mocklockid1", houseid="mockhouseid1"): + return { + "_id": lockid, + "LockID": lockid, + "LockName": lockid + " Name", + "HouseID": houseid, + "UserType": "owner", + "SerialNumber": "mockserial", + "battery": 90, + "currentFirmwareVersion": "mockfirmware", + "Bridge": { + "_id": "bridgeid1", + "firmwareVersion": "mockfirm", + "operative": True, + }, + "LockStatus": {"doorState": "open"}, + } + + +def _mock_operative_august_lock_detail(lockid): + operative_lock_detail_data = _mock_august_lock_data(lockid=lockid) + return LockDetail(operative_lock_detail_data) + + +def _mock_inoperative_august_lock_detail(lockid): + inoperative_lock_detail_data = _mock_august_lock_data(lockid=lockid) + del inoperative_lock_detail_data["Bridge"] + return LockDetail(inoperative_lock_detail_data) + + +def _mock_doorsense_enabled_august_lock_detail(lockid): + doorsense_lock_detail_data = _mock_august_lock_data(lockid=lockid) + return LockDetail(doorsense_lock_detail_data) + + +def _mock_doorsense_missing_august_lock_detail(lockid): + doorsense_lock_detail_data = _mock_august_lock_data(lockid=lockid) + del doorsense_lock_detail_data["LockStatus"]["doorState"] + return LockDetail(doorsense_lock_detail_data) diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index cf2555e67a5..3a43a0a841a 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -2,6 +2,9 @@ import asyncio from unittest.mock import MagicMock +from august.lock import LockDetail +from requests import RequestException + from homeassistant.components import august from homeassistant.exceptions import HomeAssistantError @@ -11,6 +14,10 @@ from tests.components.august.mocks import ( _mock_august_authentication, _mock_august_authenticator, _mock_august_lock, + _mock_doorsense_enabled_august_lock_detail, + _mock_doorsense_missing_august_lock_detail, + _mock_inoperative_august_lock_detail, + _mock_operative_august_lock_detail, ) @@ -19,7 +26,7 @@ def test_get_lock_name(): data = MockAugustComponentData(last_lock_status_update_timestamp=1) lock = _mock_august_lock() data.set_mocked_locks([lock]) - assert data.get_lock_name("mockdeviceid1") == "Mocked Lock 1" + assert data.get_lock_name("mocklockid1") == "mocklockid1 Name" def test_unlock_throws_august_api_http_error(): @@ -29,11 +36,12 @@ def test_unlock_throws_august_api_http_error(): data.set_mocked_locks([lock]) last_err = None try: - data.unlock("mockdeviceid1") + data.unlock("mocklockid1") except HomeAssistantError as err: last_err = err assert ( - str(last_err) == "Mocked Lock 1: This should bubble up as its user consumable" + str(last_err) + == "mocklockid1 Name: This should bubble up as its user consumable" ) @@ -44,14 +52,51 @@ def test_lock_throws_august_api_http_error(): data.set_mocked_locks([lock]) last_err = None try: - data.unlock("mockdeviceid1") + data.unlock("mocklockid1") except HomeAssistantError as err: last_err = err assert ( - str(last_err) == "Mocked Lock 1: This should bubble up as its user consumable" + str(last_err) + == "mocklockid1 Name: This should bubble up as its user consumable" ) +def test_inoperative_locks_are_filtered_out(): + """Ensure inoperative locks do not get setup.""" + august_operative_lock = _mock_operative_august_lock_detail("oplockid1") + data = _create_august_data_with_lock_details( + [august_operative_lock, _mock_inoperative_august_lock_detail("inoplockid1")] + ) + + assert len(data.locks) == 1 + assert data.locks[0].device_id == "oplockid1" + + +def test_lock_has_doorsense(): + """Check to see if a lock has doorsense.""" + data = _create_august_data_with_lock_details( + [ + _mock_doorsense_enabled_august_lock_detail("doorsenselock1"), + _mock_doorsense_missing_august_lock_detail("nodoorsenselock1"), + RequestException("mocked request error"), + RequestException("mocked request error"), + ] + ) + + assert data.lock_has_doorsense("doorsenselock1") is True + assert data.lock_has_doorsense("nodoorsenselock1") is False + + # The api calls are mocked to fail on the second + # run of async_get_lock_detail + # + # This will be switched to await data.async_get_lock_detail("doorsenselock1") + # once we mock the full home assistant setup + data._update_locks_detail() + # doorsenselock1 should be false if we cannot tell due + # to an api error + assert data.lock_has_doorsense("doorsenselock1") is False + + async def test__refresh_access_token(hass): """Test refresh of the access token.""" authentication = _mock_august_authentication("original_token", 1234) @@ -72,3 +117,21 @@ async def test__refresh_access_token(hass): authenticator.refresh_access_token.assert_called() assert data._access_token == "new_token" assert data._access_token_expires == 5678 + + +def _create_august_data_with_lock_details(lock_details): + locks = [] + for lock in lock_details: + if isinstance(lock, LockDetail): + locks.append(_mock_august_lock(lock.device_id)) + authentication = _mock_august_authentication("original_token", 1234) + authenticator = _mock_august_authenticator() + token_refresh_lock = MagicMock() + api = MagicMock() + api.get_lock_status = MagicMock(return_value=(MagicMock(), MagicMock())) + api.get_lock_detail = MagicMock(side_effect=lock_details) + api.get_operable_locks = MagicMock(return_value=locks) + api.get_doorbells = MagicMock(return_value=[]) + return august.AugustData( + MagicMock(), api, authentication, authenticator, token_refresh_lock + ) diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index d63a861fa28..8b036861899 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -21,10 +21,7 @@ from tests.components.august.mocks import ( def test__sync_lock_activity_locked_via_onetouchlock(): """Test _sync_lock_activity locking.""" - data = MockAugustComponentData(last_lock_status_update_timestamp=1) - august_lock = _mock_august_lock() - data.set_mocked_locks([august_lock]) - lock = MockAugustComponentLock(data, august_lock) + lock = _mocked_august_component_lock() lock_activity_start_timestamp = 1234 lock_activity = MockActivity( action=ACTION_LOCK_ONETOUCHLOCK, @@ -40,10 +37,7 @@ def test__sync_lock_activity_locked_via_onetouchlock(): def test__sync_lock_activity_locked_via_lock(): """Test _sync_lock_activity locking.""" - data = MockAugustComponentData(last_lock_status_update_timestamp=1) - august_lock = _mock_august_lock() - data.set_mocked_locks([august_lock]) - lock = MockAugustComponentLock(data, august_lock) + lock = _mocked_august_component_lock() lock_activity_start_timestamp = 1234 lock_activity = MockActivity( action=ACTION_LOCK_LOCK, @@ -59,10 +53,7 @@ def test__sync_lock_activity_locked_via_lock(): def test__sync_lock_activity_unlocked(): """Test _sync_lock_activity unlocking.""" - data = MockAugustComponentData(last_lock_status_update_timestamp=1) - august_lock = _mock_august_lock() - data.set_mocked_locks([august_lock]) - lock = MockAugustComponentLock(data, august_lock) + lock = _mocked_august_component_lock() lock_activity_timestamp = 1234 lock_activity = MockActivity( action=ACTION_LOCK_UNLOCK, @@ -110,3 +101,10 @@ def test__sync_lock_activity_ignores_old_data(): assert lock.last_update_lock_status["activity_start_time_utc"] == dt.as_utc( datetime.datetime.fromtimestamp(first_lock_activity_timestamp) ) + + +def _mocked_august_component_lock(): + data = MockAugustComponentData(last_lock_status_update_timestamp=1) + august_lock = _mock_august_lock() + data.set_mocked_locks([august_lock]) + return MockAugustComponentLock(data, august_lock)