diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 67e177d11d9861..7c7108943fbd53 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -18,7 +18,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle, dt +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -45,11 +45,6 @@ # avoid hitting rate limits MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES = timedelta(seconds=1800) -# Limit locks status check to 900 seconds now that -# we get the state from the lock and unlock api calls -# and the lock and unlock activities are now captured -MIN_TIME_BETWEEN_LOCK_STATUS_UPDATES = timedelta(seconds=900) - # Doorbells need to update more frequently than locks # since we get an image from the doorbell api MIN_TIME_BETWEEN_DOORBELL_STATUS_UPDATES = timedelta(seconds=20) @@ -218,16 +213,11 @@ def __init__(self, hass, api, authentication, authenticator, token_refresh_lock) self._house_ids.add(device.house_id) self._doorbell_detail_by_id = {} - self._door_last_state_update_time_utc_by_id = {} - self._lock_last_status_update_time_utc_by_id = {} - self._lock_status_by_id = {} self._lock_detail_by_id = {} - 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() @@ -344,8 +334,13 @@ def update_door_state(self, lock_id, door_state, update_start_time_utc): This is called when newer activity is detected on the activity feed in order to keep the internal data in sync """ - self._door_state_by_id[lock_id] = door_state - self._door_last_state_update_time_utc_by_id[lock_id] = update_start_time_utc + # When syncing the door state became available via py-august, this + # function caused to be actively used. It will be again as we will + # update the door state from lock/unlock operations as the august api + # does report the door state on lock/unlock, however py-august does not + # expose this to us yet. + self._lock_detail_by_id[lock_id].door_state = door_state + self._lock_detail_by_id[lock_id].door_state_datetime = update_start_time_utc return True def update_lock_status(self, lock_id, lock_status, update_start_time_utc): @@ -355,8 +350,8 @@ def update_lock_status(self, lock_id, lock_status, update_start_time_utc): or newer activity is detected on the activity feed in order to keep the internal data in sync """ - self._lock_status_by_id[lock_id] = lock_status - self._lock_last_status_update_time_utc_by_id[lock_id] = update_start_time_utc + self._lock_detail_by_id[lock_id].lock_status = lock_status + self._lock_detail_by_id[lock_id].lock_status_datetime = update_start_time_utc return True def lock_has_doorsense(self, lock_id): @@ -367,18 +362,10 @@ def lock_has_doorsense(self, lock_id): 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. - - This is status for the lock itself. - """ - await self._async_update_locks() - return self._lock_status_by_id.get(lock_id) - async def async_get_lock_detail(self, lock_id): """Return lock detail.""" - await self._async_update_locks() - return self._lock_detail_by_id.get(lock_id) + await self._async_update_locks_detail() + return self._lock_detail_by_id[lock_id] def get_lock_name(self, device_id): """Return lock name as August has it stored.""" @@ -386,85 +373,6 @@ def get_lock_name(self, device_id): if lock.device_id == device_id: return lock.device_name - async def async_get_door_state(self, lock_id): - """Return status if the door is open or closed. - - This is the status from the door sensor. - """ - await self._async_update_locks_status() - return self._door_state_by_id.get(lock_id) - - async def _async_update_locks(self): - await self._async_update_locks_status() - await self._async_update_locks_detail() - - @Throttle(MIN_TIME_BETWEEN_LOCK_STATUS_UPDATES) - async def _async_update_locks_status(self): - await self._hass.async_add_executor_job(self._update_locks_status) - - def _update_locks_status(self): - status_by_id = {} - state_by_id = {} - lock_last_status_update_by_id = {} - door_last_state_update_by_id = {} - - _LOGGER.debug("Start retrieving lock and door status") - for lock in self._locks: - update_start_time_utc = dt.utcnow() - _LOGGER.debug("Updating lock and door status for %s", lock.device_name) - try: - ( - status_by_id[lock.device_id], - state_by_id[lock.device_id], - ) = self._api.get_lock_status( - self._access_token, lock.device_id, door_status=True - ) - # Since there is a a race condition between calling the - # lock and activity apis, we set the last update time - # BEFORE making the api call since we will compare this - # to activity later we want activity to win over stale lock/door - # state. - lock_last_status_update_by_id[lock.device_id] = update_start_time_utc - door_last_state_update_by_id[lock.device_id] = update_start_time_utc - except RequestException as ex: - _LOGGER.error( - "Request error trying to retrieve lock and door status for %s. %s", - lock.device_name, - ex, - ) - status_by_id[lock.device_id] = None - state_by_id[lock.device_id] = None - except Exception: - status_by_id[lock.device_id] = None - state_by_id[lock.device_id] = None - raise - - _LOGGER.debug("Completed retrieving lock and door status") - self._lock_status_by_id = status_by_id - self._door_state_by_id = state_by_id - self._door_last_state_update_time_utc_by_id = door_last_state_update_by_id - self._lock_last_status_update_time_utc_by_id = lock_last_status_update_by_id - - def get_last_lock_status_update_time_utc(self, lock_id): - """Return the last time that a lock status update was seen from the august API.""" - # Since the activity api is called more frequently than - # the lock api it is possible that the lock has not - # been updated yet - if lock_id not in self._lock_last_status_update_time_utc_by_id: - return dt.utc_from_timestamp(0) - - return self._lock_last_status_update_time_utc_by_id[lock_id] - - def get_last_door_state_update_time_utc(self, lock_id): - """Return the last time that a door status update was seen from the august API.""" - # Since the activity api is called more frequently than - # the lock api it is possible that the door has not - # been updated yet - if lock_id not in self._door_last_state_update_time_utc_by_id: - return dt.utc_from_timestamp(0) - - return self._door_last_state_update_time_utc_by_id[lock_id] - @Throttle(MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES) async def _async_update_locks_detail(self): await self._hass.async_add_executor_job(self._update_locks_detail) diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index aed1995d592bfc..935642585fdcdc 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -2,11 +2,11 @@ from datetime import datetime, timedelta import logging -from august.activity import ACTIVITY_ACTION_STATES, ActivityType +from august.activity import ActivityType from august.lock import LockDoorStatus +from august.util import update_lock_detail_from_activity from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.util import dt from . import DATA_AUGUST @@ -15,11 +15,6 @@ SCAN_INTERVAL = timedelta(seconds=5) -async def _async_retrieve_door_state(data, lock): - """Get the latest state of the DoorSense sensor.""" - return await data.async_get_door_state(lock.device_id) - - async def _async_retrieve_online_state(data, doorbell): """Get the latest state of the sensor.""" detail = await data.async_get_doorbell_detail(doorbell.device_id) @@ -61,8 +56,6 @@ async def _async_activity_time_based_state(data, doorbell, activity_types): SENSOR_STATE_PROVIDER = 2 # sensor_type: [name, device_class, async_state_provider] -SENSOR_TYPES_DOOR = {"door_open": ["Open", "door", _async_retrieve_door_state]} - SENSOR_TYPES_DOORBELL = { "doorbell_ding": ["Ding", "occupancy", _async_retrieve_ding_state], "doorbell_motion": ["Motion", "motion", _async_retrieve_motion_state], @@ -76,21 +69,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= devices = [] for door in data.locks: - for sensor_type in SENSOR_TYPES_DOOR: - if not data.lock_has_doorsense(door.device_id): - _LOGGER.debug( - "Not adding sensor class %s for lock %s ", - SENSOR_TYPES_DOOR[sensor_type][SENSOR_DEVICE_CLASS], - door.device_name, - ) - continue - + if not data.lock_has_doorsense(door.device_id): _LOGGER.debug( - "Adding sensor class %s for %s", - SENSOR_TYPES_DOOR[sensor_type][SENSOR_DEVICE_CLASS], - door.device_name, + "Not adding sensor class door for lock %s ", door.device_name, ) - devices.append(AugustDoorBinarySensor(data, sensor_type, door)) + continue + + _LOGGER.debug( + "Adding sensor class door for %s", door.device_name, + ) + devices.append(AugustDoorBinarySensor(data, "door_open", door)) for doorbell in data.doorbells: for sensor_type in SENSOR_TYPES_DOORBELL: @@ -127,81 +115,35 @@ def is_on(self): @property def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_DEVICE_CLASS] + """Return the class of this device.""" + return "door" @property def name(self): """Return the name of the binary sensor.""" - return "{} {}".format( - self._door.device_name, SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_NAME] - ) + return "{} Open".format(self._door.device_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][ - 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 ) + detail = await self._data.async_get_lock_detail(self._door.device_id) if door_activity is not None: - self._sync_door_activity(door_activity) - - def _update_door_state(self, door_state, update_start_time): - new_state = door_state == LockDoorStatus.OPEN - if self._state != new_state: - self._state = new_state - self._data.update_door_state( - self._door.device_id, door_state, update_start_time - ) - - def _sync_door_activity(self, door_activity): - """Check the activity for the latest door open/close activity (events). + update_lock_detail_from_activity(detail, door_activity) - We use this to determine the door state in between calls to the lock - api as we update it more frequently - """ - last_door_state_update_time_utc = self._data.get_last_door_state_update_time_utc( - self._door.device_id - ) - activity_end_time_utc = dt.as_utc(door_activity.activity_end_time) + lock_door_state = None + if detail is not None: + lock_door_state = detail.door_state - if activity_end_time_utc > last_door_state_update_time_utc: - _LOGGER.debug( - "The activity log has new events for %s: [action=%s] [activity_end_time_utc=%s] > [last_door_state_update_time_utc=%s]", - self.name, - door_activity.action, - activity_end_time_utc, - last_door_state_update_time_utc, - ) - activity_start_time_utc = dt.as_utc(door_activity.activity_start_time) - if door_activity.action in ACTIVITY_ACTION_STATES: - self._update_door_state( - ACTIVITY_ACTION_STATES[door_activity.action], - activity_start_time_utc, - ) - else: - _LOGGER.info( - "Unhandled door activity action %s for %s", - door_activity.action, - self.name, - ) + self._available = lock_door_state != LockDoorStatus.UNKNOWN + self._state = lock_door_state == LockDoorStatus.OPEN @property 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][SENSOR_NAME].lower(), - ) + return f"{self._door.device_id}_open" class AugustDoorbellBinarySensor(BinarySensorDevice): diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 9d5df1192a7b9a..0097b6029a06be 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -2,8 +2,9 @@ from datetime import timedelta import logging -from august.activity import ACTIVITY_ACTION_STATES, ActivityType +from august.activity import ActivityType from august.lock import LockStatus +from august.util import update_lock_detail_from_activity from homeassistant.components.lock import LockDevice from homeassistant.const import ATTR_BATTERY_LEVEL @@ -13,7 +14,7 @@ _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=10) +SCAN_INTERVAL = timedelta(seconds=5) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -66,51 +67,19 @@ def _update_lock_status(self, lock_status, update_start_time_utc): 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 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( self._lock.device_id, ActivityType.LOCK_OPERATION ) if lock_activity is not None: self._changed_by = lock_activity.operated_by - self._sync_lock_activity(lock_activity) + update_lock_detail_from_activity(self._lock_detail, lock_activity) - def _sync_lock_activity(self, lock_activity): - """Check the activity for the latest lock/unlock activity (events). - - We use this to determine the lock state in between calls to the lock - api as we update it more frequently - """ - last_lock_status_update_time_utc = self._data.get_last_lock_status_update_time_utc( - self._lock.device_id + self._lock_status = self._lock_detail.lock_status + self._available = ( + self._lock_status is not None and self._lock_status != LockStatus.UNKNOWN ) - activity_end_time_utc = dt.as_utc(lock_activity.activity_end_time) - - if activity_end_time_utc > last_lock_status_update_time_utc: - _LOGGER.debug( - "The activity log has new events for %s: [action=%s] [activity_end_time_utc=%s] > [last_lock_status_update_time_utc=%s]", - self.name, - lock_activity.action, - activity_end_time_utc, - last_lock_status_update_time_utc, - ) - activity_start_time_utc = dt.as_utc(lock_activity.activity_start_time) - if lock_activity.action in ACTIVITY_ACTION_STATES: - self._update_lock_status( - ACTIVITY_ACTION_STATES[lock_activity.action], - activity_start_time_utc, - ) - else: - _LOGGER.info( - "Unhandled lock activity action %s for %s", - lock_activity.action, - self.name, - ) @property def name(self): diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 7afa742f3cac00..53bbdaaa33fa75 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": ["py-august==0.14.0"], + "requirements": ["py-august==0.17.0"], "dependencies": ["configurator"], "codeowners": ["@bdraco"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2fef238a7ed51a..c58a841fcb3299 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1075,7 +1075,7 @@ pushover_complete==1.1.1 pwmled==1.5.0 # homeassistant.components.august -py-august==0.14.0 +py-august==0.17.0 # homeassistant.components.canary py-canary==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c22db276bca31..4add975522950f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -391,7 +391,7 @@ pure-python-adb==0.2.2.dev0 pushbullet.py==0.11.0 # homeassistant.components.august -py-august==0.14.0 +py-august==0.17.0 # homeassistant.components.canary py-canary==0.5.0 diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index 9be8f697b8b018..30269bec11e883 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -1,17 +1,83 @@ """Mocks for the august component.""" import datetime +import json +import os from unittest.mock import MagicMock, PropertyMock +from asynctest import mock from august.activity import Activity from august.api import Api +from august.authenticator import AuthenticationState +from august.doorbell import Doorbell, DoorbellDetail from august.exceptions import AugustApiHTTPError -from august.lock import Lock, LockDetail - -from homeassistant.components.august import AugustData +from august.lock import Lock, LockDetail, LockStatus + +from homeassistant.components.august import ( + CONF_LOGIN_METHOD, + CONF_PASSWORD, + CONF_USERNAME, + DOMAIN, + AugustData, +) from homeassistant.components.august.binary_sensor import AugustDoorBinarySensor -from homeassistant.components.august.lock import AugustLock +from homeassistant.setup import async_setup_component from homeassistant.util import dt +from tests.common import load_fixture + + +def _mock_get_config(): + """Return a default august config.""" + return { + DOMAIN: { + CONF_LOGIN_METHOD: "email", + CONF_USERNAME: "mocked_username", + CONF_PASSWORD: "mocked_password", + } + } + + +@mock.patch("homeassistant.components.august.Api") +@mock.patch("homeassistant.components.august.Authenticator.authenticate") +async def _mock_setup_august(hass, api_mocks_callback, authenticate_mock, api_mock): + """Set up august integration.""" + authenticate_mock.side_effect = MagicMock( + return_value=_mock_august_authentication("original_token", 1234) + ) + api_mocks_callback(api_mock) + assert await async_setup_component(hass, DOMAIN, _mock_get_config()) + await hass.async_block_till_done() + return True + + +async def _create_august_with_devices(hass, lock_details=[], doorbell_details=[]): + locks = [] + doorbells = [] + for lock in lock_details: + if isinstance(lock, LockDetail): + locks.append(_mock_august_lock(lock.device_id)) + for doorbell in doorbell_details: + if isinstance(lock, DoorbellDetail): + doorbells.append(_mock_august_doorbell(doorbell.device_id)) + + def api_mocks_callback(api): + def get_lock_detail_side_effect(access_token, device_id): + for lock in lock_details: + if isinstance(lock, LockDetail) and lock.device_id == device_id: + return lock + + api_instance = MagicMock() + api_instance.get_lock_detail.side_effect = get_lock_detail_side_effect + api_instance.get_operable_locks.return_value = locks + api_instance.get_doorbells.return_value = doorbells + api_instance.lock.return_value = LockStatus.LOCKED + api_instance.unlock.return_value = LockStatus.UNLOCKED + api.return_value = api_instance + + await _mock_setup_august(hass, api_mocks_callback) + + return True + class MockAugustApiFailing(Api): """A mock for py-august Api class that always has an AugustApiHTTPError.""" @@ -61,21 +127,6 @@ def _update_door_state(self, door_state, activity_start_time_utc): self.last_update_door_state["activity_start_time_utc"] = activity_start_time_utc -class MockAugustComponentLock(AugustLock): - """A mock for august component AugustLock class.""" - - def _update_lock_status(self, lock_status, activity_start_time_utc): - """Mock updating the lock status.""" - self._data.set_last_lock_status_update_time_utc( - self._lock.device_id, activity_start_time_utc - ) - self.last_update_lock_status = {} - self.last_update_lock_status["lock_status"] = lock_status - self.last_update_lock_status[ - "activity_start_time_utc" - ] = activity_start_time_utc - - class MockAugustComponentData(AugustData): """A wrapper to mock AugustData.""" @@ -143,6 +194,9 @@ def _mock_august_authenticator(): def _mock_august_authentication(token_text, token_timestamp): authentication = MagicMock(name="august.authentication") + type(authentication).state = PropertyMock( + return_value=AuthenticationState.AUTHENTICATED + ) type(authentication).access_token = PropertyMock(return_value=token_text) type(authentication).access_token_expires = PropertyMock( return_value=token_timestamp @@ -154,6 +208,31 @@ def _mock_august_lock(lockid="mocklockid1", houseid="mockhouseid1"): return Lock(lockid, _mock_august_lock_data(lockid=lockid, houseid=houseid)) +def _mock_august_doorbell(deviceid="mockdeviceid1", houseid="mockhouseid1"): + return Doorbell( + deviceid, _mock_august_doorbell_data(device=deviceid, houseid=houseid) + ) + + +def _mock_august_doorbell_data(deviceid="mockdeviceid1", houseid="mockhouseid1"): + return { + "_id": deviceid, + "DeviceID": deviceid, + "DeviceName": deviceid + " Name", + "HouseID": houseid, + "UserType": "owner", + "SerialNumber": "mockserial", + "battery": 90, + "currentFirmwareVersion": "mockfirmware", + "Bridge": { + "_id": "bridgeid1", + "firmwareVersion": "mockfirm", + "operative": True, + }, + "LockStatus": {"doorState": "open"}, + } + + def _mock_august_lock_data(lockid="mocklockid1", houseid="mockhouseid1"): return { "_id": lockid, @@ -189,6 +268,18 @@ def _mock_doorsense_enabled_august_lock_detail(lockid): return LockDetail(doorsense_lock_detail_data) +async def _mock_lock_from_fixture(hass, path): + json_dict = await _load_json_fixture(hass, path) + return LockDetail(json_dict) + + +async def _load_json_fixture(hass, path): + fixture = await hass.async_add_executor_job( + load_fixture, os.path.join("august", path) + ) + return json.loads(fixture) + + 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"] diff --git a/tests/components/august/test_binary_sensor.py b/tests/components/august/test_binary_sensor.py index 0fbd120ea8b364..5988e21ebac827 100644 --- a/tests/components/august/test_binary_sensor.py +++ b/tests/components/august/test_binary_sensor.py @@ -1,89 +1 @@ -"""The lock tests for the august platform.""" - -import datetime - -from august.activity import ACTION_DOOR_CLOSED, ACTION_DOOR_OPEN -from august.lock import LockDoorStatus - -from homeassistant.util import dt - -from tests.components.august.mocks import ( - MockActivity, - MockAugustComponentData, - MockAugustComponentDoorBinarySensor, - _mock_august_lock, -) - - -def test__sync_door_activity_doored_via_dooropen(): - """Test _sync_door_activity dooropen.""" - data = MockAugustComponentData(last_door_state_update_timestamp=1) - lock = _mock_august_lock() - data.set_mocked_locks([lock]) - door = MockAugustComponentDoorBinarySensor(data, "door_open", lock) - door_activity_start_timestamp = 1234 - door_activity = MockActivity( - action=ACTION_DOOR_OPEN, - activity_start_timestamp=door_activity_start_timestamp, - activity_end_timestamp=5678, - ) - door._sync_door_activity(door_activity) - assert door.last_update_door_state["door_state"] == LockDoorStatus.OPEN - assert door.last_update_door_state["activity_start_time_utc"] == dt.as_utc( - datetime.datetime.fromtimestamp(door_activity_start_timestamp) - ) - - -def test__sync_door_activity_doorclosed(): - """Test _sync_door_activity doorclosed.""" - data = MockAugustComponentData(last_door_state_update_timestamp=1) - lock = _mock_august_lock() - data.set_mocked_locks([lock]) - door = MockAugustComponentDoorBinarySensor(data, "door_open", lock) - door_activity_timestamp = 1234 - door_activity = MockActivity( - action=ACTION_DOOR_CLOSED, - activity_start_timestamp=door_activity_timestamp, - activity_end_timestamp=door_activity_timestamp, - ) - door._sync_door_activity(door_activity) - assert door.last_update_door_state["door_state"] == LockDoorStatus.CLOSED - assert door.last_update_door_state["activity_start_time_utc"] == dt.as_utc( - datetime.datetime.fromtimestamp(door_activity_timestamp) - ) - - -def test__sync_door_activity_ignores_old_data(): - """Test _sync_door_activity dooropen then expired doorclosed.""" - data = MockAugustComponentData(last_door_state_update_timestamp=1) - lock = _mock_august_lock() - data.set_mocked_locks([lock]) - door = MockAugustComponentDoorBinarySensor(data, "door_open", lock) - first_door_activity_timestamp = 1234 - door_activity = MockActivity( - action=ACTION_DOOR_OPEN, - activity_start_timestamp=first_door_activity_timestamp, - activity_end_timestamp=first_door_activity_timestamp, - ) - door._sync_door_activity(door_activity) - assert door.last_update_door_state["door_state"] == LockDoorStatus.OPEN - assert door.last_update_door_state["activity_start_time_utc"] == dt.as_utc( - datetime.datetime.fromtimestamp(first_door_activity_timestamp) - ) - - # Now we do the update with an older start time to - # make sure it ignored - data.set_last_door_state_update_time_utc( - lock.device_id, dt.as_utc(datetime.datetime.fromtimestamp(1000)) - ) - door_activity_timestamp = 2 - door_activity = MockActivity( - action=ACTION_DOOR_CLOSED, - activity_start_timestamp=door_activity_timestamp, - activity_end_timestamp=door_activity_timestamp, - ) - door._sync_door_activity(door_activity) - assert door.last_update_door_state["door_state"] == LockDoorStatus.OPEN - assert door.last_update_door_state["activity_start_time_utc"] == dt.as_utc( - datetime.datetime.fromtimestamp(first_door_activity_timestamp) - ) +"""The binary_sensor tests for the august platform.""" diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index 3a43a0a841a086..eb50e37561e007 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -128,7 +128,6 @@ def _create_august_data_with_lock_details(lock_details): 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=[]) diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index 8b0368618998a7..518cf22b5bad99 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -1,110 +1,46 @@ """The lock tests for the august platform.""" -import datetime - -from august.activity import ( - ACTION_LOCK_LOCK, - ACTION_LOCK_ONETOUCHLOCK, - ACTION_LOCK_UNLOCK, +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_UNLOCK, + STATE_LOCKED, + STATE_ON, + STATE_UNLOCKED, ) -from august.lock import LockStatus - -from homeassistant.util import dt from tests.components.august.mocks import ( - MockActivity, - MockAugustComponentData, - MockAugustComponentLock, - _mock_august_lock, + _create_august_with_devices, + _mock_lock_from_fixture, ) -def test__sync_lock_activity_locked_via_onetouchlock(): - """Test _sync_lock_activity locking.""" - lock = _mocked_august_component_lock() - lock_activity_start_timestamp = 1234 - lock_activity = MockActivity( - action=ACTION_LOCK_ONETOUCHLOCK, - activity_start_timestamp=lock_activity_start_timestamp, - activity_end_timestamp=5678, - ) - lock._sync_lock_activity(lock_activity) - assert lock.last_update_lock_status["lock_status"] == LockStatus.LOCKED - assert lock.last_update_lock_status["activity_start_time_utc"] == dt.as_utc( - datetime.datetime.fromtimestamp(lock_activity_start_timestamp) +async def test_one_lock_unlock_happy_path(hass): + """Test creation of a lock with doorsense and bridge.""" + lock_one = await _mock_lock_from_fixture( + hass, "get_lock.online_with_doorsense.json" ) + lock_details = [lock_one] + await _create_august_with_devices(hass, lock_details=lock_details) + lock_abc_name = hass.states.get("lock.abc_name") -def test__sync_lock_activity_locked_via_lock(): - """Test _sync_lock_activity locking.""" - lock = _mocked_august_component_lock() - lock_activity_start_timestamp = 1234 - lock_activity = MockActivity( - action=ACTION_LOCK_LOCK, - activity_start_timestamp=lock_activity_start_timestamp, - activity_end_timestamp=5678, - ) - lock._sync_lock_activity(lock_activity) - assert lock.last_update_lock_status["lock_status"] == LockStatus.LOCKED - assert lock.last_update_lock_status["activity_start_time_utc"] == dt.as_utc( - datetime.datetime.fromtimestamp(lock_activity_start_timestamp) - ) + assert lock_abc_name.state == STATE_LOCKED + assert lock_abc_name.attributes.get("battery_level") == 92 + assert lock_abc_name.attributes.get("friendly_name") == "ABC Name" -def test__sync_lock_activity_unlocked(): - """Test _sync_lock_activity unlocking.""" - lock = _mocked_august_component_lock() - lock_activity_timestamp = 1234 - lock_activity = MockActivity( - action=ACTION_LOCK_UNLOCK, - activity_start_timestamp=lock_activity_timestamp, - activity_end_timestamp=lock_activity_timestamp, - ) - lock._sync_lock_activity(lock_activity) - assert lock.last_update_lock_status["lock_status"] == LockStatus.UNLOCKED - assert lock.last_update_lock_status["activity_start_time_utc"] == dt.as_utc( - datetime.datetime.fromtimestamp(lock_activity_timestamp) + data = {} + data[ATTR_ENTITY_ID] = "lock.abc_name" + assert await hass.services.async_call( + LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True ) + lock_abc_name = hass.states.get("lock.abc_name") + assert lock_abc_name.state == STATE_UNLOCKED -def test__sync_lock_activity_ignores_old_data(): - """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) - first_lock_activity_timestamp = 1234 - lock_activity = MockActivity( - action=ACTION_LOCK_UNLOCK, - activity_start_timestamp=first_lock_activity_timestamp, - activity_end_timestamp=first_lock_activity_timestamp, - ) - lock._sync_lock_activity(lock_activity) - assert lock.last_update_lock_status["lock_status"] == LockStatus.UNLOCKED - assert lock.last_update_lock_status["activity_start_time_utc"] == dt.as_utc( - datetime.datetime.fromtimestamp(first_lock_activity_timestamp) - ) - - # Now we do the update with an older start time to - # make sure it ignored - data.set_last_lock_status_update_time_utc( - august_lock.device_id, dt.as_utc(datetime.datetime.fromtimestamp(1000)) - ) - lock_activity_timestamp = 2 - lock_activity = MockActivity( - action=ACTION_LOCK_LOCK, - activity_start_timestamp=lock_activity_timestamp, - activity_end_timestamp=lock_activity_timestamp, - ) - lock._sync_lock_activity(lock_activity) - assert lock.last_update_lock_status["lock_status"] == LockStatus.UNLOCKED - assert lock.last_update_lock_status["activity_start_time_utc"] == dt.as_utc( - datetime.datetime.fromtimestamp(first_lock_activity_timestamp) - ) - + assert lock_abc_name.attributes.get("battery_level") == 92 + assert lock_abc_name.attributes.get("friendly_name") == "ABC Name" -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) + binary_sensor_abc_name = hass.states.get("binary_sensor.abc_name_open") + assert binary_sensor_abc_name.state == STATE_ON diff --git a/tests/fixtures/august/get_lock.doorsense_init.json b/tests/fixtures/august/get_lock.doorsense_init.json new file mode 100644 index 00000000000000..be60bbe6236d1b --- /dev/null +++ b/tests/fixtures/august/get_lock.doorsense_init.json @@ -0,0 +1,103 @@ +{ + "LockName": "Front Door Lock", + "Type": 2, + "Created": "2017-12-10T03:12:09.210Z", + "Updated": "2017-12-10T03:12:09.210Z", + "LockID": "A6697750D607098BAE8D6BAA11EF8063", + "HouseID": "000000000000", + "HouseName": "My House", + "Calibrated": false, + "skuNumber": "AUG-SL02-M02-S02", + "timeZone": "America/Vancouver", + "battery": 0.88, + "SerialNumber": "X2FSW05DGA", + "LockStatus": { + "status": "locked", + "doorState": "init", + "dateTime": "2017-12-10T04:48:30.272Z", + "isLockStatusChanged": false, + "valid": true + }, + "currentFirmwareVersion": "109717e9-3.0.44-3.0.30", + "homeKitEnabled": false, + "zWaveEnabled": false, + "isGalileo": false, + "Bridge": { + "_id": "aaacab87f7efxa0015884999", + "mfgBridgeID": "AAGPP102XX", + "deviceModel": "august-doorbell", + "firmwareVersion": "2.3.0-RC153+201711151527", + "operative": true + }, + "keypad": { + "_id": "5bc65c24e6ef2a263e1450a8", + "serialNumber": "K1GXB0054Z", + "lockID": "92412D1B44004595B5DEB134E151A8D3", + "currentFirmwareVersion": "2.27.0", + "battery": {}, + "batteryLevel": "Medium", + "batteryRaw": 170 + }, + "OfflineKeys": { + "created": [], + "loaded": [ + { + "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", + "slot": 1, + "key": "kkk01d4300c1dcxxx1c330f794941111", + "created": "2017-12-10T03:12:09.215Z", + "loaded": "2017-12-10T03:12:54.391Z" + } + ], + "deleted": [], + "loadedhk": [ + { + "key": "kkk01d4300c1dcxxx1c330f794941222", + "slot": 256, + "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", + "created": "2017-12-10T03:12:09.218Z", + "loaded": "2017-12-10T03:12:55.563Z" + } + ] + }, + "parametersToSet": {}, + "users": { + "cccca94e-373e-aaaa-bbbb-333396827777": { + "UserType": "superuser", + "FirstName": "Foo", + "LastName": "Bar", + "identifiers": [ + "email:foo@bar.com", + "phone:+177777777777" + ], + "imageInfo": { + "original": { + "width": 948, + "height": 949, + "format": "jpg", + "url": "http://www.image.com/foo.jpeg", + "secure_url": "https://www.image.com/foo.jpeg" + }, + "thumbnail": { + "width": 128, + "height": 128, + "format": "jpg", + "url": "http://www.image.com/foo.jpeg", + "secure_url": "https://www.image.com/foo.jpeg" + } + } + } + }, + "pubsubChannel": "3333a674-ffff-aaaa-b351-b3a4473f3333", + "ruleHash": {}, + "cameras": [], + "geofenceLimits": { + "ios": { + "debounceInterval": 90, + "gpsAccuracyMultiplier": 2.5, + "maximumGeofence": 5000, + "minimumGeofence": 100, + "minGPSAccuracyRequired": 80 + } + } +} diff --git a/tests/fixtures/august/get_lock.offline.json b/tests/fixtures/august/get_lock.offline.json new file mode 100644 index 00000000000000..502a78674e999b --- /dev/null +++ b/tests/fixtures/august/get_lock.offline.json @@ -0,0 +1,68 @@ +{ + "Calibrated" : false, + "Created" : "2000-00-00T00:00:00.447Z", + "HouseID" : "houseid", + "HouseName" : "MockName", + "LockID" : "ABC", + "LockName" : "Test", + "LockStatus" : { + "status" : "unknown" + }, + "OfflineKeys" : { + "created" : [], + "createdhk" : [ + { + "UserID" : "mock-user-id", + "created" : "2000-00-00T00:00:00.447Z", + "key" : "mockkey", + "slot" : 12 + } + ], + "deleted" : [], + "loaded" : [ + { + "UserID" : "userid", + "created" : "2000-00-00T00:00:00.447Z", + "key" : "key", + "loaded" : "2000-00-00T00:00:00.447Z", + "slot" : 1 + } + ] + }, + "SerialNumber" : "ABC", + "Type" : 3, + "Updated" : "2000-00-00T00:00:00.447Z", + "battery" : -1, + "cameras" : [], + "currentFirmwareVersion" : "undefined-1.59.0-1.13.2", + "geofenceLimits" : { + "ios" : { + "debounceInterval" : 90, + "gpsAccuracyMultiplier" : 2.5, + "maximumGeofence" : 5000, + "minGPSAccuracyRequired" : 80, + "minimumGeofence" : 100 + } + }, + "homeKitEnabled" : false, + "isGalileo" : false, + "macAddress" : "a:b:c", + "parametersToSet" : {}, + "pubsubChannel" : "mockpubsub", + "ruleHash" : {}, + "skuNumber" : "AUG-X", + "supportsEntryCodes" : false, + "users" : { + "mockuserid" : { + "FirstName" : "MockName", + "LastName" : "House", + "UserType" : "superuser", + "identifiers" : [ + "phone:+15558675309", + "email:mockme@mock.org" + ] + } + }, + "zWaveDSK" : "1-2-3-4", + "zWaveEnabled" : true +} diff --git a/tests/fixtures/august/get_lock.online.json b/tests/fixtures/august/get_lock.online.json new file mode 100644 index 00000000000000..8003359e589c3e --- /dev/null +++ b/tests/fixtures/august/get_lock.online.json @@ -0,0 +1,103 @@ +{ + "LockName": "Front Door Lock", + "Type": 2, + "Created": "2017-12-10T03:12:09.210Z", + "Updated": "2017-12-10T03:12:09.210Z", + "LockID": "A6697750D607098BAE8D6BAA11EF8063", + "HouseID": "000000000000", + "HouseName": "My House", + "Calibrated": false, + "skuNumber": "AUG-SL02-M02-S02", + "timeZone": "America/Vancouver", + "battery": 0.88, + "SerialNumber": "X2FSW05DGA", + "LockStatus": { + "status": "locked", + "doorState": "closed", + "dateTime": "2017-12-10T04:48:30.272Z", + "isLockStatusChanged": true, + "valid": true + }, + "currentFirmwareVersion": "109717e9-3.0.44-3.0.30", + "homeKitEnabled": false, + "zWaveEnabled": false, + "isGalileo": false, + "Bridge": { + "_id": "aaacab87f7efxa0015884999", + "mfgBridgeID": "AAGPP102XX", + "deviceModel": "august-doorbell", + "firmwareVersion": "2.3.0-RC153+201711151527", + "operative": true + }, + "keypad": { + "_id": "5bc65c24e6ef2a263e1450a8", + "serialNumber": "K1GXB0054Z", + "lockID": "92412D1B44004595B5DEB134E151A8D3", + "currentFirmwareVersion": "2.27.0", + "battery": {}, + "batteryLevel": "Medium", + "batteryRaw": 170 + }, + "OfflineKeys": { + "created": [], + "loaded": [ + { + "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", + "slot": 1, + "key": "kkk01d4300c1dcxxx1c330f794941111", + "created": "2017-12-10T03:12:09.215Z", + "loaded": "2017-12-10T03:12:54.391Z" + } + ], + "deleted": [], + "loadedhk": [ + { + "key": "kkk01d4300c1dcxxx1c330f794941222", + "slot": 256, + "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", + "created": "2017-12-10T03:12:09.218Z", + "loaded": "2017-12-10T03:12:55.563Z" + } + ] + }, + "parametersToSet": {}, + "users": { + "cccca94e-373e-aaaa-bbbb-333396827777": { + "UserType": "superuser", + "FirstName": "Foo", + "LastName": "Bar", + "identifiers": [ + "email:foo@bar.com", + "phone:+177777777777" + ], + "imageInfo": { + "original": { + "width": 948, + "height": 949, + "format": "jpg", + "url": "http://www.image.com/foo.jpeg", + "secure_url": "https://www.image.com/foo.jpeg" + }, + "thumbnail": { + "width": 128, + "height": 128, + "format": "jpg", + "url": "http://www.image.com/foo.jpeg", + "secure_url": "https://www.image.com/foo.jpeg" + } + } + } + }, + "pubsubChannel": "3333a674-ffff-aaaa-b351-b3a4473f3333", + "ruleHash": {}, + "cameras": [], + "geofenceLimits": { + "ios": { + "debounceInterval": 90, + "gpsAccuracyMultiplier": 2.5, + "maximumGeofence": 5000, + "minimumGeofence": 100, + "minGPSAccuracyRequired": 80 + } + } +} diff --git a/tests/fixtures/august/get_lock.online_with_doorsense.json b/tests/fixtures/august/get_lock.online_with_doorsense.json new file mode 100644 index 00000000000000..b0f9475c0094aa --- /dev/null +++ b/tests/fixtures/august/get_lock.online_with_doorsense.json @@ -0,0 +1,51 @@ +{ + "Bridge" : { + "_id" : "bridgeid", + "deviceModel" : "august-connect", + "firmwareVersion" : "2.2.1", + "hyperBridge" : true, + "mfgBridgeID" : "C5WY200WSH", + "operative" : true, + "status" : { + "current" : "online", + "lastOffline" : "2000-00-00T00:00:00.447Z", + "lastOnline" : "2000-00-00T00:00:00.447Z", + "updated" : "2000-00-00T00:00:00.447Z" + } + }, + "Calibrated" : false, + "Created" : "2000-00-00T00:00:00.447Z", + "HouseID" : "123", + "HouseName" : "Test", + "LockID" : "ABC", + "LockName" : "Online door with doorsense", + "LockStatus" : { + "dateTime" : "2017-12-10T04:48:30.272Z", + "doorState" : "open", + "isLockStatusChanged" : false, + "status" : "locked", + "valid" : true + }, + "SerialNumber" : "XY", + "Type" : 1001, + "Updated" : "2000-00-00T00:00:00.447Z", + "battery" : 0.922, + "currentFirmwareVersion" : "undefined-4.3.0-1.8.14", + "homeKitEnabled" : true, + "hostLockInfo" : { + "manufacturer" : "yale", + "productID" : 1536, + "productTypeID" : 32770, + "serialNumber" : "ABC" + }, + "isGalileo" : false, + "macAddress" : "12:22", + "pins" : { + "created" : [], + "loaded" : [] + }, + "skuNumber" : "AUG-MD01", + "supportsEntryCodes" : true, + "timeZone" : "Pacific/Hawaii", + "zWaveEnabled" : false +} diff --git a/tests/fixtures/august/get_locks.json b/tests/fixtures/august/get_locks.json new file mode 100644 index 00000000000000..3fab55f82c9602 --- /dev/null +++ b/tests/fixtures/august/get_locks.json @@ -0,0 +1,16 @@ +{ + "A6697750D607098BAE8D6BAA11EF8063": { + "LockName": "Front Door Lock", + "UserType": "superuser", + "macAddress": "2E:BA:C4:14:3F:09", + "HouseID": "000000000000", + "HouseName": "A House" + }, + "A6697750D607098BAE8D6BAA11EF9999": { + "LockName": "Back Door Lock", + "UserType": "user", + "macAddress": "2E:BA:C4:14:3F:88", + "HouseID": "000000000011", + "HouseName": "A House" + } +} diff --git a/tests/fixtures/august/lock_open.json b/tests/fixtures/august/lock_open.json new file mode 100644 index 00000000000000..67e3ccfbf159b4 --- /dev/null +++ b/tests/fixtures/august/lock_open.json @@ -0,0 +1,26 @@ +{ + "status" : "kAugLockState_Locked", + "resultsFromOperationCache" : false, + "retryCount" : 1, + "info" : { + "wlanRSSI" : -54, + "lockType" : "lock_version_1001", + "lockStatusChanged" : false, + "serialNumber" : "ABC", + "serial" : "123", + "action" : "lock", + "context" : { + "startDate" : "2020-02-19T01:59:39.516Z", + "retryCount" : 1, + "transactionID" : "mock" + }, + "bridgeID" : "mock", + "wlanSNR" : 41, + "startTime" : "2020-02-19T01:59:39.517Z", + "duration" : 5149, + "lockID" : "ABC", + "rssi" : -77 + }, + "totalTime" : 5162, + "doorState" : "kAugDoorState_Open" +} diff --git a/tests/fixtures/august/unlock_closed.json b/tests/fixtures/august/unlock_closed.json new file mode 100644 index 00000000000000..57b712f55e170a --- /dev/null +++ b/tests/fixtures/august/unlock_closed.json @@ -0,0 +1,26 @@ +{ + "status" : "kAugLockState_Unlocked", + "resultsFromOperationCache" : false, + "retryCount" : 1, + "info" : { + "wlanRSSI" : -54, + "lockType" : "lock_version_1001", + "lockStatusChanged" : false, + "serialNumber" : "ABC", + "serial" : "123", + "action" : "lock", + "context" : { + "startDate" : "2020-02-19T01:59:39.516Z", + "retryCount" : 1, + "transactionID" : "mock" + }, + "bridgeID" : "mock", + "wlanSNR" : 41, + "startTime" : "2020-02-19T01:59:39.517Z", + "duration" : 5149, + "lockID" : "ABC", + "rssi" : -77 + }, + "totalTime" : 5162, + "doorState" : "kAugDoorState_Closed" +}