diff --git a/homeassistant/components/yalexs_ble/__init__.py b/homeassistant/components/yalexs_ble/__init__.py index 4e9b7513745..11516015b6c 100644 --- a/homeassistant/components/yalexs_ble/__init__.py +++ b/homeassistant/components/yalexs_ble/__init__.py @@ -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) diff --git a/homeassistant/components/yalexs_ble/config_flow.py b/homeassistant/components/yalexs_ble/config_flow.py index b87066b50fe..3a6d91c4f55 100644 --- a/homeassistant/components/yalexs_ble/config_flow.py +++ b/homeassistant/components/yalexs_ble/config_flow.py @@ -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, + } + ), + ) diff --git a/homeassistant/components/yalexs_ble/const.py b/homeassistant/components/yalexs_ble/const.py index f38a376a717..18555f91075 100644 --- a/homeassistant/components/yalexs_ble/const.py +++ b/homeassistant/components/yalexs_ble/const.py @@ -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 diff --git a/homeassistant/components/yalexs_ble/models.py b/homeassistant/components/yalexs_ble/models.py index d79668f1c70..3b83b52cf73 100644 --- a/homeassistant/components/yalexs_ble/models.py +++ b/homeassistant/components/yalexs_ble/models.py @@ -12,3 +12,4 @@ class YaleXSBLEData: title: str lock: PushLock + always_connected: bool diff --git a/homeassistant/components/yalexs_ble/strings.json b/homeassistant/components/yalexs_ble/strings.json index c2d1a2155c3..bd96e07f6ba 100644 --- a/homeassistant/components/yalexs_ble/strings.json +++ b/homeassistant/components/yalexs_ble/strings.json @@ -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" + } + } + } } } diff --git a/tests/components/yalexs_ble/test_config_flow.py b/tests/components/yalexs_ble/test_config_flow.py index a0b8dfb6862..2df37a72b70 100644 --- a/tests/components/yalexs_ble/test_config_flow.py +++ b/tests/components/yalexs_ble/test_config_flow.py @@ -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