mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
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:
parent
f1ecac92df
commit
81651b0b25
@ -14,6 +14,7 @@ from homeassistant.const import (
|
|||||||
CONF_MAC,
|
CONF_MAC,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
|
CONF_SCAN_INTERVAL,
|
||||||
CONF_SSL,
|
CONF_SSL,
|
||||||
CONF_USERNAME,
|
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.event import async_track_time_interval
|
||||||
from homeassistant.helpers.typing import HomeAssistantType
|
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(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
@ -41,8 +49,6 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(minutes=15)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Set up Synology DSM sensors from legacy config file."""
|
"""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):
|
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
|
||||||
"""Set up Synology DSM sensors."""
|
"""Set up Synology DSM sensors."""
|
||||||
host = entry.data[CONF_HOST]
|
api = SynoApi(hass, entry)
|
||||||
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)
|
|
||||||
|
|
||||||
await api.async_setup()
|
await api.async_setup()
|
||||||
|
|
||||||
|
undo_listener = entry.add_update_listener(_async_update_listener)
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})
|
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
|
# For SSDP compat
|
||||||
if not entry.data.get(CONF_MAC):
|
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):
|
async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
|
||||||
"""Unload Synology DSM sensors."""
|
"""Unload Synology DSM sensors."""
|
||||||
api = hass.data[DOMAIN][entry.unique_id]
|
unload_ok = await hass.config_entries.async_forward_entry_unload(entry, "sensor")
|
||||||
await api.async_unload()
|
|
||||||
return 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 SynoApi:
|
||||||
"""Class to interface with Synology DSM API."""
|
"""Class to interface with Synology DSM API."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, hass: HomeAssistantType, entry: ConfigEntry):
|
||||||
self,
|
|
||||||
hass: HomeAssistantType,
|
|
||||||
host: str,
|
|
||||||
port: int,
|
|
||||||
username: str,
|
|
||||||
password: str,
|
|
||||||
temp_unit: str,
|
|
||||||
use_ssl: bool,
|
|
||||||
device_token: str,
|
|
||||||
):
|
|
||||||
"""Initialize the API wrapper class."""
|
"""Initialize the API wrapper class."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._host = host
|
self._entry = entry
|
||||||
self._port = port
|
|
||||||
self._username = username
|
self.temp_unit = hass.config.units.temperature_unit
|
||||||
self._password = password
|
|
||||||
self._use_ssl = use_ssl
|
|
||||||
self._device_token = device_token
|
|
||||||
self.temp_unit = temp_unit
|
|
||||||
|
|
||||||
self.dsm: SynologyDSM = None
|
self.dsm: SynologyDSM = None
|
||||||
self.information: SynoDSMInformation = None
|
self.information: SynoDSMInformation = None
|
||||||
@ -138,19 +138,25 @@ class SynoApi:
|
|||||||
async def async_setup(self):
|
async def async_setup(self):
|
||||||
"""Start interacting with the NAS."""
|
"""Start interacting with the NAS."""
|
||||||
self.dsm = SynologyDSM(
|
self.dsm = SynologyDSM(
|
||||||
self._host,
|
self._entry.data[CONF_HOST],
|
||||||
self._port,
|
self._entry.data[CONF_PORT],
|
||||||
self._username,
|
self._entry.data[CONF_USERNAME],
|
||||||
self._password,
|
self._entry.data[CONF_PASSWORD],
|
||||||
self._use_ssl,
|
self._entry.data[CONF_SSL],
|
||||||
device_token=self._device_token,
|
device_token=self._entry.data.get("device_token"),
|
||||||
)
|
)
|
||||||
|
|
||||||
await self._hass.async_add_executor_job(self._fetch_device_configuration)
|
await self._hass.async_add_executor_job(self._fetch_device_configuration)
|
||||||
await self.update()
|
await self.update()
|
||||||
|
|
||||||
self._unsub_dispatcher = async_track_time_interval(
|
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):
|
def _fetch_device_configuration(self):
|
||||||
|
@ -21,11 +21,20 @@ from homeassistant.const import (
|
|||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
|
CONF_SCAN_INTERVAL,
|
||||||
CONF_SSL,
|
CONF_SSL,
|
||||||
CONF_USERNAME,
|
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
|
from .const import DOMAIN # pylint: disable=unused-import
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -61,6 +70,12 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
VERSION = 1
|
VERSION = 1
|
||||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
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):
|
def __init__(self):
|
||||||
"""Initialize the synology_dsm config flow."""
|
"""Initialize the synology_dsm config flow."""
|
||||||
self.saved_user_input = {}
|
self.saved_user_input = {}
|
||||||
@ -212,6 +227,31 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
return mac in existing_macs
|
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):
|
def _login_and_fetch_syno_info(api, otp_code):
|
||||||
"""Login to the NAS and fetch basic data."""
|
"""Login to the NAS and fetch basic data."""
|
||||||
# These do i/o
|
# These do i/o
|
||||||
|
@ -9,10 +9,17 @@ from homeassistant.const import (
|
|||||||
DOMAIN = "synology_dsm"
|
DOMAIN = "synology_dsm"
|
||||||
BASE_NAME = "Synology"
|
BASE_NAME = "Synology"
|
||||||
|
|
||||||
|
# Entry keys
|
||||||
|
SYNO_API = "syno_api"
|
||||||
|
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
||||||
|
|
||||||
|
# Configuration
|
||||||
CONF_VOLUMES = "volumes"
|
CONF_VOLUMES = "volumes"
|
||||||
DEFAULT_SSL = True
|
DEFAULT_SSL = True
|
||||||
DEFAULT_PORT = 5000
|
DEFAULT_PORT = 5000
|
||||||
DEFAULT_PORT_SSL = 5001
|
DEFAULT_PORT_SSL = 5001
|
||||||
|
# Options
|
||||||
|
DEFAULT_SCAN_INTERVAL = 15 # min
|
||||||
|
|
||||||
UTILISATION_SENSORS = {
|
UTILISATION_SENSORS = {
|
||||||
"cpu_other_load": ["CPU Load (Other)", UNIT_PERCENTAGE, "mdi:chip"],
|
"cpu_other_load": ["CPU Load (Other)", UNIT_PERCENTAGE, "mdi:chip"],
|
||||||
|
@ -22,6 +22,7 @@ from .const import (
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
STORAGE_DISK_SENSORS,
|
STORAGE_DISK_SENSORS,
|
||||||
STORAGE_VOL_SENSORS,
|
STORAGE_VOL_SENSORS,
|
||||||
|
SYNO_API,
|
||||||
TEMP_SENSORS_KEYS,
|
TEMP_SENSORS_KEYS,
|
||||||
UTILISATION_SENSORS,
|
UTILISATION_SENSORS,
|
||||||
)
|
)
|
||||||
@ -34,7 +35,7 @@ async def async_setup_entry(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Synology NAS Sensor."""
|
"""Set up the Synology NAS Sensor."""
|
||||||
|
|
||||||
api = hass.data[DOMAIN][entry.unique_id]
|
api = hass.data[DOMAIN][entry.unique_id][SYNO_API]
|
||||||
|
|
||||||
sensors = [
|
sensors = [
|
||||||
SynoNasUtilSensor(api, sensor_type, UTILISATION_SENSORS[sensor_type])
|
SynoNasUtilSensor(api, sensor_type, UTILISATION_SENSORS[sensor_type])
|
||||||
|
@ -37,5 +37,14 @@
|
|||||||
"unknown": "Unknown error: please check logs to get more details"
|
"unknown": "Unknown error: please check logs to get more details"
|
||||||
},
|
},
|
||||||
"abort": { "already_configured": "Host already configured" }
|
"abort": { "already_configured": "Host already configured" }
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"scan_interval": "Minutes between scans"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
},
|
},
|
||||||
"link": {
|
"link": {
|
||||||
"data": {
|
"data": {
|
||||||
"api_version": "DSM version",
|
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"port": "Port (Optional)",
|
"port": "Port (Optional)",
|
||||||
"ssl": "Use SSL/TLS to connect to your NAS",
|
"ssl": "Use SSL/TLS to connect to your NAS",
|
||||||
@ -31,7 +30,6 @@
|
|||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"api_version": "DSM version",
|
|
||||||
"host": "Host",
|
"host": "Host",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"port": "Port (Optional)",
|
"port": "Port (Optional)",
|
||||||
@ -41,5 +39,14 @@
|
|||||||
"title": "Synology DSM"
|
"title": "Synology DSM"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"scan_interval": "Minutes between scans"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,8 +4,8 @@ import pytest
|
|||||||
from tests.async_mock import patch
|
from tests.async_mock import patch
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="dsm_bypass_setup", autouse=True)
|
@pytest.fixture(name="bypass_setup", autouse=True)
|
||||||
def dsm_bypass_setup_fixture():
|
def bypass_setup_fixture():
|
||||||
"""Mock component setup."""
|
"""Mock component setup."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.synology_dsm.async_setup_entry", return_value=True
|
"homeassistant.components.synology_dsm.async_setup_entry", return_value=True
|
||||||
|
@ -17,6 +17,7 @@ from homeassistant.components.synology_dsm.const import (
|
|||||||
CONF_VOLUMES,
|
CONF_VOLUMES,
|
||||||
DEFAULT_PORT,
|
DEFAULT_PORT,
|
||||||
DEFAULT_PORT_SSL,
|
DEFAULT_PORT_SSL,
|
||||||
|
DEFAULT_SCAN_INTERVAL,
|
||||||
DEFAULT_SSL,
|
DEFAULT_SSL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
@ -27,6 +28,7 @@ from homeassistant.const import (
|
|||||||
CONF_MAC,
|
CONF_MAC,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
|
CONF_SCAN_INTERVAL,
|
||||||
CONF_SSL,
|
CONF_SSL,
|
||||||
CONF_USERNAME,
|
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("device_token") is None
|
||||||
assert result["data"].get(CONF_DISKS) is None
|
assert result["data"].get(CONF_DISKS) is None
|
||||||
assert result["data"].get(CONF_VOLUMES) 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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user