Cleanup August activity processing and add tests (#31774)

* Update py-august to 0.12.0

* Upstream update also resolves issue #28960
This commit is contained in:
J. Nick Koston 2020-02-12 17:35:07 -06:00 committed by GitHub
parent 834acd85d3
commit 6879105b14
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 378 additions and 52 deletions

View file

@ -2,7 +2,7 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
from august.activity import ActivityType from august.activity import ACTIVITY_ACTION_STATES, ActivityType
from august.lock import LockDoorStatus from august.lock import LockDoorStatus
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
@ -138,12 +138,12 @@ class AugustDoorBinarySensor(BinarySensorDevice):
self._state = self._state == LockDoorStatus.OPEN self._state = self._state == LockDoorStatus.OPEN
activity = self._data.get_latest_device_activity( door_activity = self._data.get_latest_device_activity(
self._door.device_id, ActivityType.DOOR_OPERATION self._door.device_id, ActivityType.DOOR_OPERATION
) )
if activity is not None: if door_activity is not None:
self._sync_door_activity(activity) self._sync_door_activity(door_activity)
def _update_door_state(self, door_state, update_start_time): def _update_door_state(self, door_state, update_start_time):
new_state = door_state == LockDoorStatus.OPEN new_state = door_state == LockDoorStatus.OPEN
@ -153,7 +153,7 @@ class AugustDoorBinarySensor(BinarySensorDevice):
self._door.device_id, door_state, update_start_time self._door.device_id, door_state, update_start_time
) )
def _sync_door_activity(self, activity): def _sync_door_activity(self, door_activity):
"""Check the activity for the latest door open/close activity (events). """Check the activity for the latest door open/close activity (events).
We use this to determine the door state in between calls to the lock We use this to determine the door state in between calls to the lock
@ -162,25 +162,26 @@ class AugustDoorBinarySensor(BinarySensorDevice):
last_door_state_update_time_utc = self._data.get_last_door_state_update_time_utc( last_door_state_update_time_utc = self._data.get_last_door_state_update_time_utc(
self._door.device_id self._door.device_id
) )
activity_end_time_utc = dt.as_utc(activity.activity_end_time) activity_end_time_utc = dt.as_utc(door_activity.activity_end_time)
if activity_end_time_utc > last_door_state_update_time_utc: if activity_end_time_utc > last_door_state_update_time_utc:
_LOGGER.debug( _LOGGER.debug(
"The activity log has new events for %s: [action=%s] [activity_end_time_utc=%s] > [last_door_state_update_time_utc=%s]", "The activity log has new events for %s: [action=%s] [activity_end_time_utc=%s] > [last_door_state_update_time_utc=%s]",
self.name, self.name,
activity.action, door_activity.action,
activity_end_time_utc, activity_end_time_utc,
last_door_state_update_time_utc, last_door_state_update_time_utc,
) )
activity_start_time_utc = dt.as_utc(activity.activity_start_time) activity_start_time_utc = dt.as_utc(door_activity.activity_start_time)
if activity.action == "doorclosed": if door_activity.action in ACTIVITY_ACTION_STATES:
self._update_door_state(LockDoorStatus.CLOSED, activity_start_time_utc) self._update_door_state(
elif activity.action == "dooropen": ACTIVITY_ACTION_STATES[door_activity.action],
self._update_door_state(LockDoorStatus.OPEN, activity_start_time_utc) activity_start_time_utc,
)
else: else:
_LOGGER.info( _LOGGER.info(
"Unhandled door activity action %s for %s", "Unhandled door activity action %s for %s",
activity.action, door_activity.action,
self.name, self.name,
) )

View file

@ -22,10 +22,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class AugustCamera(Camera): class AugustCamera(Camera):
"""An implementation of a Canary security camera.""" """An implementation of a August security camera."""
def __init__(self, data, doorbell, timeout): def __init__(self, data, doorbell, timeout):
"""Initialize a Canary security camera.""" """Initialize a August security camera."""
super().__init__() super().__init__()
self._data = data self._data = data
self._doorbell = doorbell self._doorbell = doorbell

View file

@ -2,7 +2,7 @@
from datetime import timedelta from datetime import timedelta
import logging import logging
from august.activity import ActivityType from august.activity import ACTIVITY_ACTION_STATES, ActivityType
from august.lock import LockStatus from august.lock import LockStatus
from homeassistant.components.lock import LockDevice from homeassistant.components.lock import LockDevice
@ -67,15 +67,15 @@ class AugustLock(LockDevice):
self._lock_detail = self._data.get_lock_detail(self._lock.device_id) self._lock_detail = self._data.get_lock_detail(self._lock.device_id)
activity = self._data.get_latest_device_activity( lock_activity = self._data.get_latest_device_activity(
self._lock.device_id, ActivityType.LOCK_OPERATION self._lock.device_id, ActivityType.LOCK_OPERATION
) )
if activity is not None: if lock_activity is not None:
self._changed_by = activity.operated_by self._changed_by = lock_activity.operated_by
self._sync_lock_activity(activity) self._sync_lock_activity(lock_activity)
def _sync_lock_activity(self, activity): def _sync_lock_activity(self, lock_activity):
"""Check the activity for the latest lock/unlock activity (events). """Check the activity for the latest lock/unlock activity (events).
We use this to determine the lock state in between calls to the lock We use this to determine the lock state in between calls to the lock
@ -84,25 +84,26 @@ class AugustLock(LockDevice):
last_lock_status_update_time_utc = self._data.get_last_lock_status_update_time_utc( last_lock_status_update_time_utc = self._data.get_last_lock_status_update_time_utc(
self._lock.device_id self._lock.device_id
) )
activity_end_time_utc = dt.as_utc(activity.activity_end_time) activity_end_time_utc = dt.as_utc(lock_activity.activity_end_time)
if activity_end_time_utc > last_lock_status_update_time_utc: if activity_end_time_utc > last_lock_status_update_time_utc:
_LOGGER.debug( _LOGGER.debug(
"The activity log has new events for %s: [action=%s] [activity_end_time_utc=%s] > [last_lock_status_update_time_utc=%s]", "The activity log has new events for %s: [action=%s] [activity_end_time_utc=%s] > [last_lock_status_update_time_utc=%s]",
self.name, self.name,
activity.action, lock_activity.action,
activity_end_time_utc, activity_end_time_utc,
last_lock_status_update_time_utc, last_lock_status_update_time_utc,
) )
activity_start_time_utc = dt.as_utc(activity.activity_start_time) activity_start_time_utc = dt.as_utc(lock_activity.activity_start_time)
if activity.action == "lock" or activity.action == "onetouchlock": if lock_activity.action in ACTIVITY_ACTION_STATES:
self._update_lock_status(LockStatus.LOCKED, activity_start_time_utc) self._update_lock_status(
elif activity.action == "unlock": ACTIVITY_ACTION_STATES[lock_activity.action],
self._update_lock_status(LockStatus.UNLOCKED, activity_start_time_utc) activity_start_time_utc,
)
else: else:
_LOGGER.info( _LOGGER.info(
"Unhandled lock activity action %s for %s", "Unhandled lock activity action %s for %s",
activity.action, lock_activity.action,
self.name, self.name,
) )

