Provide user consumable errors when lock operations fail (#31864)

* Provide user consumable errors when lock operations fail

This resolves issue #26672

* include from in raise

* pylint

* Cleanup of mocking.
This commit is contained in:
J. Nick Koston 2020-02-17 12:30:14 -06:00 committed by GitHub
parent 18dfb02355
commit 00ac7a7d70
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 194 additions and 93 deletions

View file

@ -4,7 +4,7 @@ from datetime import timedelta
from functools import partial
import logging
from august.api import Api
from august.api import Api, AugustApiHTTPError
from august.authenticator import AuthenticationState, Authenticator, ValidationResult
from requests import RequestException, Session
import voluptuous as vol
@ -15,6 +15,7 @@ from homeassistant.const import (
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle, dt
@ -364,6 +365,12 @@ class AugustData:
await self._async_update_locks()
return self._lock_detail_by_id.get(lock_id)
def get_lock_name(self, device_id):
"""Return lock name as August has it stored."""
for lock in self._locks:
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.
@ -472,8 +479,33 @@ class AugustData:
def lock(self, device_id):
"""Lock the device."""
return self._api.lock(self._access_token, device_id)
return _call_api_operation_that_requires_bridge(
self.get_lock_name(device_id),
"lock",
self._api.lock,
self._access_token,
device_id,
)
def unlock(self, device_id):
"""Unlock the device."""
return self._api.unlock(self._access_token, device_id)
return _call_api_operation_that_requires_bridge(
self.get_lock_name(device_id),
"unlock",
self._api.unlock,
self._access_token,
device_id,
)
def _call_api_operation_that_requires_bridge(
device_name, operation_name, func, *args, **kwargs
):
"""Call an API that requires the bridge to be online."""
ret = None
try:
ret = func(*args, **kwargs)
except AugustApiHTTPError as err:
raise HomeAssistantError(device_name + ": " + str(err))
return ret

View file

@ -56,7 +56,7 @@ async def _async_activity_time_based_state(data, doorbell, activity_types):
return None
# Sensor types: Name, device_class, async_state_provider
# sensor_type: [name, device_class, async_state_provider]
SENSOR_TYPES_DOOR = {"door_open": ["Open", "door", _async_retrieve_door_state]}
SENSOR_TYPES_DOORBELL = {

View file

@ -2,7 +2,7 @@
"domain": "august",
"name": "August",
"documentation": "https://www.home-assistant.io/integrations/august",
"requirements": ["py-august==0.12.0"],
"requirements": ["py-august==0.14.0"],
"dependencies": ["configurator"],
"codeowners": ["@bdraco"]
}

View file

@ -1075,7 +1075,7 @@ pushover_complete==1.1.1
pwmled==1.4.1
# homeassistant.components.august
py-august==0.12.0
py-august==0.14.0
# homeassistant.components.canary
py-canary==0.5.0

View file

@ -391,7 +391,7 @@ pure-python-adb==0.2.2.dev0
pushbullet.py==0.11.0
# homeassistant.components.august
py-august==0.12.0
py-august==0.14.0
# homeassistant.components.canary
py-canary==0.5.0

View file

@ -3,11 +3,32 @@ import datetime
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 homeassistant.components.august import AugustData
from homeassistant.components.august.binary_sensor import AugustDoorBinarySensor
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):
"""A mock for py-august Api class that always has an AugustApiHTTPError."""
def _call_api(self, *args, **kwargs):
"""Mock the time activity started."""
raise AugustApiHTTPError("This should bubble up as its user consumable")
class MockActivity(Activity):
"""A mock for py-august Activity class."""
@ -35,14 +56,48 @@ class MockActivity(Activity):
return self._action
class MockAugustData(AugustData):
class MockAugustComponentDoorBinarySensor(AugustDoorBinarySensor):
"""A mock for august component AugustDoorBinarySensor class."""
def _update_door_state(self, door_state, activity_start_time_utc):
"""Mock updating the lock status."""
self._data.set_last_door_state_update_time_utc(
self._door.device_id, activity_start_time_utc
)
self.last_update_door_state = {}
self.last_update_door_state["door_state"] = door_state
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."""
# AugustData support multiple locks, however for the purposes of
# mocking we currently only mock one lockid
def __init__(
self, last_lock_status_update_timestamp=1, last_door_state_update_timestamp=1
self,
last_lock_status_update_timestamp=1,
last_door_state_update_timestamp=1,
api=MockAugustApi(),
access_token="mocked_access_token",
locks=[],
doorbells=[],
):
"""Mock AugustData."""
self._last_lock_status_update_time_utc = dt.as_utc(
@ -51,6 +106,20 @@ class MockAugustData(AugustData):
self._last_door_state_update_time_utc = dt.as_utc(
datetime.datetime.fromtimestamp(last_lock_status_update_timestamp)
)
self._api = api
self._access_token = access_token
self._locks = locks
self._doorbells = doorbells
self._lock_status_by_id = {}
self._lock_last_status_update_time_utc_by_id = {}
def set_mocked_locks(self, locks):
"""Set lock mocks."""
self._locks = locks
def set_mocked_doorbells(self, doorbells):
"""Set doorbell mocks."""
self._doorbells = doorbells
def get_last_lock_status_update_time_utc(self, device_id):
"""Mock to get last lock status update time."""
@ -69,12 +138,6 @@ class MockAugustData(AugustData):
self._last_door_state_update_time_utc = update_time
def _mock_august_lock():
lock = MagicMock(name="august.lock")
type(lock).device_id = PropertyMock(return_value="lock_device_id_1")
return lock
def _mock_august_authenticator():
authenticator = MagicMock(name="august.authenticator")
authenticator.should_refresh = MagicMock(
@ -93,3 +156,10 @@ def _mock_august_authentication(token_text, token_timestamp):
return_value=token_timestamp
)
return authentication
def _mock_august_lock():
return Lock(
"mockdeviceid1",
{"LockName": "Mocked Lock 1", "HouseID": "mockhouseid1", "UserType": "owner"},
)

View file

@ -1,54 +1,26 @@
"""The lock tests for the august platform."""
import datetime
from unittest.mock import MagicMock
from august.activity import ACTION_DOOR_CLOSED, ACTION_DOOR_OPEN
from august.lock import LockDoorStatus
from homeassistant.components.august.binary_sensor import AugustDoorBinarySensor
from homeassistant.util import dt
from tests.components.august.mocks import (
MockActivity,
MockAugustData,
MockAugustComponentData,
MockAugustComponentDoorBinarySensor,
_mock_august_lock,
)
class MockAugustDoorBinarySensor(AugustDoorBinarySensor):
"""A mock for august component AugustLock class."""
def __init__(self, august_data=None):
"""Init the mock for august component AugustLock class."""
self._data = august_data
self._door = _mock_august_lock()
@property
def name(self):
"""Mock name."""
return "mockedname1"
@property
def device_id(self):
"""Mock device_id."""
return "mockdeviceid1"
def _update_door_state(self, door_state, activity_start_time_utc):
"""Mock updating the lock status."""
self._data.set_last_door_state_update_time_utc(
self._door.device_id, activity_start_time_utc
)
self.last_update_door_state = {}
self.last_update_door_state["door_state"] = door_state
self.last_update_door_state["activity_start_time_utc"] = activity_start_time_utc
return MagicMock()
def test__sync_door_activity_doored_via_dooropen():
"""Test _sync_door_activity dooropen."""
data = MockAugustData(last_door_state_update_timestamp=1)
door = MockAugustDoorBinarySensor(august_data=data)
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,
@ -64,8 +36,10 @@ def test__sync_door_activity_doored_via_dooropen():
def test__sync_door_activity_doorclosed():
"""Test _sync_door_activity doorclosed."""
data = MockAugustData(last_door_state_update_timestamp=1)
door = MockAugustDoorBinarySensor(august_data=data)
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,
@ -81,8 +55,10 @@ def test__sync_door_activity_doorclosed():
def test__sync_door_activity_ignores_old_data():
"""Test _sync_door_activity dooropen then expired doorclosed."""
data = MockAugustData(last_door_state_update_timestamp=1)
door = MockAugustDoorBinarySensor(august_data=data)
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,
@ -98,7 +74,7 @@ def test__sync_door_activity_ignores_old_data():
# Now we do the update with an older start time to
# make sure it ignored
data.set_last_door_state_update_time_utc(
door.device_id, dt.as_utc(datetime.datetime.fromtimestamp(1000))
lock.device_id, dt.as_utc(datetime.datetime.fromtimestamp(1000))
)
door_activity_timestamp = 2
door_activity = MockActivity(

View file

@ -3,15 +3,57 @@ import asyncio
from unittest.mock import MagicMock
from homeassistant.components import august
from homeassistant.exceptions import HomeAssistantError
from tests.components.august.mocks import (
MockAugustApiFailing,
MockAugustComponentData,
_mock_august_authentication,
_mock_august_authenticator,
_mock_august_lock,
)
def test_get_lock_name():
"""Get the lock name from August data."""
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"
def test_unlock_throws_august_api_http_error():
"""Test unlock."""
data = MockAugustComponentData(api=MockAugustApiFailing())
lock = _mock_august_lock()
data.set_mocked_locks([lock])
last_err = None
try:
data.unlock("mockdeviceid1")
except HomeAssistantError as err:
last_err = err
assert (
str(last_err) == "Mocked Lock 1: This should bubble up as its user consumable"
)
def test_lock_throws_august_api_http_error():
"""Test lock."""
data = MockAugustComponentData(api=MockAugustApiFailing())
lock = _mock_august_lock()
data.set_mocked_locks([lock])
last_err = None
try:
data.unlock("mockdeviceid1")
except HomeAssistantError as err:
last_err = err
assert (
str(last_err) == "Mocked Lock 1: This should bubble up as its user consumable"
)
async def test__refresh_access_token(hass):
"""Set up things to be run when tests are started."""
"""Test refresh of the access token."""
authentication = _mock_august_authentication("original_token", 1234)
authenticator = _mock_august_authenticator()
token_refresh_lock = asyncio.Lock()

View file

@ -1,7 +1,6 @@
"""The lock tests for the august platform."""
import datetime
from unittest.mock import MagicMock
from august.activity import (
ACTION_LOCK_LOCK,
@ -10,46 +9,22 @@ from august.activity import (
)
from august.lock import LockStatus
from homeassistant.components.august.lock import AugustLock
from homeassistant.util import dt
from tests.components.august.mocks import (
MockActivity,
MockAugustData,
MockAugustComponentData,
MockAugustComponentLock,
_mock_august_lock,
)
class MockAugustLock(AugustLock):
"""A mock for august component AugustLock class."""
def __init__(self, august_data=None):
"""Init the mock for august component AugustLock class."""
self._data = august_data
self._lock = _mock_august_lock()
@property
def device_id(self):
"""Mock device_id."""
return "mockdeviceid1"
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
return MagicMock()
def test__sync_lock_activity_locked_via_onetouchlock():
"""Test _sync_lock_activity locking."""
data = MockAugustData(last_lock_status_update_timestamp=1)
lock = MockAugustLock(august_data=data)
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_activity_start_timestamp = 1234
lock_activity = MockActivity(
action=ACTION_LOCK_ONETOUCHLOCK,
@ -65,8 +40,10 @@ def test__sync_lock_activity_locked_via_onetouchlock():
def test__sync_lock_activity_locked_via_lock():
"""Test _sync_lock_activity locking."""
data = MockAugustData(last_lock_status_update_timestamp=1)
lock = MockAugustLock(august_data=data)
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_activity_start_timestamp = 1234
lock_activity = MockActivity(
action=ACTION_LOCK_LOCK,
@ -82,8 +59,10 @@ def test__sync_lock_activity_locked_via_lock():
def test__sync_lock_activity_unlocked():
"""Test _sync_lock_activity unlocking."""
data = MockAugustData(last_lock_status_update_timestamp=1)
lock = MockAugustLock(august_data=data)
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_activity_timestamp = 1234
lock_activity = MockActivity(
action=ACTION_LOCK_UNLOCK,
@ -99,8 +78,10 @@ def test__sync_lock_activity_unlocked():
def test__sync_lock_activity_ignores_old_data():
"""Test _sync_lock_activity unlocking."""
data = MockAugustData(last_lock_status_update_timestamp=1)
lock = MockAugustLock(august_data=data)
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,
@ -116,7 +97,7 @@ def test__sync_lock_activity_ignores_old_data():
# Now we do the update with an older start time to
# make sure it ignored
data.set_last_lock_status_update_time_utc(
lock.device_id, dt.as_utc(datetime.datetime.fromtimestamp(1000))
august_lock.device_id, dt.as_utc(datetime.datetime.fromtimestamp(1000))
)
lock_activity_timestamp = 2
lock_activity = MockActivity(