From 7c676c0a7d62681d31a7860772cd1b15cab82d32 Mon Sep 17 00:00:00 2001 From: disforw Date: Tue, 27 Jun 2023 11:42:27 -0400 Subject: [PATCH] Add config_flow to QNAP (#80450) * Create config_flow.py * Update __init__.py * Create const.py * Create strings.json * Update sensor.py * Update manifest.json * Update manifest.json * Add device name to entities * Correcting health sensor * Update manifest.json * Adding integration_type * Update sensor.py * Update __init__.py * Enums * Update sensor.py * Removed unused notify_create * Switch to SensorDeviceClass.TEMPERATURE * Update enums * Remove SENSOR_TYPES from const.py * Add SENSOR_TYPES to sensor.py * Removed dependancies * Removed import yaml * Removed entity_registry_enabled_default * Update const.py remove dups * Update manifest.json removed dups * Update __init__.py * Update const.py * Update manifest.json * Update sensor.py * Update sensor.py * Update sensor.py * Update sensor.py * Update sensor.py * Update sensor.py * Update sensor.py remove unused * Update sensor.py add docstring * Update sensor.py add super * Remove FOLDER sensors * Remove VOLUME_NAME * fix cli * fix cli * Add config flow tests * Update requirements_test_all.txt * Update CODEOWNERS * Update homeassistant/components/qnap/config_flow.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/qnap/config_flow.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/qnap/config_flow.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update __init__.py Change unload to walrus Remove async_setup * Update const.py remove PLATFORMS * Update __init__.py add Platform Enum As per epenet * Update __init__.py * Update config_flow.py * Update sensor.py * Update __init__.py ruff * Update config_flow.py Ruff * Update sensor.py * Update const.py remove attrs * Update sensor.py add attrs * Revert tuple for sensor extend * Update sensor.py * Update coordinator.py * Update coordinator.py * Update sensor.py * Update coordinator.py * Update homeassistant/components/qnap/__init__.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/qnap/const.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/qnap/__init__.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update coordinator.py * Update __init__.py * Update coordinator.py * Update sensor.py * Add device_info * Update sensor.py * Update sensor.py self._attr_unique_id * Update sensor.py * Update sensor.py * Type Hints * Move tuples * Drive sensor name * Remove caps * Remove caps * Add YAML import * Update sensor.py fix ruff * Revert tuples * Update sensor.py as per review * Update sensor.py * Update sensor.py * Update sensor.py * Update sensor.py * Update sensor.py * Update sensor.py * Update sensor.py * assert uid is not None * Update homeassistant/components/qnap/sensor.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/qnap/sensor.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update .coveragerc Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/qnap/config_flow.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/qnap/config_flow.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/qnap/const.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/qnap/const.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update sensor.py * Update config_flow.py add imports * Update const.py * Update sensor.py move confs to const * Update config_flow.py add const * Update test_config_flow.py remove const name * Update test_config_flow.py remove standard result const * Update test_config_flow.py * Combine tests * Update test_config_flow.py * Update config_flow.py * Update test_config_flow.py * Update config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update sensor.py change UID as requested * Update sensor.py added check for monitor_device * fix tests * Remove rounding * Revert "Remove rounding" This reverts commit 61bf653c069d37cd7c20e3dd2f555f80b6e5d94f. --------- Co-authored-by: starkillerOG Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .coveragerc | 2 + CODEOWNERS | 1 + homeassistant/components/qnap/__init__.py | 32 ++++++ homeassistant/components/qnap/config_flow.py | 99 +++++++++++++++++ homeassistant/components/qnap/const.py | 6 + homeassistant/components/qnap/coordinator.py | 18 +-- homeassistant/components/qnap/manifest.json | 1 + homeassistant/components/qnap/sensor.py | 106 +++++++++++------- homeassistant/components/qnap/strings.json | 23 ++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 2 +- requirements_test_all.txt | 3 + tests/components/qnap/__init__.py | 1 + tests/components/qnap/conftest.py | 33 ++++++ tests/components/qnap/test_config_flow.py | 110 +++++++++++++++++++ 15 files changed, 389 insertions(+), 49 deletions(-) create mode 100644 homeassistant/components/qnap/config_flow.py create mode 100644 homeassistant/components/qnap/strings.json create mode 100644 tests/components/qnap/__init__.py create mode 100644 tests/components/qnap/conftest.py create mode 100644 tests/components/qnap/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index c75737b530f..f3c34d26f73 100644 --- a/.coveragerc +++ b/.coveragerc @@ -942,6 +942,8 @@ omit = homeassistant/components/pyload/sensor.py homeassistant/components/qbittorrent/__init__.py homeassistant/components/qbittorrent/sensor.py + homeassistant/components/qnap/__init__.py + homeassistant/components/qnap/coordinator.py homeassistant/components/qnap/sensor.py homeassistant/components/qrcode/image_processing.py homeassistant/components/quantum_gateway/device_tracker.py diff --git a/CODEOWNERS b/CODEOWNERS index 38929301f87..8b575d11e6c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -970,6 +970,7 @@ build.json @home-assistant/supervisor /homeassistant/components/qld_bushfire/ @exxamalte /tests/components/qld_bushfire/ @exxamalte /homeassistant/components/qnap/ @disforw +/tests/components/qnap/ @disforw /homeassistant/components/qnap_qsw/ @Noltari /tests/components/qnap_qsw/ @Noltari /homeassistant/components/quantum_gateway/ @cisasteelersfan diff --git a/homeassistant/components/qnap/__init__.py b/homeassistant/components/qnap/__init__.py index 534096628df..2491e69803f 100644 --- a/homeassistant/components/qnap/__init__.py +++ b/homeassistant/components/qnap/__init__.py @@ -1 +1,33 @@ """The qnap component.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import QnapCoordinator + +PLATFORMS: list[Platform] = [ + Platform.SENSOR, +] + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set the config entry up.""" + hass.data.setdefault(DOMAIN, {}) + coordinator = QnapCoordinator(hass, config_entry) + # Fetch initial data so we have data when entities subscribe + await coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][config_entry.entry_id] = coordinator + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ): + hass.data[DOMAIN].pop(config_entry.entry_id) + return unload_ok diff --git a/homeassistant/components/qnap/config_flow.py b/homeassistant/components/qnap/config_flow.py new file mode 100644 index 00000000000..689fe30a870 --- /dev/null +++ b/homeassistant/components/qnap/config_flow.py @@ -0,0 +1,99 @@ +"""Config flow to configure qnap component.""" +from __future__ import annotations + +import logging +from typing import Any + +from qnapstats import QNAPStats +from requests.exceptions import ConnectTimeout +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_HOST, + CONF_MONITORED_CONDITIONS, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv + +from .const import ( + CONF_DRIVES, + CONF_NICS, + CONF_VOLUMES, + DEFAULT_PORT, + DEFAULT_SSL, + DEFAULT_TIMEOUT, + DEFAULT_VERIFY_SSL, + DOMAIN, +) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } +) + +_LOGGER = logging.getLogger(__name__) + + +class QnapConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Qnap configuration flow.""" + + VERSION = 1 + + async def async_step_import(self, import_info: dict[str, Any]) -> FlowResult: + """Set the config entry up from yaml.""" + import_info.pop(CONF_MONITORED_CONDITIONS, None) + import_info.pop(CONF_NICS, None) + import_info.pop(CONF_DRIVES, None) + import_info.pop(CONF_VOLUMES, None) + return await self.async_step_user(import_info) + + async def async_step_user( + self, + user_input: dict[str, Any] | None = None, + ) -> FlowResult: + """Handle a flow initialized by the user.""" + errors = {} + if user_input is not None: + host = user_input[CONF_HOST] + protocol = "https" if user_input[CONF_SSL] else "http" + api = QNAPStats( + host=f"{protocol}://{host}", + port=user_input[CONF_PORT], + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], + verify_ssl=user_input[CONF_VERIFY_SSL], + timeout=DEFAULT_TIMEOUT, + ) + try: + stats = await self.hass.async_add_executor_job(api.get_system_stats) + except ConnectTimeout: + errors["base"] = "cannot_connect" + except TypeError: + errors["base"] = "invalid_auth" + except Exception as error: # pylint: disable=broad-except + _LOGGER.error(error) + errors["base"] = "unknown" + else: + unique_id = stats["system"]["serial_number"] + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + title = stats["system"]["name"] + return self.async_create_entry(title=title, data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=self.add_suggested_values_to_schema(DATA_SCHEMA, user_input), + errors=errors, + ) diff --git a/homeassistant/components/qnap/const.py b/homeassistant/components/qnap/const.py index b5a0aef3dbc..d1bbb64cc47 100644 --- a/homeassistant/components/qnap/const.py +++ b/homeassistant/components/qnap/const.py @@ -1,6 +1,12 @@ """The Qnap constants.""" +CONF_DRIVES = "drives" +CONF_NICS = "nics" +CONF_VOLUMES = "volumes" + DEFAULT_PORT = 8080 DEFAULT_TIMEOUT = 5 +DEFAULT_SSL = False +DEFAULT_VERIFY_SSL = True DOMAIN = "qnap" diff --git a/homeassistant/components/qnap/coordinator.py b/homeassistant/components/qnap/coordinator.py index bcf0820615d..b868a931ebd 100644 --- a/homeassistant/components/qnap/coordinator.py +++ b/homeassistant/components/qnap/coordinator.py @@ -7,6 +7,7 @@ from typing import Any from qnapstats import QNAPStats +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -17,7 +18,6 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN @@ -30,18 +30,18 @@ _LOGGER = logging.getLogger(__name__) class QnapCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]): """Custom coordinator for the qnap integration.""" - def __init__(self, hass: HomeAssistant, config: ConfigType) -> None: + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize the qnap coordinator.""" super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) - protocol = "https" if config[CONF_SSL] else "http" + protocol = "https" if config_entry.data[CONF_SSL] else "http" self._api = QNAPStats( - f"{protocol}://{config.get(CONF_HOST)}", - config.get(CONF_PORT), - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD), - verify_ssl=config.get(CONF_VERIFY_SSL), - timeout=config.get(CONF_TIMEOUT), + f"{protocol}://{config_entry.data.get(CONF_HOST)}", + config_entry.data.get(CONF_PORT), + config_entry.data.get(CONF_USERNAME), + config_entry.data.get(CONF_PASSWORD), + verify_ssl=config_entry.data.get(CONF_VERIFY_SSL), + timeout=config_entry.data.get(CONF_TIMEOUT), ) def _sync_update(self) -> dict[str, dict[str, Any]]: diff --git a/homeassistant/components/qnap/manifest.json b/homeassistant/components/qnap/manifest.json index fb55d40c66e..608d57a7cc4 100644 --- a/homeassistant/components/qnap/manifest.json +++ b/homeassistant/components/qnap/manifest.json @@ -2,6 +2,7 @@ "domain": "qnap", "name": "QNAP", "codeowners": ["@disforw"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/qnap", "integration_type": "device", "iot_class": "local_polling", diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index 59dab85f04c..6d214b63e2e 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -1,8 +1,11 @@ """Support for QNAP NAS Sensors.""" +from __future__ import annotations + import logging import voluptuous as vol +from homeassistant import config_entries from homeassistant.components.sensor import ( PLATFORM_SCHEMA, SensorDeviceClass, @@ -28,17 +31,25 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DEFAULT_PORT, DEFAULT_TIMEOUT +from .const import ( + CONF_DRIVES, + CONF_NICS, + CONF_VOLUMES, + DEFAULT_PORT, + DEFAULT_TIMEOUT, + DOMAIN, +) from .coordinator import QnapCoordinator _LOGGER = logging.getLogger(__name__) ATTR_DRIVE = "Drive" -ATTR_DRIVE_SIZE = "Drive Size" ATTR_IP = "IP Address" ATTR_MAC = "MAC Address" ATTR_MASK = "Mask" @@ -53,13 +64,6 @@ ATTR_TYPE = "Type" ATTR_UPTIME = "Uptime" ATTR_VOLUME_SIZE = "Volume Size" -CONF_DRIVES = "drives" -CONF_NICS = "nics" -CONF_VOLUMES = "volumes" - -NOTIFICATION_ID = "qnap_notification" -NOTIFICATION_TITLE = "QNAP Sensor Setup" - _SYSTEM_MON_COND: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="status", @@ -224,72 +228,87 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the QNAP NAS sensor.""" - coordinator = QnapCoordinator(hass, config) + """Set up the qnap sensor platform from yaml.""" + + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.12.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config + ) + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up entry.""" + coordinator = QnapCoordinator(hass, config_entry) await coordinator.async_refresh() if not coordinator.last_update_success: raise PlatformNotReady - - monitored_conditions = config[CONF_MONITORED_CONDITIONS] + uid = config_entry.unique_id + assert uid is not None sensors: list[QNAPSensor] = [] - # Basic sensors sensors.extend( [ - QNAPSystemSensor(coordinator, description) + QNAPSystemSensor(coordinator, description, uid) for description in _SYSTEM_MON_COND - if description.key in monitored_conditions ] ) + sensors.extend( - [ - QNAPCPUSensor(coordinator, description) - for description in _CPU_MON_COND - if description.key in monitored_conditions - ] + [QNAPCPUSensor(coordinator, description, uid) for description in _CPU_MON_COND] ) + sensors.extend( [ - QNAPMemorySensor(coordinator, description) + QNAPMemorySensor(coordinator, description, uid) for description in _MEMORY_MON_COND - if description.key in monitored_conditions ] ) # Network sensors sensors.extend( [ - QNAPNetworkSensor(coordinator, description, nic) - for nic in config.get(CONF_NICS, coordinator.data["system_stats"]["nics"]) + QNAPNetworkSensor(coordinator, description, uid, nic) + for nic in coordinator.data["system_stats"]["nics"] for description in _NETWORK_MON_COND - if description.key in monitored_conditions ] ) # Drive sensors sensors.extend( [ - QNAPDriveSensor(coordinator, description, drive) - for drive in config.get(CONF_DRIVES, coordinator.data["smart_drive_health"]) + QNAPDriveSensor(coordinator, description, uid, drive) + for drive in coordinator.data["smart_drive_health"] for description in _DRIVE_MON_COND - if description.key in monitored_conditions ] ) # Volume sensors sensors.extend( [ - QNAPVolumeSensor(coordinator, description, volume) - for volume in config.get(CONF_VOLUMES, coordinator.data["volumes"]) + QNAPVolumeSensor(coordinator, description, uid, volume) + for volume in coordinator.data["volumes"] for description in _VOLUME_MON_COND - if description.key in monitored_conditions ] ) - - add_entities(sensors) + async_add_entities(sensors) def round_nicely(number): @@ -309,13 +328,24 @@ class QNAPSensor(CoordinatorEntity[QnapCoordinator], SensorEntity): self, coordinator: QnapCoordinator, description: SensorEntityDescription, + unique_id: str, monitor_device: str | None = None, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = description - self.monitor_device = monitor_device self.device_name = self.coordinator.data["system_stats"]["system"]["name"] + self.monitor_device = monitor_device + self._attr_unique_id = f"{unique_id}_{description.key}" + if monitor_device: + self._attr_unique_id = f"{self._attr_unique_id}_{monitor_device}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + name=self.device_name, + model=self.coordinator.data["system_stats"]["system"]["model"], + sw_version=self.coordinator.data["system_stats"]["firmware"]["version"], + manufacturer="QNAP", + ) @property def name(self): @@ -493,7 +523,5 @@ class QNAPVolumeSensor(QNAPSensor): total_gb = int(data["total_size"]) / 1024 / 1024 / 1024 return { - ATTR_VOLUME_SIZE: ( - f"{round_nicely(total_gb)} {UnitOfInformation.GIBIBYTES}" - ) + ATTR_VOLUME_SIZE: f"{round_nicely(total_gb)} {UnitOfInformation.GIBIBYTES}" } diff --git a/homeassistant/components/qnap/strings.json b/homeassistant/components/qnap/strings.json new file mode 100644 index 00000000000..26ca5dedd34 --- /dev/null +++ b/homeassistant/components/qnap/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "title": "Connect to the QNAP device", + "description": "This qnap sensor allows getting various statistics from your QNAP NAS.", + "data": { + "host": "Hostname", + "username": "Username", + "password": "Password", + "port": "Port", + "ssl": "Enable SSL", + "verify_ssl": "Verify SSL" + } + } + }, + "error": { + "cannot_connect": "Cannot connect to host", + "invalid_auth": "Bad authentication", + "unknown": "Unknown error" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 77b1c77a914..fe7586e5890 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -355,6 +355,7 @@ FLOWS = { "pvpc_hourly_pricing", "qbittorrent", "qingping", + "qnap", "qnap_qsw", "rachio", "radarr", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 0d080763f55..5e10fdf86ff 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4387,7 +4387,7 @@ "integrations": { "qnap": { "integration_type": "device", - "config_flow": false, + "config_flow": true, "iot_class": "local_polling", "name": "QNAP" }, diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e694d61fe79..fae3f617db7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1636,6 +1636,9 @@ pyzerproc==0.4.8 # homeassistant.components.qingping qingping-ble==0.8.2 +# homeassistant.components.qnap +qnapstats==0.4.0 + # homeassistant.components.radio_browser radios==0.1.1 diff --git a/tests/components/qnap/__init__.py b/tests/components/qnap/__init__.py new file mode 100644 index 00000000000..46978f6cc8d --- /dev/null +++ b/tests/components/qnap/__init__.py @@ -0,0 +1 @@ +"""Tests for the QNAP integration.""" diff --git a/tests/components/qnap/conftest.py b/tests/components/qnap/conftest.py new file mode 100644 index 00000000000..7f26763d4d7 --- /dev/null +++ b/tests/components/qnap/conftest.py @@ -0,0 +1,33 @@ +"""Setup the QNAP tests.""" +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +TEST_HOST = "1.2.3.4" +TEST_USERNAME = "admin" +TEST_PASSWORD = "password" +TEST_NAS_NAME = "Test NAS name" +TEST_SERIAL = "123456789" + +TEST_SYSTEM_STATS = {"system": {"serial_number": TEST_SERIAL, "name": TEST_NAS_NAME}} + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.qnap.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def qnap_connect(mock_get_source_ip: None) -> Generator[MagicMock, None, None]: + """Mock qnap connection.""" + with patch( + "homeassistant.components.qnap.config_flow.QNAPStats", autospec=True + ) as host_mock_class: + host_mock = host_mock_class.return_value + host_mock.get_system_stats.return_value = TEST_SYSTEM_STATS + yield host_mock diff --git a/tests/components/qnap/test_config_flow.py b/tests/components/qnap/test_config_flow.py new file mode 100644 index 00000000000..eb77109d62e --- /dev/null +++ b/tests/components/qnap/test_config_flow.py @@ -0,0 +1,110 @@ +"""Test the QNAP config flow.""" +from unittest.mock import MagicMock + +import pytest +from requests.exceptions import ConnectTimeout + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.qnap import const +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import HomeAssistant + +from .conftest import TEST_HOST, TEST_PASSWORD, TEST_USERNAME + +STANDARD_CONFIG = { + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_HOST: TEST_HOST, +} + + +pytestmark = pytest.mark.usefixtures("mock_setup_entry", "qnap_connect") + + +async def test_config_flow(hass: HomeAssistant, qnap_connect: MagicMock) -> None: + """Config flow manually initialized by the user.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + qnap_connect.get_system_stats.side_effect = ConnectTimeout("Test error") + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + STANDARD_CONFIG, + ) + + assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + qnap_connect.get_system_stats.side_effect = TypeError("Test error") + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + STANDARD_CONFIG, + ) + + assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} + + qnap_connect.get_system_stats.side_effect = Exception("Test error") + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + STANDARD_CONFIG, + ) + + assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "unknown"} + + qnap_connect.get_system_stats.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + STANDARD_CONFIG, + ) + + assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "Test NAS name" + assert result["data"] == { + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "admin", + CONF_PASSWORD: "password", + CONF_SSL: const.DEFAULT_SSL, + CONF_VERIFY_SSL: const.DEFAULT_VERIFY_SSL, + CONF_PORT: const.DEFAULT_PORT, + } + + +async def test_config_flow_import(hass: HomeAssistant) -> None: + """Test import of YAML config.""" + data = STANDARD_CONFIG + data[CONF_SSL] = const.DEFAULT_SSL + data[CONF_VERIFY_SSL] = const.DEFAULT_VERIFY_SSL + data[CONF_PORT] = const.DEFAULT_PORT + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=data, + ) + + assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "Test NAS name" + assert result["data"] == { + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "admin", + CONF_PASSWORD: "password", + CONF_SSL: const.DEFAULT_SSL, + CONF_VERIFY_SSL: const.DEFAULT_VERIFY_SSL, + CONF_PORT: const.DEFAULT_PORT, + }