From 81651b0b25444a38f3189c8dfb2f95c1431a9d45 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 7 May 2020 00:15:49 +0200 Subject: [PATCH] Add Synology DSM scan interval option flow (#35183) * Add Synology DSM scan interval option flow * Add options tests * Review: use entry.update_listeners * Use cv.positive_int * Try to fix "ValueError: Config entry has already been setup!" * Fix ValueError --- .../components/synology_dsm/__init__.py | 88 ++++++++++--------- .../components/synology_dsm/config_flow.py | 42 ++++++++- .../components/synology_dsm/const.py | 7 ++ .../components/synology_dsm/sensor.py | 3 +- .../components/synology_dsm/strings.json | 9 ++ .../synology_dsm/translations/en.json | 11 ++- tests/components/synology_dsm/conftest.py | 4 +- .../synology_dsm/test_config_flow.py | 39 ++++++++ 8 files changed, 156 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 9431cb7b1c9..39ce1547a2c 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -14,6 +14,7 @@ from homeassistant.const import ( CONF_MAC, CONF_PASSWORD, CONF_PORT, + CONF_SCAN_INTERVAL, CONF_SSL, CONF_USERNAME, ) @@ -22,7 +23,14 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType -from .const import CONF_VOLUMES, DEFAULT_SSL, DOMAIN +from .const import ( + CONF_VOLUMES, + DEFAULT_SCAN_INTERVAL, + DEFAULT_SSL, + DOMAIN, + SYNO_API, + UNDO_UPDATE_LISTENER, +) CONFIG_SCHEMA = vol.Schema( { @@ -41,8 +49,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SCAN_INTERVAL = timedelta(minutes=15) - async def async_setup(hass, config): """Set up Synology DSM sensors from legacy config file.""" @@ -63,20 +69,17 @@ async def async_setup(hass, config): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Set up Synology DSM sensors.""" - host = entry.data[CONF_HOST] - port = entry.data[CONF_PORT] - username = entry.data[CONF_USERNAME] - password = entry.data[CONF_PASSWORD] - unit = hass.config.units.temperature_unit - use_ssl = entry.data[CONF_SSL] - device_token = entry.data.get("device_token") - - api = SynoApi(hass, host, port, username, password, unit, use_ssl, device_token) + api = SynoApi(hass, entry) await api.async_setup() + undo_listener = entry.add_update_listener(_async_update_listener) + hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.unique_id] = api + hass.data[DOMAIN][entry.unique_id] = { + SYNO_API: api, + UNDO_UPDATE_LISTENER: undo_listener, + } # For SSDP compat if not entry.data.get(CONF_MAC): @@ -94,34 +97,31 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): """Unload Synology DSM sensors.""" - api = hass.data[DOMAIN][entry.unique_id] - await api.async_unload() - return await hass.config_entries.async_forward_entry_unload(entry, "sensor") + unload_ok = await hass.config_entries.async_forward_entry_unload(entry, "sensor") + + if unload_ok: + entry_data = hass.data[DOMAIN][entry.unique_id] + entry_data[UNDO_UPDATE_LISTENER]() + await entry_data[SYNO_API].async_unload() + hass.data[DOMAIN].pop(entry.unique_id) + + return unload_ok + + +async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) class SynoApi: """Class to interface with Synology DSM API.""" - def __init__( - self, - hass: HomeAssistantType, - host: str, - port: int, - username: str, - password: str, - temp_unit: str, - use_ssl: bool, - device_token: str, - ): + def __init__(self, hass: HomeAssistantType, entry: ConfigEntry): """Initialize the API wrapper class.""" self._hass = hass - self._host = host - self._port = port - self._username = username - self._password = password - self._use_ssl = use_ssl - self._device_token = device_token - self.temp_unit = temp_unit + self._entry = entry + + self.temp_unit = hass.config.units.temperature_unit self.dsm: SynologyDSM = None self.information: SynoDSMInformation = None @@ -138,19 +138,25 @@ class SynoApi: async def async_setup(self): """Start interacting with the NAS.""" self.dsm = SynologyDSM( - self._host, - self._port, - self._username, - self._password, - self._use_ssl, - device_token=self._device_token, + self._entry.data[CONF_HOST], + self._entry.data[CONF_PORT], + self._entry.data[CONF_USERNAME], + self._entry.data[CONF_PASSWORD], + self._entry.data[CONF_SSL], + device_token=self._entry.data.get("device_token"), ) await self._hass.async_add_executor_job(self._fetch_device_configuration) await self.update() self._unsub_dispatcher = async_track_time_interval( - self._hass, self.update, SCAN_INTERVAL + self._hass, + self.update, + timedelta( + minutes=self._entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ) + ), ) def _fetch_device_configuration(self): diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index c3d15aff2fd..0ddd22d3e1b 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -21,11 +21,20 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_PORT, + CONF_SCAN_INTERVAL, CONF_SSL, CONF_USERNAME, ) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv -from .const import CONF_VOLUMES, DEFAULT_PORT, DEFAULT_PORT_SSL, DEFAULT_SSL +from .const import ( + CONF_VOLUMES, + DEFAULT_PORT, + DEFAULT_PORT_SSL, + DEFAULT_SCAN_INTERVAL, + DEFAULT_SSL, +) from .const import DOMAIN # pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -61,6 +70,12 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return SynologyDSMOptionsFlowHandler(config_entry) + def __init__(self): """Initialize the synology_dsm config flow.""" self.saved_user_input = {} @@ -212,6 +227,31 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return mac in existing_macs +class SynologyDSMOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + data_schema = vol.Schema( + { + vol.Optional( + CONF_SCAN_INTERVAL, + default=self.config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), + ): cv.positive_int + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) + + def _login_and_fetch_syno_info(api, otp_code): """Login to the NAS and fetch basic data.""" # These do i/o diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index b3c9f66c8da..e0a166e908b 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -9,10 +9,17 @@ from homeassistant.const import ( DOMAIN = "synology_dsm" BASE_NAME = "Synology" +# Entry keys +SYNO_API = "syno_api" +UNDO_UPDATE_LISTENER = "undo_update_listener" + +# Configuration CONF_VOLUMES = "volumes" DEFAULT_SSL = True DEFAULT_PORT = 5000 DEFAULT_PORT_SSL = 5001 +# Options +DEFAULT_SCAN_INTERVAL = 15 # min UTILISATION_SENSORS = { "cpu_other_load": ["CPU Load (Other)", UNIT_PERCENTAGE, "mdi:chip"], diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 87c1ba128c6..e776f5e97e3 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -22,6 +22,7 @@ from .const import ( DOMAIN, STORAGE_DISK_SENSORS, STORAGE_VOL_SENSORS, + SYNO_API, TEMP_SENSORS_KEYS, UTILISATION_SENSORS, ) @@ -34,7 +35,7 @@ async def async_setup_entry( ) -> None: """Set up the Synology NAS Sensor.""" - api = hass.data[DOMAIN][entry.unique_id] + api = hass.data[DOMAIN][entry.unique_id][SYNO_API] sensors = [ SynoNasUtilSensor(api, sensor_type, UTILISATION_SENSORS[sensor_type]) diff --git a/homeassistant/components/synology_dsm/strings.json b/homeassistant/components/synology_dsm/strings.json index c58b0d819ea..7c81b1dae28 100644 --- a/homeassistant/components/synology_dsm/strings.json +++ b/homeassistant/components/synology_dsm/strings.json @@ -37,5 +37,14 @@ "unknown": "Unknown error: please check logs to get more details" }, "abort": { "already_configured": "Host already configured" } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Minutes between scans" + } + } + } } } diff --git a/homeassistant/components/synology_dsm/translations/en.json b/homeassistant/components/synology_dsm/translations/en.json index 60a17d703de..3a52b0dc7d4 100644 --- a/homeassistant/components/synology_dsm/translations/en.json +++ b/homeassistant/components/synology_dsm/translations/en.json @@ -20,7 +20,6 @@ }, "link": { "data": { - "api_version": "DSM version", "password": "Password", "port": "Port (Optional)", "ssl": "Use SSL/TLS to connect to your NAS", @@ -31,7 +30,6 @@ }, "user": { "data": { - "api_version": "DSM version", "host": "Host", "password": "Password", "port": "Port (Optional)", @@ -41,5 +39,14 @@ "title": "Synology DSM" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Minutes between scans" + } + } + } } } \ No newline at end of file diff --git a/tests/components/synology_dsm/conftest.py b/tests/components/synology_dsm/conftest.py index 67c3cab659e..41bd42a98b3 100644 --- a/tests/components/synology_dsm/conftest.py +++ b/tests/components/synology_dsm/conftest.py @@ -4,8 +4,8 @@ import pytest from tests.async_mock import patch -@pytest.fixture(name="dsm_bypass_setup", autouse=True) -def dsm_bypass_setup_fixture(): +@pytest.fixture(name="bypass_setup", autouse=True) +def bypass_setup_fixture(): """Mock component setup.""" with patch( "homeassistant.components.synology_dsm.async_setup_entry", return_value=True diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 66fc943920e..a339f015584 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -17,6 +17,7 @@ from homeassistant.components.synology_dsm.const import ( CONF_VOLUMES, DEFAULT_PORT, DEFAULT_PORT_SSL, + DEFAULT_SCAN_INTERVAL, DEFAULT_SSL, DOMAIN, ) @@ -27,6 +28,7 @@ from homeassistant.const import ( CONF_MAC, CONF_PASSWORD, CONF_PORT, + CONF_SCAN_INTERVAL, CONF_SSL, CONF_USERNAME, ) @@ -393,3 +395,40 @@ async def test_form_ssdp(hass: HomeAssistantType, service: MagicMock): assert result["data"].get("device_token") is None assert result["data"].get(CONF_DISKS) is None assert result["data"].get(CONF_VOLUMES) is None + + +async def test_options_flow(hass: HomeAssistantType, service: MagicMock): + """Test config flow options.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_MAC: MACS, + }, + unique_id=SERIAL, + ) + config_entry.add_to_hass(hass) + + assert config_entry.options == {} + + 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" + + # Scan interval + # Default + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options[CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL + + # Manual + result = await hass.config_entries.options.async_init(config_entry.entry_id) + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_SCAN_INTERVAL: 2}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options[CONF_SCAN_INTERVAL] == 2