Add august open action (#113795)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Maximilian Hildebrand 2024-05-29 08:35:53 +02:00 committed by GitHub
parent ae6c394b53
commit 05d0174e07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 220 additions and 1 deletions

View File

@ -381,6 +381,25 @@ class AugustData(AugustSubscriberMixin):
hyper_bridge,
)
async def async_unlatch(self, device_id: str) -> list[ActivityTypes]:
"""Open/unlatch the device."""
return await self._async_call_api_op_requires_bridge(
device_id,
self._api.async_unlatch_return_activities,
self._august_gateway.access_token,
device_id,
)
async def async_unlatch_async(self, device_id: str, hyper_bridge: bool) -> str:
"""Open/unlatch the device but do not wait for a response since it will come via pubnub."""
return await self._async_call_api_op_requires_bridge(
device_id,
self._api.async_unlatch_async,
self._august_gateway.access_token,
device_id,
hyper_bridge,
)
async def async_unlock(self, device_id: str) -> list[ActivityTypes]:
"""Unlock the device."""
return await self._async_call_api_op_requires_bridge(

View File

@ -11,7 +11,7 @@ from yalexs.activity import SOURCE_PUBNUB, ActivityType, ActivityTypes
from yalexs.lock import Lock, LockStatus
from yalexs.util import get_latest_activity, update_lock_detail_from_activity
from homeassistant.components.lock import ATTR_CHANGED_BY, LockEntity
from homeassistant.components.lock import ATTR_CHANGED_BY, LockEntity, LockEntityFeature
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -46,6 +46,8 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity):
super().__init__(data, device)
self._lock_status = None
self._attr_unique_id = f"{self._device_id:s}_lock"
if self._detail.unlatch_supported:
self._attr_supported_features = LockEntityFeature.OPEN
self._update_from_data()
async def async_lock(self, **kwargs: Any) -> None:
@ -56,6 +58,14 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity):
return
await self._call_lock_operation(self._data.async_lock)
async def async_open(self, **kwargs: Any) -> None:
"""Open/unlatch the device."""
assert self._data.activity_stream is not None
if self._data.activity_stream.pubnub.connected:
await self._data.async_unlatch_async(self._device_id, self._hyper_bridge)
return
await self._call_lock_operation(self._data.async_unlatch)
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the device."""
assert self._data.activity_stream is not None

View File

@ -0,0 +1,94 @@
{
"LockName": "Lock online with unlatch supported",
"Type": 17,
"Created": "2024-03-14T18:03:09.003Z",
"Updated": "2024-03-14T18:03:09.003Z",
"LockID": "online_with_unlatch",
"HouseID": "mockhouseid1",
"HouseName": "Zuhause",
"Calibrated": false,
"timeZone": "Europe/Berlin",
"battery": 0.61,
"batteryInfo": {
"level": 0.61,
"warningState": "lock_state_battery_warning_none",
"infoUpdatedDate": "2024-04-30T17:55:09.045Z",
"lastChangeDate": "2024-03-15T07:04:00.000Z",
"lastChangeVoltage": 8350,
"state": "Mittel",
"icon": "https://app-resources.aaecosystem.com/images/lock_battery_state_medium.png"
},
"hostHardwareID": "xxx",
"supportsEntryCodes": true,
"remoteOperateSecret": "xxxx",
"skuNumber": "NONE",
"macAddress": "DE:AD:BE:00:00:00",
"SerialNumber": "LPOC000000",
"LockStatus": {
"status": "locked",
"dateTime": "2024-04-30T18:41:25.673Z",
"isLockStatusChanged": false,
"valid": true,
"doorState": "init"
},
"currentFirmwareVersion": "1.0.4",
"homeKitEnabled": false,
"zWaveEnabled": false,
"isGalileo": false,
"Bridge": {
"_id": "65f33445529187c78a100000",
"mfgBridgeID": "LPOCH0004Y",
"deviceModel": "august-lock",
"firmwareVersion": "1.0.4",
"operative": true,
"status": {
"current": "online",
"lastOnline": "2024-04-30T18:41:27.971Z",
"updated": "2024-04-30T18:41:27.971Z",
"lastOffline": "2024-04-25T14:41:40.118Z"
},
"locks": [
{
"_id": "656858c182e6c7c555faf758",
"LockID": "68895DD075A1444FAD4C00B273EEEF28",
"macAddress": "DE:AD:BE:EF:0B:BC"
}
],
"hyperBridge": true
},
"OfflineKeys": {
"created": [],
"loaded": [
{
"created": "2024-03-14T18:03:09.034Z",
"key": "055281d4aa9bd7b68c7b7bb78e2f34ca",
"slot": 1,
"UserID": "b4b44424-0000-0000-0000-25c224dad337",
"loaded": "2024-03-14T18:03:33.470Z"
}
],
"deleted": []
},
"parametersToSet": {},
"users": {
"b4b44424-0000-0000-0000-25c224dad337": {
"UserType": "superuser",
"FirstName": "m10x",
"LastName": "m10x",
"identifiers": ["phone:+494444444", "email:m10x@example.com"]
}
},
"pubsubChannel": "pubsub",
"ruleHash": {},
"cameras": [],
"geofenceLimits": {
"ios": {
"debounceInterval": 90,
"gpsAccuracyMultiplier": 2.5,
"maximumGeofence": 5000,
"minimumGeofence": 100,
"minGPSAccuracyRequired": 80
}
},
"accessSchedulesAllowed": true
}

View File

@ -191,6 +191,9 @@ async def _create_august_api_with_devices(
api_call_side_effects.setdefault(
"unlock_return_activities", unlock_return_activities_side_effect
)
api_call_side_effects.setdefault(
"async_unlatch_return_activities", unlock_return_activities_side_effect
)
api_instance, entry = await _mock_setup_august_with_api_side_effects(
hass, api_call_side_effects, pubnub, brand
@ -244,10 +247,17 @@ async def _mock_setup_august_with_api_side_effects(
side_effect=api_call_side_effects["unlock_return_activities"]
)
if api_call_side_effects["async_unlatch_return_activities"]:
type(api_instance).async_unlatch_return_activities = AsyncMock(
side_effect=api_call_side_effects["async_unlatch_return_activities"]
)
api_instance.async_unlock_async = AsyncMock()
api_instance.async_lock_async = AsyncMock()
api_instance.async_status_async = AsyncMock()
api_instance.async_get_user = AsyncMock(return_value={"UserID": "abc"})
api_instance.async_unlatch_async = AsyncMock()
api_instance.async_unlatch = AsyncMock()
return api_instance, await _mock_setup_august(
hass, api_instance, pubnub, brand=brand
@ -366,6 +376,10 @@ async def _mock_doorsense_missing_august_lock_detail(hass):
return await _mock_lock_from_fixture(hass, "get_lock.online_missing_doorsense.json")
async def _mock_lock_with_unlatch(hass):
return await _mock_lock_from_fixture(hass, "get_lock.online_with_unlatch.json")
def _mock_lock_operation_activity(lock, action, offset):
return LockOperationActivity(
SOURCE_LOCK_OPERATE,

View File

@ -3,6 +3,7 @@
from unittest.mock import Mock, patch
from aiohttp import ClientResponseError
import pytest
from yalexs.authenticator_common import AuthenticationState
from yalexs.exceptions import AugustApiAIOHTTPError
@ -12,6 +13,7 @@ from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_LOCK,
SERVICE_OPEN,
SERVICE_UNLOCK,
STATE_LOCKED,
STATE_ON,
@ -162,6 +164,17 @@ async def test_lock_throws_august_api_http_error(hass: HomeAssistant) -> None:
)
async def test_open_throws_hass_service_not_supported_error(
hass: HomeAssistant,
) -> None:
"""Test open throws correct error on entity does not support this service error."""
mocked_lock_detail = await _mock_operative_august_lock_detail(hass)
await _create_august_with_devices(hass, [mocked_lock_detail])
data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"}
with pytest.raises(HomeAssistantError):
await hass.services.async_call(LOCK_DOMAIN, SERVICE_OPEN, data, blocking=True)
async def test_inoperative_locks_are_filtered_out(hass: HomeAssistant) -> None:
"""Ensure inoperative locks do not get setup."""
august_operative_lock = await _mock_operative_august_lock_detail(hass)

View File

@ -18,6 +18,7 @@ from homeassistant.components.lock import (
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_LOCK,
SERVICE_OPEN,
SERVICE_UNLOCK,
STATE_LOCKED,
STATE_UNAVAILABLE,
@ -25,6 +26,7 @@ from homeassistant.const import (
STATE_UNLOCKED,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
import homeassistant.util.dt as dt_util
@ -33,6 +35,8 @@ from .mocks import (
_mock_activities_from_fixture,
_mock_doorsense_enabled_august_lock_detail,
_mock_lock_from_fixture,
_mock_lock_with_unlatch,
_mock_operative_august_lock_detail,
)
from tests.common import async_fire_time_changed
@ -156,6 +160,60 @@ async def test_one_lock_operation(
)
async def test_open_lock_operation(hass: HomeAssistant) -> None:
"""Test open lock operation using the open service."""
lock_with_unlatch = await _mock_lock_with_unlatch(hass)
await _create_august_with_devices(hass, [lock_with_unlatch])
lock_online_with_unlatch_name = hass.states.get("lock.online_with_unlatch_name")
assert lock_online_with_unlatch_name.state == STATE_LOCKED
data = {ATTR_ENTITY_ID: "lock.online_with_unlatch_name"}
await hass.services.async_call(LOCK_DOMAIN, SERVICE_OPEN, data, blocking=True)
await hass.async_block_till_done()
lock_online_with_unlatch_name = hass.states.get("lock.online_with_unlatch_name")
assert lock_online_with_unlatch_name.state == STATE_UNLOCKED
async def test_open_lock_operation_pubnub_connected(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test open lock operation using the open service when pubnub is connected."""
lock_with_unlatch = await _mock_lock_with_unlatch(hass)
assert lock_with_unlatch.pubsub_channel == "pubsub"
pubnub = AugustPubNub()
await _create_august_with_devices(hass, [lock_with_unlatch], pubnub=pubnub)
pubnub.connected = True
lock_online_with_unlatch_name = hass.states.get("lock.online_with_unlatch_name")
assert lock_online_with_unlatch_name.state == STATE_LOCKED
data = {ATTR_ENTITY_ID: "lock.online_with_unlatch_name"}
await hass.services.async_call(LOCK_DOMAIN, SERVICE_OPEN, data, blocking=True)
await hass.async_block_till_done()
pubnub.message(
pubnub,
Mock(
channel=lock_with_unlatch.pubsub_channel,
timetoken=(dt_util.utcnow().timestamp() + 2) * 10000000,
message={
"status": "kAugLockState_Unlocked",
},
),
)
await hass.async_block_till_done()
await hass.async_block_till_done()
lock_online_with_unlatch_name = hass.states.get("lock.online_with_unlatch_name")
assert lock_online_with_unlatch_name.state == STATE_UNLOCKED
await hass.async_block_till_done()
async def test_one_lock_operation_pubnub_connected(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
@ -449,3 +507,14 @@ async def test_lock_update_via_pubnub(hass: HomeAssistant) -> None:
await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()
async def test_open_throws_hass_service_not_supported_error(
hass: HomeAssistant,
) -> None:
"""Test open throws correct error on entity does not support this service error."""
mocked_lock_detail = await _mock_operative_august_lock_detail(hass)
await _create_august_with_devices(hass, [mocked_lock_detail])
data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"}
with pytest.raises(HomeAssistantError, match="does not support this service"):
await hass.services.async_call(LOCK_DOMAIN, SERVICE_OPEN, data, blocking=True)