Add always connected option to Yale Access Bluetooth (#93224)

* Add always connected option to Yale Access Bluetooth

If the lock does not support push updates via advertisements or you want lock operation to be more responsive, you can enable always connected mode. Always connected will cause the lock to stay connected to Home Assistant via Bluetooth, which will use more battery.

* Update homeassistant/components/yalexs_ble/config_flow.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
pull/93242/head
J. Nick Koston 2023-05-18 10:48:04 -05:00 committed by GitHub
parent 2eef2ed911
commit 763b898621
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 138 additions and 7 deletions

View File

@ -19,7 +19,14 @@ from homeassistant.const import CONF_ADDRESS, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from .const import CONF_KEY, CONF_LOCAL_NAME, CONF_SLOT, DEVICE_TIMEOUT, DOMAIN
from .const import (
CONF_ALWAYS_CONNECTED,
CONF_KEY,
CONF_LOCAL_NAME,
CONF_SLOT,
DEVICE_TIMEOUT,
DOMAIN,
)
from .models import YaleXSBLEData
from .util import async_find_existing_service_info, bluetooth_callback_matcher
@ -33,7 +40,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
key = entry.data[CONF_KEY]
slot = entry.data[CONF_SLOT]
has_unique_local_name = local_name_is_unique(local_name)
push_lock = PushLock(local_name, address, None, key, slot)
always_connected = entry.options.get(CONF_ALWAYS_CONNECTED, False)
push_lock = PushLock(
local_name, address, None, key, slot, always_connected=always_connected
)
id_ = local_name if has_unique_local_name else address
push_lock.set_name(f"{entry.title} ({id_})")
@ -79,7 +89,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
) from ex
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = YaleXSBLEData(
entry.title, push_lock
entry.title, push_lock, always_connected
)
@callback
@ -115,7 +125,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
data: YaleXSBLEData = hass.data[DOMAIN][entry.entry_id]
if entry.title != data.title:
if entry.title != data.title or data.always_connected != entry.options.get(
CONF_ALWAYS_CONNECTED
):
await hass.config_entries.async_reload(entry.entry_id)

View File

@ -23,10 +23,11 @@ from homeassistant.components.bluetooth import (
async_discovered_service_info,
)
from homeassistant.const import CONF_ADDRESS
from homeassistant.core import callback
from homeassistant.data_entry_flow import AbortFlow, FlowResult
from homeassistant.helpers.typing import DiscoveryInfoType
from .const import CONF_KEY, CONF_LOCAL_NAME, CONF_SLOT, DOMAIN
from .const import CONF_ALWAYS_CONNECTED, CONF_KEY, CONF_LOCAL_NAME, CONF_SLOT, DOMAIN
from .util import async_find_existing_service_info, human_readable_name
_LOGGER = logging.getLogger(__name__)
@ -297,3 +298,46 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
data_schema=data_schema,
errors=errors,
)
@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> YaleXSBLEOptionsFlowHandler:
"""Get the options flow for this handler."""
return YaleXSBLEOptionsFlowHandler(config_entry)
class YaleXSBLEOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle YaleXSBLE options."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize YaleXSBLE options flow."""
self.entry = config_entry
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the YaleXSBLE options."""
return await self.async_step_device_options()
async def async_step_device_options(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the YaleXSBLE devices options."""
if user_input is not None:
return self.async_create_entry(
data={CONF_ALWAYS_CONNECTED: user_input[CONF_ALWAYS_CONNECTED]},
)
return self.async_show_form(
step_id="device_options",
data_schema=vol.Schema(
{
vol.Optional(
CONF_ALWAYS_CONNECTED,
default=self.entry.options.get(CONF_ALWAYS_CONNECTED, False),
): bool,
}
),
)

View File

@ -5,5 +5,6 @@ DOMAIN = "yalexs_ble"
CONF_LOCAL_NAME = "local_name"
CONF_KEY = "key"
CONF_SLOT = "slot"
CONF_ALWAYS_CONNECTED = "always_connected"
DEVICE_TIMEOUT = 55

View File

@ -12,3 +12,4 @@ class YaleXSBLEData:
title: str
lock: PushLock
always_connected: bool

View File

@ -35,5 +35,15 @@
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"options": {
"step": {
"device_options": {
"description": "If the lock does not support push updates via advertisements or you want lock operation to be more responsive, you can enable always connected mode. Always connected will cause the lock to stay connected to Home Assistant via Bluetooth, which will use more battery.",
"data": {
"always_connected": "Always connected"
}
}
}
}
}

View File

@ -1,13 +1,14 @@
"""Test the Yale Access Bluetooth config flow."""
import asyncio
from unittest.mock import patch
from unittest.mock import AsyncMock, Mock, patch
from bleak import BleakError
import pytest
from yalexs_ble import AuthError
from yalexs_ble import AuthError, DoorStatus, LockInfo, LockState, LockStatus
from homeassistant import config_entries
from homeassistant.components.yalexs_ble.const import (
CONF_ALWAYS_CONNECTED,
CONF_KEY,
CONF_LOCAL_NAME,
CONF_SLOT,
@ -27,6 +28,23 @@ from . import (
from tests.common import MockConfigEntry
def _get_mock_push_lock():
"""Return a mock PushLock."""
mock_push_lock = Mock()
mock_push_lock.start = AsyncMock()
mock_push_lock.wait_for_first_update = AsyncMock()
mock_push_lock.stop = AsyncMock()
mock_push_lock.lock_state = LockState(
LockStatus.UNLOCKED, DoorStatus.CLOSED, None, None
)
mock_push_lock.lock_status = LockStatus.UNLOCKED
mock_push_lock.door_status = DoorStatus.CLOSED
mock_push_lock.lock_info = LockInfo("Front Door", "M1XXX012LU", "1.0.0", "1.0.0")
mock_push_lock.device_info = None
mock_push_lock.address = YALE_ACCESS_LOCK_DISCOVERY_INFO.address
return mock_push_lock
@pytest.mark.parametrize("slot", [0, 1, 66])
async def test_user_step_success(hass: HomeAssistant, slot: int) -> None:
"""Test user step success path."""
@ -947,3 +965,48 @@ async def test_reauth(hass: HomeAssistant) -> None:
assert result3["type"] == FlowResultType.ABORT
assert result3["reason"] == "reauth_successful"
assert len(mock_setup_entry.mock_calls) == 1
async def test_options(hass: HomeAssistant) -> None:
"""Test options."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name,
CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f",
CONF_SLOT: 66,
},
unique_id=YALE_ACCESS_LOCK_DISCOVERY_INFO.address,
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.yalexs_ble.PushLock",
return_value=_get_mock_push_lock(),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(
entry.entry_id,
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "device_options"
with patch(
"homeassistant.components.yalexs_ble.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
{
CONF_ALWAYS_CONNECTED: True,
},
)
await hass.async_block_till_done()
assert result2["type"] == FlowResultType.CREATE_ENTRY
assert entry.options == {CONF_ALWAYS_CONNECTED: True}
assert len(mock_setup_entry.mock_calls) == 1