View file

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

View file

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

View file

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

View file

@ -0,0 +1,95 @@
"""Mocks for the august component."""
import datetime
from unittest.mock import MagicMock, PropertyMock
from august.activity import Activity
from homeassistant.components.august import AugustData
from homeassistant.util import dt
class MockActivity(Activity):
"""A mock for py-august Activity class."""
def __init__(
self, action=None, activity_start_timestamp=None, activity_end_timestamp=None
):
"""Init the py-august Activity class mock."""
self._action = action
self._activity_start_timestamp = activity_start_timestamp
self._activity_end_timestamp = activity_end_timestamp
@property
def activity_start_time(self):
"""Mock the time activity started."""
return datetime.datetime.fromtimestamp(self._activity_start_timestamp)
@property
def activity_end_time(self):
"""Mock the time activity ended."""
return datetime.datetime.fromtimestamp(self._activity_end_timestamp)
@property
def action(self):
"""Mock the action."""
return self._action
class MockAugustData(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
):
"""Mock AugustData."""
self._last_lock_status_update_time_utc = dt.as_utc(
datetime.datetime.fromtimestamp(last_lock_status_update_timestamp)
)
self._last_door_state_update_time_utc = dt.as_utc(
datetime.datetime.fromtimestamp(last_lock_status_update_timestamp)
)
def get_last_lock_status_update_time_utc(self, device_id):
"""Mock to get last lock status update time."""
return self._last_lock_status_update_time_utc
def set_last_lock_status_update_time_utc(self, device_id, update_time):
"""Mock to set last lock status update time."""
self._last_lock_status_update_time_utc = update_time
def get_last_door_state_update_time_utc(self, device_id):
"""Mock to get last door state update time."""
return self._last_door_state_update_time_utc
def set_last_door_state_update_time_utc(self, device_id, update_time):
"""Mock to set last door state 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(
name="august.authenticator.should_refresh", return_value=0
)
authenticator.refresh_access_token = MagicMock(
name="august.authenticator.refresh_access_token"
)
return authenticator
def _mock_august_authentication(token_text, token_timestamp):
authentication = MagicMock(name="august.authentication")
type(authentication).access_token = PropertyMock(return_value=token_text)
type(authentication).access_token_expires = PropertyMock(
return_value=token_timestamp
)
return authentication

View file

@ -0,0 +1,113 @@
"""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,
_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)
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 = MockAugustData(last_door_state_update_timestamp=1)
door = MockAugustDoorBinarySensor(august_data=data)
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 = MockAugustData(last_door_state_update_timestamp=1)
door = MockAugustDoorBinarySensor(august_data=data)
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(
door.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)
)

View file

@ -1,27 +1,12 @@
"""The tests for the august platform.""" """The tests for the august platform."""
from unittest.mock import MagicMock, PropertyMock from unittest.mock import MagicMock
from homeassistant.components import august from homeassistant.components import august
from tests.components.august.mocks import (
def _mock_august_authenticator(): _mock_august_authentication,
authenticator = MagicMock(name="august.authenticator") _mock_august_authenticator,
authenticator.should_refresh = MagicMock( )
name="august.authenticator.should_refresh", return_value=0
)
authenticator.refresh_access_token = MagicMock(
name="august.authenticator.refresh_access_token"
)
return authenticator
def _mock_august_authentication(token_text, token_timestamp):
authentication = MagicMock(name="august.authentication")
type(authentication).access_token = PropertyMock(return_value=token_text)
type(authentication).access_token_expires = PropertyMock(
return_value=token_timestamp
)
return authentication
def test__refresh_access_token(): def test__refresh_access_token():

View file

@ -0,0 +1,131 @@
"""The lock tests for the august platform."""
import datetime
from unittest.mock import MagicMock
from august.activity import (
ACTION_LOCK_LOCK,
ACTION_LOCK_ONETOUCHLOCK,
ACTION_LOCK_UNLOCK,
)
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,
_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)
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)
)
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)
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)
)
def test__sync_lock_activity_unlocked():
"""Test _sync_lock_activity unlocking."""
data = MockAugustData(last_lock_status_update_timestamp=1)
lock = MockAugustLock(august_data=data)
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)
)
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)
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(
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)
)