From 32a94fc1140be37cfff659cb8dfb18b2fcd16950 Mon Sep 17 00:00:00 2001 From: Lenn <78048721+LennP@users.noreply.github.com> Date: Sat, 22 Jun 2024 09:13:14 +0200 Subject: [PATCH] Motionblinds Bluetooth options (#120110) --- .../components/motionblinds_ble/__init__.py | 35 ++++++++++- .../motionblinds_ble/config_flow.py | 59 ++++++++++++++++++- .../components/motionblinds_ble/const.py | 3 + .../components/motionblinds_ble/strings.json | 12 ++++ .../motionblinds_ble/test_config_flow.py | 32 ++++++++++ 5 files changed, 138 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motionblinds_ble/__init__.py b/homeassistant/components/motionblinds_ble/__init__.py index 3c6df12e878..1b664eeede3 100644 --- a/homeassistant/components/motionblinds_ble/__init__.py +++ b/homeassistant/components/motionblinds_ble/__init__.py @@ -24,7 +24,13 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType -from .const import CONF_BLIND_TYPE, CONF_MAC_CODE, DOMAIN +from .const import ( + CONF_BLIND_TYPE, + CONF_MAC_CODE, + DOMAIN, + OPTION_DISCONNECT_TIME, + OPTION_PERMANENT_CONNECTION, +) _LOGGER = logging.getLogger(__name__) @@ -86,13 +92,40 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = device + # Register OptionsFlow update listener + entry.async_on_unload(entry.add_update_listener(options_update_listener)) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + # Apply options + entry.async_create_background_task( + hass, apply_options(hass, entry), device.ble_device.address + ) + _LOGGER.debug("(%s) Finished setting up device", entry.data[CONF_MAC_CODE]) return True +async def options_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + _LOGGER.debug( + "(%s) Updated device options: %s", entry.data[CONF_MAC_CODE], entry.options + ) + await apply_options(hass, entry) + + +async def apply_options(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Apply the options from the OptionsFlow.""" + + device: MotionDevice = hass.data[DOMAIN][entry.entry_id] + disconnect_time: float | None = entry.options.get(OPTION_DISCONNECT_TIME, None) + permanent_connection: bool = entry.options.get(OPTION_PERMANENT_CONNECTION, False) + + device.set_custom_disconnect_time(disconnect_time) + await device.set_permanent_connection(permanent_connection) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Motionblinds Bluetooth device from a config entry.""" diff --git a/homeassistant/components/motionblinds_ble/config_flow.py b/homeassistant/components/motionblinds_ble/config_flow.py index 23302ae9624..b8e03386844 100644 --- a/homeassistant/components/motionblinds_ble/config_flow.py +++ b/homeassistant/components/motionblinds_ble/config_flow.py @@ -7,13 +7,19 @@ import re from typing import TYPE_CHECKING, Any from bleak.backends.device import BLEDevice -from motionblindsble.const import DISPLAY_NAME, MotionBlindType +from motionblindsble.const import DISPLAY_NAME, SETTING_DISCONNECT_TIME, MotionBlindType import voluptuous as vol from homeassistant.components import bluetooth from homeassistant.components.bluetooth import BluetoothServiceInfoBleak -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_ADDRESS +from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.selector import ( SelectSelector, @@ -30,6 +36,8 @@ from .const import ( ERROR_INVALID_MAC_CODE, ERROR_NO_BLUETOOTH_ADAPTER, ERROR_NO_DEVICES_FOUND, + OPTION_DISCONNECT_TIME, + OPTION_PERMANENT_CONNECTION, ) _LOGGER = logging.getLogger(__name__) @@ -174,6 +182,53 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): self._mac_code = mac_code.upper() self._display_name = DISPLAY_NAME.format(mac_code=self._mac_code) + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> OptionsFlow: + """Create the options flow.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(OptionsFlow): + """Handle an options flow for Motionblinds BLE.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required( + OPTION_PERMANENT_CONNECTION, + default=( + self.config_entry.options.get( + OPTION_PERMANENT_CONNECTION, False + ) + ), + ): bool, + vol.Optional( + OPTION_DISCONNECT_TIME, + default=( + self.config_entry.options.get( + OPTION_DISCONNECT_TIME, SETTING_DISCONNECT_TIME + ) + ), + ): vol.All(vol.Coerce(int), vol.Range(min=0)), + } + ), + ) + def is_valid_mac(data: str) -> bool: """Validate the provided MAC address.""" diff --git a/homeassistant/components/motionblinds_ble/const.py b/homeassistant/components/motionblinds_ble/const.py index bd88927559e..0b4a2a7f947 100644 --- a/homeassistant/components/motionblinds_ble/const.py +++ b/homeassistant/components/motionblinds_ble/const.py @@ -19,3 +19,6 @@ ERROR_NO_DEVICES_FOUND = "no_devices_found" ICON_VERTICAL_BLIND = "mdi:blinds-vertical-closed" MANUFACTURER = "Motionblinds" + +OPTION_DISCONNECT_TIME = "disconnect_time" +OPTION_PERMANENT_CONNECTION = "permanent_connection" diff --git a/homeassistant/components/motionblinds_ble/strings.json b/homeassistant/components/motionblinds_ble/strings.json index 0bc9ad4c012..ab26f26ce44 100644 --- a/homeassistant/components/motionblinds_ble/strings.json +++ b/homeassistant/components/motionblinds_ble/strings.json @@ -20,6 +20,18 @@ } } }, + "options": { + "step": { + "init": { + "title": "Connection options", + "description": "The default disconnect time is 15 seconds, adjustable using the slider below. You may want to adjust this if you have larger blinds or other specific needs. You can also enable a permanent connection to the motor, which disables the disconnect time and automatically reconnects when the motor is disconnected for any reason.\n**WARNING**: Changing any of the below options may significantly reduce battery life of your motor!", + "data": { + "permanent_connection": "Permanent connection", + "disconnect_time": "Disconnect time (seconds)" + } + } + } + }, "selector": { "blind_type": { "options": { diff --git a/tests/components/motionblinds_ble/test_config_flow.py b/tests/components/motionblinds_ble/test_config_flow.py index 90d2cbdcbc6..4cab12269dd 100644 --- a/tests/components/motionblinds_ble/test_config_flow.py +++ b/tests/components/motionblinds_ble/test_config_flow.py @@ -14,6 +14,7 @@ from homeassistant.data_entry_flow import FlowResultType from .conftest import TEST_ADDRESS, TEST_MAC, TEST_NAME +from tests.common import MockConfigEntry from tests.components.bluetooth import generate_advertisement_data, generate_ble_device TEST_BLIND_TYPE = MotionBlindType.ROLLER.name.lower() @@ -255,3 +256,34 @@ async def test_config_flow_bluetooth_success(hass: HomeAssistant) -> None: const.CONF_BLIND_TYPE: TEST_BLIND_TYPE, } assert result["options"] == {} + + +async def test_options_flow(hass: HomeAssistant) -> None: + """Test the options flow.""" + entry = MockConfigEntry( + domain=const.DOMAIN, + unique_id="0123456789", + data={ + const.CONF_BLIND_TYPE: MotionBlindType.ROLLER, + }, + ) + entry.add_to_hass(hass) + + 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"] is FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + const.OPTION_PERMANENT_CONNECTION: True, + const.OPTION_DISCONNECT_TIME: 10, + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY