From 5b896b315eb1eea2b89e385ed1566d1ab1f22ef0 Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Wed, 25 May 2022 01:49:53 -0700 Subject: [PATCH] Add TotalConnect options flow to auto-bypass low battery (#62458) * rebase * use bypass option in async_setup_entry * add test for options flow * default to False for AUTO_BYPASS * fix bypass defaults --- .../components/totalconnect/__init__.py | 16 ++++++- .../components/totalconnect/config_flow.py | 34 +++++++++++++- .../components/totalconnect/const.py | 2 + .../components/totalconnect/strings.json | 11 +++++ .../totalconnect/test_config_flow.py | 46 ++++++++++++++++++- 5 files changed, 105 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index 8a4aee0debb..87977e5c1db 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -17,7 +17,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_USERCODES, DOMAIN +from .const import AUTO_BYPASS, CONF_USERCODES, DOMAIN PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR] @@ -31,6 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: conf = entry.data username = conf[CONF_USERNAME] password = conf[CONF_PASSWORD] + bypass = entry.options.get(AUTO_BYPASS, False) if CONF_USERCODES not in conf: # should only happen for those who used UI before we added usercodes @@ -41,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: client = await hass.async_add_executor_job( - TotalConnectClient, username, password, usercodes + TotalConnectClient, username, password, usercodes, bypass ) except AuthenticationError as exception: raise ConfigEntryAuthFailed( @@ -54,6 +55,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + entry.async_on_unload(entry.add_update_listener(update_listener)) + return True @@ -66,6 +70,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener.""" + bypass = entry.options.get(AUTO_BYPASS, False) + client = hass.data[DOMAIN][entry.entry_id].client + for location_id in client.locations: + client.locations[location_id].auto_bypass_low_battery = bypass + + class TotalConnectDataUpdateCoordinator(DataUpdateCoordinator): """Class to fetch data from TotalConnect.""" diff --git a/homeassistant/components/totalconnect/config_flow.py b/homeassistant/components/totalconnect/config_flow.py index 013b08b50be..49e60b5b46e 100644 --- a/homeassistant/components/totalconnect/config_flow.py +++ b/homeassistant/components/totalconnect/config_flow.py @@ -5,8 +5,9 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_LOCATION, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback -from .const import CONF_USERCODES, DOMAIN +from .const import AUTO_BYPASS, CONF_USERCODES, DOMAIN PASSWORD_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) @@ -162,3 +163,34 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_abort(reason="reauth_successful") + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get options flow.""" + return TotalConnectOptionsFlowHandler(config_entry) + + +class TotalConnectOptionsFlowHandler(config_entries.OptionsFlow): + """TotalConnect options flow handler.""" + + def __init__(self, config_entry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """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( + AUTO_BYPASS, + default=self.config_entry.options.get(AUTO_BYPASS, False), + ): bool + } + ), + ) diff --git a/homeassistant/components/totalconnect/const.py b/homeassistant/components/totalconnect/const.py index ba217bd4ca7..5012a303b69 100644 --- a/homeassistant/components/totalconnect/const.py +++ b/homeassistant/components/totalconnect/const.py @@ -2,6 +2,8 @@ DOMAIN = "totalconnect" CONF_USERCODES = "usercodes" +CONF_LOCATION = "location" +AUTO_BYPASS = "auto_bypass_low_battery" # Most TotalConnect alarms will work passing '-1' as usercode DEFAULT_USERCODE = "-1" diff --git a/homeassistant/components/totalconnect/strings.json b/homeassistant/components/totalconnect/strings.json index 64ca1beafd8..346ea7ef403 100644 --- a/homeassistant/components/totalconnect/strings.json +++ b/homeassistant/components/totalconnect/strings.json @@ -28,5 +28,16 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "no_locations": "No locations are available for this user, check TotalConnect settings" } + }, + "options": { + "step": { + "init": { + "title": "TotalConnect Options", + "description": "Automatically bypass zones the moment they report a low battery.", + "data": { + "auto_bypass_low_battery": "Auto bypass low battery" + } + } + } } } diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py index 631553a4af4..78b121dda77 100644 --- a/tests/components/totalconnect/test_config_flow.py +++ b/tests/components/totalconnect/test_config_flow.py @@ -4,9 +4,14 @@ from unittest.mock import patch from total_connect_client.exceptions import AuthenticationError from homeassistant import data_entry_flow -from homeassistant.components.totalconnect.const import CONF_USERCODES, DOMAIN +from homeassistant.components.totalconnect.const import ( + AUTO_BYPASS, + CONF_USERCODES, + DOMAIN, +) from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD +from homeassistant.core import HomeAssistant from .common import ( CONFIG_DATA, @@ -190,3 +195,42 @@ async def test_no_locations(hass): await hass.async_block_till_done() assert mock_request.call_count == 1 + + +async def test_options_flow(hass: HomeAssistant): + """Test config flow options.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=CONFIG_DATA, + unique_id=USERNAME, + ) + config_entry.add_to_hass(hass) + + responses = [ + RESPONSE_AUTHENTICATE, + RESPONSE_PARTITION_DETAILS, + RESPONSE_GET_ZONE_DETAILS_SUCCESS, + RESPONSE_DISARMED, + RESPONSE_DISARMED, + RESPONSE_DISARMED, + ] + + with patch(TOTALCONNECT_REQUEST, side_effect=responses): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={AUTO_BYPASS: True} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == {AUTO_BYPASS: True} + await hass.async_block_till_done() + + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done()