1 """Config flow to configure the Synology DSM integration."""
3 from __future__
import annotations
5 from collections.abc
import Mapping
6 from ipaddress
import ip_address
as ip
8 from typing
import Any, cast
9 from urllib.parse
import urlparse
11 from synology_dsm
import SynologyDSM
12 from synology_dsm.exceptions
import (
14 SynologyDSMLogin2SAFailedException,
15 SynologyDSMLogin2SARequiredException,
16 SynologyDSMLoginInvalidException,
17 SynologyDSMRequestException,
19 import voluptuous
as vol
49 CONF_SNAPSHOT_QUALITY,
53 DEFAULT_SCAN_INTERVAL,
54 DEFAULT_SNAPSHOT_QUALITY,
61 _LOGGER = logging.getLogger(__name__)
63 CONF_OTP_CODE =
"otp_code"
65 HTTP_SUFFIX =
"._http._tcp.local."
75 vol.Required(CONF_USERNAME): str,
76 vol.Required(CONF_PASSWORD): str,
82 user_schema: VolDictType = {
83 vol.Required(CONF_HOST, default=user_input.get(CONF_HOST,
"")): str,
87 return vol.Schema(user_schema)
92 vol.Required(CONF_USERNAME, default=schema_input.get(CONF_USERNAME,
"")): str,
93 vol.Required(CONF_PASSWORD, default=schema_input.get(CONF_PASSWORD,
"")): str,
94 vol.Optional(CONF_PORT, default=schema_input.get(CONF_PORT,
"")): str,
96 CONF_SSL, default=schema_input.get(CONF_SSL, DEFAULT_USE_SSL)
100 default=schema_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL),
106 """Format a mac address to the format used by Synology DSM."""
107 return mac.replace(
":",
"").replace(
"-",
"").upper()
111 """Handle a config flow."""
118 config_entry: ConfigEntry,
119 ) -> SynologyDSMOptionsFlowHandler:
120 """Get the options flow for this handler."""
124 """Initialize the synology_dsm config flow."""
127 self.
reauth_confreauth_conf: Mapping[str, Any] = {}
128 self.reauth_reason: str |
None =
None
133 user_input: dict[str, Any] |
None =
None,
134 errors: dict[str, str] |
None =
None,
135 ) -> ConfigFlowResult:
136 """Show the setup form to the user."""
140 description_placeholders = {}
143 if step_id ==
"link":
147 elif step_id ==
"reauth_confirm":
149 elif step_id ==
"user":
154 data_schema=data_schema,
156 description_placeholders=description_placeholders,
160 self, user_input: dict[str, Any], step_id: str
161 ) -> ConfigFlowResult:
162 """Process user input and create new or update existing config entry."""
163 host = user_input[CONF_HOST]
164 port = user_input.get(CONF_PORT)
165 username = user_input[CONF_USERNAME]
166 password = user_input[CONF_PASSWORD]
167 use_ssl = user_input.get(CONF_SSL, DEFAULT_USE_SSL)
168 verify_ssl = user_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL)
169 otp_code = user_input.get(CONF_OTP_CODE)
170 friendly_name = user_input.get(CONF_NAME)
174 port = DEFAULT_PORT_SSL
180 session, host, port, username, password, use_ssl, timeout=DEFAULT_TIMEOUT
186 except SynologyDSMLogin2SARequiredException:
188 except SynologyDSMLogin2SAFailedException:
189 errors[CONF_OTP_CODE] =
"otp_failed"
190 user_input[CONF_OTP_CODE] =
None
191 return await self.
async_step_2saasync_step_2sa(user_input, errors)
192 except SynologyDSMLoginInvalidException
as ex:
194 errors[CONF_USERNAME] =
"invalid_auth"
195 except SynologyDSMRequestException
as ex:
197 errors[CONF_HOST] =
"cannot_connect"
198 except SynologyDSMException
as ex:
200 errors[
"base"] =
"unknown"
202 errors[
"base"] =
"missing_data"
205 return self.
_show_form_show_form(step_id, user_input, errors)
208 existing_entry = await self.
async_set_unique_idasync_set_unique_id(serial, raise_on_progress=
False)
214 CONF_VERIFY_SSL: verify_ssl,
215 CONF_USERNAME: username,
216 CONF_PASSWORD: password,
217 CONF_MAC: api.network.macs,
220 config_data[CONF_DEVICE_TOKEN] = api.device_token
221 if user_input.get(CONF_DISKS):
222 config_data[CONF_DISKS] = user_input[CONF_DISKS]
223 if user_input.get(CONF_VOLUMES):
224 config_data[CONF_VOLUMES] = user_input[CONF_VOLUMES]
228 "reauth_successful" if self.
reauth_confreauth_conf
else "reconfigure_successful"
231 existing_entry, data=config_data, reason=reason
237 self, user_input: dict[str, Any] |
None =
None
238 ) -> ConfigFlowResult:
239 """Handle a flow initiated by the user."""
246 self, discovery_info: zeroconf.ZeroconfServiceInfo
247 ) -> ConfigFlowResult:
248 """Handle a discovered synology_dsm via zeroconf."""
251 for mac
in discovery_info.properties.get(
"mac_address",
"").split(
"|")
254 if not discovered_macs:
256 host = discovery_info.host
257 friendly_name = discovery_info.name.removesuffix(HTTP_SUFFIX)
261 self, discovery_info: ssdp.SsdpServiceInfo
262 ) -> ConfigFlowResult:
263 """Handle a discovered synology_dsm via ssdp."""
264 parsed_url = urlparse(discovery_info.ssdp_location)
265 upnp_friendly_name: str = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME]
266 friendly_name = upnp_friendly_name.split(
"(", 1)[0].strip()
267 mac_address = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL]
271 host = cast(str, parsed_url.hostname)
275 self, host: str, friendly_name: str, discovered_macs: list[str]
276 ) -> ConfigFlowResult:
277 """Handle a discovered synology_dsm via zeroconf or ssdp."""
278 existing_entry =
None
279 for discovered_mac
in discovered_macs:
287 and is_ip(existing_entry.data[CONF_HOST])
289 and existing_entry.data[CONF_HOST] != host
290 and ip(existing_entry.data[CONF_HOST]).version == ip(host).version
293 "Update host from '%s' to '%s' for NAS '%s' via discovery",
294 existing_entry.data[CONF_HOST],
296 existing_entry.unique_id,
298 self.hass.config_entries.async_update_entry(
300 data={**existing_entry.data, CONF_HOST: host},
308 CONF_NAME: friendly_name,
311 self.context[
"title_placeholders"] = self.
discovered_confdiscovered_conf
315 self, user_input: dict[str, Any] |
None =
None
316 ) -> ConfigFlowResult:
317 """Link a config entry from discovery."""
325 self, entry_data: Mapping[str, Any]
326 ) -> ConfigFlowResult:
327 """Perform reauth upon an API authentication error."""
330 **self.context[
"title_placeholders"],
331 CONF_HOST: entry_data[CONF_HOST],
333 self.context[
"title_placeholders"] = placeholders
338 self, user_input: dict[str, Any] |
None =
None
339 ) -> ConfigFlowResult:
340 """Perform reauth confirm upon an API authentication error."""
341 step =
"reauth_confirm"
344 user_input = {**self.
reauth_confreauth_conf, **user_input}
348 self, user_input: dict[str, Any], errors: dict[str, str] |
None =
None
349 ) -> ConfigFlowResult:
350 """Enter 2SA code to anthenticate."""
354 if not user_input.get(CONF_OTP_CODE):
357 data_schema=vol.Schema({vol.Required(CONF_OTP_CODE): str}),
367 """See if we already have a configured NAS with this MAC address."""
369 if discovered_mac
in [
377 """Handle a option flow."""
380 self, user_input: dict[str, Any] |
None =
None
381 ) -> ConfigFlowResult:
382 """Handle options flow."""
383 if user_input
is not None:
386 data_schema = vol.Schema(
391 CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
395 CONF_SNAPSHOT_QUALITY,
397 CONF_SNAPSHOT_QUALITY, DEFAULT_SNAPSHOT_QUALITY
399 ): vol.All(vol.Coerce(int), vol.Range(min=0, max=2)),
402 return self.
async_show_formasync_show_form(step_id=
"init", data_schema=data_schema)
406 """Login to the NAS and fetch basic data."""
408 await api.login(otp_code)
409 await api.utilisation.update()
410 await api.storage.update()
411 await api.network.update()
414 not api.information.serial
415 or api.utilisation.cpu_user_load
is None
416 or not api.storage.volumes_ids
417 or not api.network.macs
421 return api.information.serial
425 """Error to indicate we get invalid data from the nas."""
SynologyDSMOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
ConfigFlowResult _async_from_discovery(self, str host, str friendly_name, list[str] discovered_macs)
ConfigFlowResult async_step_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
ConfigFlowResult async_validate_input_create_entry(self, dict[str, Any] user_input, str step_id)
ConfigFlowResult _show_form(self, str step_id, dict[str, Any]|None user_input=None, dict[str, str]|None errors=None)
ConfigFlowResult async_step_link(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
ConfigEntry|None _async_get_existing_entry(self, str discovered_mac)
ConfigFlowResult async_step_ssdp(self, ssdp.SsdpServiceInfo discovery_info)
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_2sa(self, dict[str, Any] user_input, dict[str, str]|None errors=None)
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
None _abort_if_unique_id_configured(self, dict[str, Any]|None updates=None, bool reload_on_update=True, *str error="already_configured")
ConfigEntry|None async_set_unique_id(self, str|None unique_id=None, *bool raise_on_progress=True)
ConfigFlowResult async_create_entry(self, *str title, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None, Mapping[str, Any]|None options=None)
list[ConfigEntry] _async_current_entries(self, bool|None include_ignore=None)
ConfigFlowResult async_update_reload_and_abort(self, ConfigEntry entry, *str|None|UndefinedType unique_id=UNDEFINED, str|UndefinedType title=UNDEFINED, Mapping[str, Any]|UndefinedType data=UNDEFINED, Mapping[str, Any]|UndefinedType data_updates=UNDEFINED, Mapping[str, Any]|UndefinedType options=UNDEFINED, str|UndefinedType reason=UNDEFINED, bool reload_even_if_entry_is_unchanged=True)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
ConfigFlowResult async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
ConfigEntry config_entry(self)
None config_entry(self, ConfigEntry value)
_FlowResultT async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
_FlowResultT async_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
str format_synology_mac(str mac)
vol.Schema _discovery_schema_with_defaults(DiscoveryInfoType discovery_info)
str _login_and_fetch_syno_info(SynologyDSM api, str|None otp_code)
VolDictType _ordered_shared_schema(dict[str, Any] schema_input)
vol.Schema _user_schema_with_defaults(dict[str, Any] user_input)
vol.Schema _reauth_schema()
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)