Skip to content

Commit

Permalink
Provide user consumable errors when lock operations fail (#31864)
Browse files Browse the repository at this point in the history
* Provide user consumable errors when lock operations fail

This resolves issue #26672

* include from in raise

* pylint

* Cleanup of mocking.
  • Loading branch information
bdraco committed Feb 17, 2020
1 parent 18dfb02 commit 00ac7a7
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 93 deletions.
38 changes: 35 additions & 3 deletions homeassistant/components/august/__init__.py
Expand Up @@ -4,7 +4,7 @@
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
Expand All @@ -15,6 +15,7 @@
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
Expand Down Expand Up @@ -364,6 +365,12 @@ async def async_get_lock_detail(self, lock_id):
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.
Expand Down Expand Up @@ -472,8 +479,33 @@ def _update_locks_detail(self):

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
2 changes: 1 addition & 1 deletion homeassistant/components/august/binary_sensor.py
Expand Up @@ -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 = {
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/august/manifest.json
Expand Up @@ -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"]
}
2 changes: 1 addition & 1 deletion requirements_all.txt
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Expand Up @@ -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
Expand Down
86 changes: 78 additions & 8 deletions tests/components/august/mocks.py
Expand Up @@ -3,11 +3,32 @@
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."""

Expand Down Expand Up @@ -35,14 +56,48 @@ def action(self):
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(
Expand All @@ -51,6 +106,20 @@ def __init__(
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."""
Expand All @@ -69,12 +138,6 @@ def set_last_door_state_update_time_utc(self, device_id, update_time):
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(
Expand All @@ -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"},
)
54 changes: 15 additions & 39 deletions tests/components/august/test_binary_sensor.py
@@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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(
Expand Down
44 changes: 43 additions & 1 deletion tests/components/august/test_init.py
Expand Up @@ -3,15 +3,57 @@
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()
Expand Down

0 comments on commit 00ac7a7

Please sign in to comment.