diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 81842f995e8..f4a0f57eb76 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -13,6 +13,7 @@ from yalexs.lock import Lock, LockDetail from yalexs.pubnub_activity import activities_from_pubnub_message from yalexs.pubnub_async import AugustPubNub, async_create_pubnub +from homeassistant.components import yalexs_ble from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant, callback @@ -93,6 +94,26 @@ async def async_setup_august( return True +@callback +def _async_trigger_ble_lock_discovery( + hass: HomeAssistant, locks_with_offline_keys: list[LockDetail] +): + """Update keys for the yalexs-ble integration if available.""" + for lock_detail in locks_with_offline_keys: + yalexs_ble.async_discovery( + hass, + yalexs_ble.YaleXSBLEDiscovery( + { + "name": lock_detail.device_name, + "address": lock_detail.mac_address, + "serial": lock_detail.serial_number, + "key": lock_detail.offline_key, + "slot": lock_detail.offline_slot, + } + ), + ) + + class AugustData(AugustSubscriberMixin): """August data object.""" @@ -133,6 +154,19 @@ class AugustData(AugustSubscriberMixin): # detail as we cannot determine if they are usable. # This also allows us to avoid checking for # detail being None all over the place + + # Currently we know how to feed data to yalexe_ble + # but we do not know how to send it to homekit_controller + # yet + _async_trigger_ble_lock_discovery( + self._hass, + [ + lock_detail + for lock_detail in self._device_detail_by_id.values() + if isinstance(lock_detail, LockDetail) and lock_detail.offline_key + ], + ) + self._remove_inoperative_locks() self._remove_inoperative_doorbells() diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 418fa6920ad..c688aa1a775 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -24,5 +24,6 @@ ], "config_flow": true, "iot_class": "cloud_push", - "loggers": ["pubnub", "yalexs"] + "loggers": ["pubnub", "yalexs"], + "after_dependencies": ["yalexs_ble"] } diff --git a/homeassistant/components/yalexs_ble/__init__.py b/homeassistant/components/yalexs_ble/__init__.py index e38dba00ef8..265b10a502b 100644 --- a/homeassistant/components/yalexs_ble/__init__.py +++ b/homeassistant/components/yalexs_ble/__init__.py @@ -2,12 +2,13 @@ from __future__ import annotations import asyncio +from typing import TypedDict import async_timeout from yalexs_ble import PushLock, local_name_is_unique from homeassistant.components import bluetooth -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry from homeassistant.const import CONF_ADDRESS, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -19,6 +20,28 @@ from .util import async_find_existing_service_info, bluetooth_callback_matcher PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.LOCK] +class YaleXSBLEDiscovery(TypedDict): + """A validated discovery of a Yale XS BLE device.""" + + name: str + address: str + serial: str + key: str + slot: int + + +@callback +def async_discovery(hass: HomeAssistant, discovery: YaleXSBLEDiscovery) -> None: + """Update keys for the yalexs-ble integration if available.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + "yalexs_ble", + context={"source": SOURCE_INTEGRATION_DISCOVERY}, + data=discovery, + ) + ) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Yale Access Bluetooth from a config entry.""" local_name = entry.data[CONF_LOCAL_NAME] diff --git a/tests/components/august/fixtures/get_lock.doorsense_init.json b/tests/components/august/fixtures/get_lock.doorsense_init.json index d85ca3b153f..1132cc61a8d 100644 --- a/tests/components/august/fixtures/get_lock.doorsense_init.json +++ b/tests/components/august/fixtures/get_lock.doorsense_init.json @@ -40,15 +40,7 @@ }, "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" - } - ], + "loaded": [], "deleted": [], "loadedhk": [ { diff --git a/tests/components/august/fixtures/get_lock.low_keypad_battery.json b/tests/components/august/fixtures/get_lock.low_keypad_battery.json index b10c3f2600f..08bdfaa76ed 100644 --- a/tests/components/august/fixtures/get_lock.low_keypad_battery.json +++ b/tests/components/august/fixtures/get_lock.low_keypad_battery.json @@ -40,15 +40,7 @@ }, "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" - } - ], + "loaded": [], "deleted": [], "loadedhk": [ { diff --git a/tests/components/august/fixtures/get_lock.offline.json b/tests/components/august/fixtures/get_lock.offline.json index 753a1081918..50d3d345ef8 100644 --- a/tests/components/august/fixtures/get_lock.offline.json +++ b/tests/components/august/fixtures/get_lock.offline.json @@ -19,15 +19,7 @@ } ], "deleted": [], - "loaded": [ - { - "UserID": "userid", - "created": "2000-00-00T00:00:00.447Z", - "key": "key", - "loaded": "2000-00-00T00:00:00.447Z", - "slot": 1 - } - ] + "loaded": [] }, "SerialNumber": "ABC", "Type": 3, diff --git a/tests/components/august/fixtures/get_lock.online.json b/tests/components/august/fixtures/get_lock.online.json index 7fa12fa8bcb..7abadeef4b6 100644 --- a/tests/components/august/fixtures/get_lock.online.json +++ b/tests/components/august/fixtures/get_lock.online.json @@ -40,15 +40,7 @@ }, "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" - } - ], + "loaded": [], "deleted": [], "loadedhk": [ { diff --git a/tests/components/august/fixtures/get_lock.online_with_keys.json b/tests/components/august/fixtures/get_lock.online_with_keys.json new file mode 100644 index 00000000000..7fa12fa8bcb --- /dev/null +++ b/tests/components/august/fixtures/get_lock.online_with_keys.json @@ -0,0 +1,100 @@ +{ + "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/components/august/mocks.py b/tests/components/august/mocks.py index 932065f37da..2fa59fe964c 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -302,6 +302,10 @@ async def _mock_operative_august_lock_detail(hass): return await _mock_lock_from_fixture(hass, "get_lock.online.json") +async def _mock_lock_with_offline_key(hass): + return await _mock_lock_from_fixture(hass, "get_lock.online_with_keys.json") + + async def _mock_inoperative_august_lock_detail(hass): return await _mock_lock_from_fixture(hass, "get_lock.offline.json") diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index 56113832d23..ab3269e9ac8 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -29,6 +29,7 @@ from tests.components.august.mocks import ( _mock_doorsense_missing_august_lock_detail, _mock_get_config, _mock_inoperative_august_lock_detail, + _mock_lock_with_offline_key, _mock_operative_august_lock_detail, ) @@ -323,6 +324,31 @@ async def test_load_unload(hass): await hass.async_block_till_done() +async def test_load_triggers_ble_discovery(hass): + """Test that loading a lock that supports offline ble operation passes the keys to yalexe_ble.""" + + august_lock_with_key = await _mock_lock_with_offline_key(hass) + august_lock_without_key = await _mock_operative_august_lock_detail(hass) + + with patch( + "homeassistant.components.august.yalexs_ble.async_discovery" + ) as mock_discovery: + config_entry = await _create_august_with_devices( + hass, [august_lock_with_key, august_lock_without_key] + ) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + assert len(mock_discovery.mock_calls) == 1 + assert mock_discovery.mock_calls[0][1][1] == { + "name": "Front Door Lock", + "address": None, + "serial": "X2FSW05DGA", + "key": "kkk01d4300c1dcxxx1c330f794941111", + "slot": 1, + } + + async def remove_device(ws_client, device_id, config_entry_id): """Remove config entry from a device.""" await ws_client.send_json(