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
This commit is contained in:
Quentame 2020-05-07 00:15:49 +02:00 committed by GitHub
parent f1ecac92df
commit 81651b0b25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 156 additions and 47 deletions

View File

@ -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):

View File

@ -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

View File

@ -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"],

View File

@ -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])

View File

@ -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"
}
}
}
}
}

View File

@ -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"
}
}
}
}
}

View File

@ -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

View File

@ -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