diff --git a/.coveragerc b/.coveragerc index a92c092e39f24..69ffb22258001 100644 --- a/.coveragerc +++ b/.coveragerc @@ -726,7 +726,9 @@ omit = homeassistant/components/synology_chat/notify.py homeassistant/components/synology_dsm/__init__.py homeassistant/components/synology_dsm/sensor.py + homeassistant/components/synology_srm/__init__.py homeassistant/components/synology_srm/device_tracker.py + homeassistant/components/synology_srm/router.py homeassistant/components/syslog/notify.py homeassistant/components/systemmonitor/sensor.py homeassistant/components/tado/* diff --git a/homeassistant/components/synology_srm/__init__.py b/homeassistant/components/synology_srm/__init__.py index 72fd0c178214e..1d2e008701419 100644 --- a/homeassistant/components/synology_srm/__init__.py +++ b/homeassistant/components/synology_srm/__init__.py @@ -1 +1,84 @@ -"""The Synology SRM component.""" +"""The Synology SRM integration.""" +import asyncio + +from synology_srm import Client as SynologyClient +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .router import SynologySrmRouter + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) +PLATFORMS = ["device_tracker"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Synology SRM component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Synology SRM from a config entry.""" + client = get_srm_client_from_user_data(entry.data) + router = SynologySrmRouter(hass, client) + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.unique_id] = router + + await router.async_setup() + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ) + if unload_ok: + router = hass.data[DOMAIN].pop(entry.unique_id) + await router.async_unload() + + return unload_ok + + +def get_srm_client_from_user_data(data) -> SynologyClient: + """Get the Synology SRM client from user data.""" + client = SynologyClient( + host=data[CONF_HOST], + port=data[CONF_PORT], + https=data[CONF_SSL], + username=data[CONF_USERNAME], + password=data[CONF_PASSWORD], + ) + + if not data[CONF_VERIFY_SSL]: + client.http.disable_https_verify() + + return client + + +def fetch_srm_device_id(client: SynologyClient): + """Fetch the Synology SRM device ID from the user.""" + info = client.mesh.get_system_info() + return info["nodes"][0]["unique"] diff --git a/homeassistant/components/synology_srm/config_flow.py b/homeassistant/components/synology_srm/config_flow.py new file mode 100644 index 0000000000000..fcc730fd317c2 --- /dev/null +++ b/homeassistant/components/synology_srm/config_flow.py @@ -0,0 +1,86 @@ +"""Config flow for Synology SRM integration.""" +import logging + +import requests +from synology_srm.http import SynologyError, SynologyHttpException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) + +from . import fetch_srm_device_id, get_srm_client_from_user_data +from .const import DEFAULT_PORT, DEFAULT_SSL, DEFAULT_USERNAME, DEFAULT_VERIFY_SSL +from .const import DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class SynologySrmFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Synology SRM.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + client = get_srm_client_from_user_data(user_input) + device_id = await self.hass.async_add_executor_job( + fetch_srm_device_id, client + ) + + # Check if the device has already been configured + await self.async_set_unique_id(device_id, raise_on_progress=False) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=device_id, data=user_input) + except (SynologyHttpException, requests.exceptions.ConnectionError) as ex: + errors["base"] = "cannot_connect" + _LOGGER.exception(ex) + except SynologyError as error: + if error.code >= 400: + errors["base"] = "invalid_auth" + else: + errors["base"] = "cannot_connect" + _LOGGER.exception(error) + except Exception as ex: # pylint: disable=broad-except + errors["base"] = "unknown" + _LOGGER.exception(ex) + else: + user_input = {} + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str, + vol.Optional( + CONF_PORT, default=user_input.get(CONF_PORT, DEFAULT_PORT) + ): int, + vol.Required( + CONF_USERNAME, + default=user_input.get(CONF_USERNAME, DEFAULT_USERNAME), + ): str, + vol.Required( + CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") + ): str, + vol.Optional( + CONF_SSL, default=user_input.get(CONF_SSL, DEFAULT_SSL) + ): bool, + vol.Optional( + CONF_VERIFY_SSL, + default=user_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL), + ): bool, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/synology_srm/const.py b/homeassistant/components/synology_srm/const.py new file mode 100644 index 0000000000000..ea50281a36448 --- /dev/null +++ b/homeassistant/components/synology_srm/const.py @@ -0,0 +1,47 @@ +"""Constants for the Synology SRM integration.""" + +DOMAIN = "synology_srm" + +DEFAULT_USERNAME = "admin" +DEFAULT_PORT = 8001 +DEFAULT_SSL = True +DEFAULT_VERIFY_SSL = False + +DEVICE_ICON = { + "nas": "mdi:nas", + "notebook": "mdi:laptop", + "computer": "mdi:desktop-mac", + "tv": "mdi:television", + "printer": "mdi:printer", + "tablet": "mdi:tablet-ipad", + "gamebox": "mdi:gamepad-variant", + "phone": "mdi:cellphone", +} + +DEVICE_ATTRIBUTE_ALIAS = { + "band": None, + "connection": None, + "current_rate": None, + "dev_type": None, + "hostname": None, + "ip6_addr": None, + "ip_addr": None, + "is_baned": "is_banned", + "is_beamforming_on": None, + "is_guest": None, + "is_high_qos": None, + "is_low_qos": None, + "is_manual_dev_type": None, + "is_manual_hostname": None, + "is_online": None, + "is_parental_controled": "is_parental_controlled", + "is_qos": None, + "is_wireless": None, + "mac": None, + "max_rate": None, + "mesh_node_id": None, + "rate_quality": None, + "signalstrength": "signal_strength", + "transferRXRate": "transfer_rx_rate", + "transferTXRate": "transfer_tx_rate", +} diff --git a/homeassistant/components/synology_srm/device_tracker.py b/homeassistant/components/synology_srm/device_tracker.py index 55f455ad7ccf9..c11789156ea4e 100644 --- a/homeassistant/components/synology_srm/device_tracker.py +++ b/homeassistant/components/synology_srm/device_tracker.py @@ -1,143 +1,160 @@ """Device tracker for Synology SRM routers.""" import logging -import synology_srm -import voluptuous as vol - -from homeassistant.components.device_tracker import ( - DOMAIN, - PLATFORM_SCHEMA, - DeviceScanner, -) -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_SSL, - CONF_USERNAME, - CONF_VERIFY_SSL, -) -import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DEVICE_ATTRIBUTE_ALIAS, DEVICE_ICON, DOMAIN _LOGGER = logging.getLogger(__name__) -DEFAULT_USERNAME = "admin" -DEFAULT_PORT = 8001 -DEFAULT_SSL = True -DEFAULT_VERIFY_SSL = False - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - } -) - -ATTRIBUTE_ALIAS = { - "band": None, - "connection": None, - "current_rate": None, - "dev_type": None, - "hostname": None, - "ip6_addr": None, - "ip_addr": None, - "is_baned": "is_banned", - "is_beamforming_on": None, - "is_guest": None, - "is_high_qos": None, - "is_low_qos": None, - "is_manual_dev_type": None, - "is_manual_hostname": None, - "is_online": None, - "is_parental_controled": "is_parental_controlled", - "is_qos": None, - "is_wireless": None, - "mac": None, - "max_rate": None, - "mesh_node_id": None, - "rate_quality": None, - "signalstrength": "signal_strength", - "transferRXRate": "transfer_rx_rate", - "transferTXRate": "transfer_tx_rate", -} - - -def get_scanner(hass, config): - """Validate the configuration and return Synology SRM scanner.""" - scanner = SynologySrmDeviceScanner(config[DOMAIN]) - - return scanner if scanner.success_init else None - - -class SynologySrmDeviceScanner(DeviceScanner): - """This class scans for devices connected to a Synology SRM router.""" - - def __init__(self, config): - """Initialize the scanner.""" - - self.client = synology_srm.Client( - host=config[CONF_HOST], - port=config[CONF_PORT], - username=config[CONF_USERNAME], - password=config[CONF_PASSWORD], - https=config[CONF_SSL], - ) - if not config[CONF_VERIFY_SSL]: - self.client.http.disable_https_verify() +async def async_setup_entry(hass, entry, async_add_entities): + """Set up device tracker for the Synology SRM component.""" + router = hass.data[DOMAIN][entry.unique_id] + devices = set() - self.devices = [] - self.success_init = self._update_info() + @callback + def async_add_new_devices(): + """Add new devices from the router to Hass.""" + async_add_new_entities(router, async_add_entities, devices) - _LOGGER.info("Synology SRM scanner initialized") + router.listeners.append( + async_dispatcher_connect(hass, router.signal_devices_new, async_add_new_devices) + ) - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() + # Add initial devices + async_add_new_devices() - return [device["mac"] for device in self.devices] - def get_extra_attributes(self, device) -> dict: - """Get the extra attributes of a device.""" - device = next( - (result for result in self.devices if result["mac"] == device), None - ) +@callback +def async_add_new_entities(router, async_add_entities, devices): + """Add only new devices entities from the router.""" + new_devices = [] + + for device in router.devices: + if device["mac"] in devices: + continue + + new_devices.append(SynologySrmEntity(router, device)) + devices.add(device["mac"]) + + if new_devices: + async_add_entities(new_devices, True) + + +class SynologySrmEntity(ScannerEntity): + """Representation of a device connected to the Synology SRM router.""" + + def __init__(self, router, device): + """Initialize a Synology SRM device.""" + self.router = router + self.device = device + + self.mac = device["mac"] + + self.update_dispatcher = None + self.delete_dispatcher = None + + def _get(self, parameter=None, default=None): + """Get internal parameter stored in the router.""" + if not parameter: + return self.device + + if not self.device or parameter not in self.device: + return default + + return self.device[parameter] + + @property + def unique_id(self): + """Return a unique identifier (the MAC address).""" + return self._get("mac") + + @property + def name(self): + """Return the name of the entity.""" + return self._get("hostname") + + @property + def is_connected(self): + """Return true if the device is connected to the network.""" + return self._get("is_online") + + @property + def source_type(self): + """Return the source type of the entity.""" + return SOURCE_TYPE_ROUTER + + @property + def device_state_attributes(self): + """Return the state attributes.""" filtered_attributes = {} - if not device: - return filtered_attributes - for attribute, alias in ATTRIBUTE_ALIAS.items(): + device = self._get() + + for attribute, alias in DEVICE_ATTRIBUTE_ALIAS.items(): value = device.get(attribute) if value is None: continue attr = alias or attribute filtered_attributes[attr] = value - return filtered_attributes - def get_device_name(self, device): - """Return the name of the given device or None if we don't know.""" - filter_named = [ - result["hostname"] for result in self.devices if result["mac"] == device - ] - - if filter_named: - return filter_named[0] - - return None - - def _update_info(self): - """Check the router for connected devices.""" - _LOGGER.debug("Scanning for connected devices") + return filtered_attributes - try: - self.devices = self.client.core.get_network_nsm_device({"is_online": True}) - except synology_srm.http.SynologyException as ex: - _LOGGER.error("Error with the Synology SRM: %s", ex) - return False + @property + def icon(self): + """Return the icon to use in the frontend.""" + # 1 - Synology device types + device_type = self._get("dev_type") + if device_type in DEVICE_ICON: + return DEVICE_ICON[device_type] + + # 2 - Wi-Fi signal strength + if self._get("connection") == "wifi": + strength = self._get("signalstrength", 100) + thresholds = [70, 50, 30, 0] + for idx, threshold in enumerate(thresholds): + if strength >= threshold: + return "mdi:wifi-strength-{}".format(len(thresholds) - idx) + + # Fallback to a classical icon + return "mdi:ethernet" + + @property + def should_poll(self): + """No need to poll. Updates are managed by the router.""" + return False + + async def async_update(self): + """Update the current device.""" + device = self.router.get_device(self.mac) + if device: + self.device = device + + async def async_added_to_hass(self): + """Register state update/delete callback.""" + self.update_dispatcher = async_dispatcher_connect( + self.hass, self.router.signal_devices_update, self._async_update_callback + ) - _LOGGER.debug("Found %d device(s) connected to the router", len(self.devices)) + self.delete_dispatcher = async_dispatcher_connect( + self.hass, self.router.signal_devices_delete, self._async_delete_callback + ) - return True + async def _async_update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + + async def _async_delete_callback(self): + """Remove this entity.""" + if not self.router.get_device(self.mac): + # Remove this entity if parameters are not + # present in the router anymore + self.hass.async_create_task(self.async_remove()) + + async def async_will_remove_from_hass(self): + """Call when entity will be removed from hass.""" + self.update_dispatcher() + self.delete_dispatcher() diff --git a/homeassistant/components/synology_srm/manifest.json b/homeassistant/components/synology_srm/manifest.json index 798d7e7ef82cb..5a64f006d205d 100644 --- a/homeassistant/components/synology_srm/manifest.json +++ b/homeassistant/components/synology_srm/manifest.json @@ -1,6 +1,7 @@ { "domain": "synology_srm", "name": "Synology SRM", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/synology_srm", "requirements": ["synology-srm==0.2.0"], "codeowners": ["@aerialls"] diff --git a/homeassistant/components/synology_srm/router.py b/homeassistant/components/synology_srm/router.py new file mode 100644 index 0000000000000..bb4c53384799e --- /dev/null +++ b/homeassistant/components/synology_srm/router.py @@ -0,0 +1,87 @@ +"""Represent the Synology SRM router and its devices.""" +from datetime import timedelta +import logging + +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval + +from .const import DOMAIN + +SCAN_INTERVAL = timedelta(seconds=60) +_LOGGER = logging.getLogger(__name__) + + +def get_device_from_devices(devices, mac): + """Get a device based on the MAC address from the list of all devices.""" + return next((device for device in devices if device["mac"] == mac), False) + + +class SynologySrmRouter: + """Representation of a Synology SRM router.""" + + def __init__(self, hass, client): + """Initialize a Synology SRM router.""" + self.hass = hass + self.client = client + + self._unsub_interval = None + self.devices = [] + self.listeners = [] + + async def async_setup(self): + """Setups the state and trigger a global update.""" + await self._async_update() + self._unsub_interval = async_track_time_interval( + self.hass, self._async_update, SCAN_INTERVAL + ) + + async def _async_update(self, now=None): + """Update the internal state of the router by querying the API.""" + devices = await self.hass.async_add_executor_job( + self.client.core.get_network_nsm_device + ) + + has_new_devices = False + for device in devices: + if not get_device_from_devices(self.devices, device["mac"]): + has_new_devices = True + + has_deleted_devices = False + for device in self.devices: + if not get_device_from_devices(devices, device["mac"]): + has_deleted_devices = True + + # Save the current state + self.devices = devices + _LOGGER.debug("Found %d device(s)", len(devices)) + + if has_new_devices: + async_dispatcher_send(self.hass, self.signal_devices_new) + + if has_deleted_devices: + async_dispatcher_send(self.hass, self.signal_devices_delete) + + async_dispatcher_send(self.hass, self.signal_devices_update) + + def get_device(self, mac): + """Get a device connected to the router.""" + return get_device_from_devices(self.devices, mac) + + async def async_unload(self): + """Stop interacting with the router and prepare for removal from hass.""" + self._unsub_interval() + + @property + def signal_devices_new(self): + """Specific Synology SRM event to signal new devices.""" + return f"{DOMAIN}-{self.client.http.host}-devices-new" + + @property + def signal_devices_update(self): + """Specific Synology SRM event to signal update on current devices.""" + return f"{DOMAIN}-{self.client.http.host}-devices-update" + + @property + def signal_devices_delete(self): + """Specific Synology SRM event to signal deletion of devices.""" + return f"{DOMAIN}-{self.client.http.host}-devices-delete" diff --git a/homeassistant/components/synology_srm/strings.json b/homeassistant/components/synology_srm/strings.json new file mode 100644 index 0000000000000..51f318c9454bb --- /dev/null +++ b/homeassistant/components/synology_srm/strings.json @@ -0,0 +1,25 @@ +{ + "title": "Synology SRM", + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "port": "[%key:common::config_flow::data::port%]", + "ssl": "Use a secure SSL/TLS connection", + "verify_ssl": "Verify the SSL/TLS certificate" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index c35c0384c4be1..5dd4291f00c4d 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -132,6 +132,7 @@ "spotify", "starline", "synology_dsm", + "synology_srm", "tado", "tellduslive", "tesla", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3960a703459e0..749dd52d4b348 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -816,6 +816,9 @@ stringcase==1.2.0 # homeassistant.components.solarlog sunwatcher==0.2.1 +# homeassistant.components.synology_srm +synology-srm==0.2.0 + # homeassistant.components.tellduslive tellduslive==0.10.10 diff --git a/tests/components/synology_srm/__init__.py b/tests/components/synology_srm/__init__.py new file mode 100644 index 0000000000000..874d8c738dc75 --- /dev/null +++ b/tests/components/synology_srm/__init__.py @@ -0,0 +1 @@ +"""Tests for the Synology SRM integration.""" diff --git a/tests/components/synology_srm/test_config_flow.py b/tests/components/synology_srm/test_config_flow.py new file mode 100644 index 0000000000000..e0b54b26b5d68 --- /dev/null +++ b/tests/components/synology_srm/test_config_flow.py @@ -0,0 +1,141 @@ +"""Test the Synology SRM config flow.""" +from synology_srm.http import SynologyError, SynologyHttpException + +from homeassistant import config_entries, setup +from homeassistant.components.synology_srm.const import ( + DEFAULT_PORT, + DEFAULT_SSL, + DEFAULT_USERNAME, + DEFAULT_VERIFY_SSL, + DOMAIN, +) + +from tests.async_mock import patch + +SYNOLOGY_DEVICE_ID = "synology_irr436p_rt2600ac" + +BAD_PASSWORD_EXCEPTION = SynologyError(400, "No such account or incorrect password") +NO_PERMISSION_EXCEPTION = SynologyError( + 105, "The logged in session does not have permission" +) + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.synology_srm.config_flow.fetch_srm_device_id", + return_value=SYNOLOGY_DEVICE_ID, + ), patch( + "homeassistant.components.synology_srm.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.synology_srm.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.1.1.1", "password": "test-password"}, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == SYNOLOGY_DEVICE_ID + assert result2["data"] == { + "host": "1.1.1.1", + "username": DEFAULT_USERNAME, + "password": "test-password", + "port": DEFAULT_PORT, + "ssl": DEFAULT_SSL, + "verify_ssl": DEFAULT_VERIFY_SSL, + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.synology_srm.config_flow.fetch_srm_device_id", + side_effect=SynologyHttpException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + with patch( + "homeassistant.components.synology_srm.config_flow.fetch_srm_device_id", + side_effect=NO_PERMISSION_EXCEPTION, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_invalid_auth(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.synology_srm.config_flow.fetch_srm_device_id", + side_effect=BAD_PASSWORD_EXCEPTION, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_unknown_exception(hass): + """Test we handle base exception error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.synology_srm.config_flow.fetch_srm_device_id", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"}