diff --git a/.coveragerc b/.coveragerc index 12095eef247..752ea5ca7bc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -537,7 +537,12 @@ omit = homeassistant/components/hvv_departures/__init__.py homeassistant/components/hvv_departures/binary_sensor.py homeassistant/components/hvv_departures/sensor.py - homeassistant/components/hydrawise/* + homeassistant/components/hydrawise/__init__.py + homeassistant/components/hydrawise/binary_sensor.py + homeassistant/components/hydrawise/const.py + homeassistant/components/hydrawise/coordinator.py + homeassistant/components/hydrawise/sensor.py + homeassistant/components/hydrawise/switch.py homeassistant/components/ialarm/alarm_control_panel.py homeassistant/components/iammeter/sensor.py homeassistant/components/iaqualink/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 6874e81bf0a..e728d70c1bc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -562,6 +562,7 @@ build.json @home-assistant/supervisor /homeassistant/components/hvv_departures/ @vigonotion /tests/components/hvv_departures/ @vigonotion /homeassistant/components/hydrawise/ @dknowles2 @ptcryan +/tests/components/hydrawise/ @dknowles2 @ptcryan /homeassistant/components/hyperion/ @dermotduffy /tests/components/hyperion/ @dermotduffy /homeassistant/components/ialarm/ @RyuzakiKK diff --git a/homeassistant/components/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py index 6d9f2747847..560046e9c2b 100644 --- a/homeassistant/components/hydrawise/__init__.py +++ b/homeassistant/components/hydrawise/__init__.py @@ -5,13 +5,19 @@ from pydrawise.legacy import LegacyHydrawise from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol -from homeassistant.components import persistent_notification -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_SCAN_INTERVAL +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_API_KEY, + CONF_SCAN_INTERVAL, + Platform, +) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN, LOGGER, NOTIFICATION_ID, NOTIFICATION_TITLE, SCAN_INTERVAL +from .const import DOMAIN, LOGGER, SCAN_INTERVAL from .coordinator import HydrawiseDataUpdateCoordinator CONFIG_SCHEMA = vol.Schema( @@ -26,37 +32,49 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Hunter Hydrawise component.""" - conf = config[DOMAIN] - access_token = conf[CONF_ACCESS_TOKEN] - scan_interval = conf.get(CONF_SCAN_INTERVAL) + if DOMAIN not in config: + return True + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_API_KEY: config[DOMAIN][CONF_ACCESS_TOKEN]}, + ) + ) + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up Hydrawise from a config entry.""" + access_token = config_entry.data[CONF_API_KEY] try: hydrawise = await hass.async_add_executor_job(LegacyHydrawise, access_token) except (ConnectTimeout, HTTPError) as ex: LOGGER.error("Unable to connect to Hydrawise cloud service: %s", str(ex)) - _show_failure_notification(hass, str(ex)) - return False + raise ConfigEntryNotReady( + f"Unable to connect to Hydrawise cloud service: {ex}" + ) from ex - if not hydrawise.current_controller: - LOGGER.error("Failed to fetch Hydrawise data") - _show_failure_notification(hass, "Failed to fetch Hydrawise data.") - return False - - hass.data[DOMAIN] = HydrawiseDataUpdateCoordinator(hass, hydrawise, scan_interval) + hass.data.setdefault(DOMAIN, {})[ + config_entry.entry_id + ] = HydrawiseDataUpdateCoordinator(hass, hydrawise, SCAN_INTERVAL) + if not hydrawise.controller_info or not hydrawise.controller_status: + raise ConfigEntryNotReady("Hydrawise data not loaded") # NOTE: We don't need to call async_config_entry_first_refresh() because # data is fetched when the Hydrawiser object is instantiated. - + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True -def _show_failure_notification(hass: HomeAssistant, error: str) -> None: - persistent_notification.create( - hass, - f"Error: {error}
You will need to restart hass after fixing.", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py index 9298e605791..06683ff0345 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -10,6 +10,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -38,6 +39,8 @@ BINARY_SENSOR_KEYS: list[str] = [ desc.key for desc in (BINARY_SENSOR_STATUS, *BINARY_SENSOR_TYPES) ] +# Deprecated since Home Assistant 2023.10.0 +# Can be removed completely in 2024.4.0 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_MONITORED_CONDITIONS, default=BINARY_SENSOR_KEYS): vol.All( @@ -54,32 +57,39 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up a sensor for a Hydrawise device.""" - coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN] - hydrawise: LegacyHydrawise = coordinator.api - monitored_conditions = config[CONF_MONITORED_CONDITIONS] + # We don't need to trigger import flow from here as it's triggered from `__init__.py` + return - entities = [] - if BINARY_SENSOR_STATUS.key in monitored_conditions: - entities.append( - HydrawiseBinarySensor( - data=hydrawise.current_controller, - coordinator=coordinator, - description=BINARY_SENSOR_STATUS, - ) + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Hydrawise binary_sensor platform.""" + coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + hydrawise: LegacyHydrawise = coordinator.api + + entities = [ + HydrawiseBinarySensor( + data=hydrawise.current_controller, + coordinator=coordinator, + description=BINARY_SENSOR_STATUS, ) + ] # create a sensor for each zone for zone in hydrawise.relays: for description in BINARY_SENSOR_TYPES: - if description.key not in monitored_conditions: - continue entities.append( HydrawiseBinarySensor( data=zone, coordinator=coordinator, description=description ) ) - add_entities(entities, True) + async_add_entities(entities) class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity): diff --git a/homeassistant/components/hydrawise/config_flow.py b/homeassistant/components/hydrawise/config_flow.py new file mode 100644 index 00000000000..c4b37fb4a06 --- /dev/null +++ b/homeassistant/components/hydrawise/config_flow.py @@ -0,0 +1,111 @@ +"""Config flow for the Hydrawise integration.""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import Any + +from pydrawise import legacy +from requests.exceptions import ConnectTimeout, HTTPError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN +from homeassistant.data_entry_flow import AbortFlow, FlowResult, FlowResultType +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue + +from .const import DOMAIN, LOGGER + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Hydrawise.""" + + VERSION = 1 + + async def _create_entry( + self, api_key: str, *, on_failure: Callable[[str], FlowResult] + ) -> FlowResult: + """Create the config entry.""" + try: + api = await self.hass.async_add_executor_job( + legacy.LegacyHydrawise, api_key + ) + except ConnectTimeout: + return on_failure("timeout_connect") + except HTTPError as ex: + LOGGER.error("Unable to connect to Hydrawise cloud service: %s", ex) + return on_failure("cannot_connect") + + if not api.status: + return on_failure("unknown") + + await self.async_set_unique_id(f"hydrawise-{api.customer_id}") + self._abort_if_unique_id_configured() + + return self.async_create_entry(title="Hydrawise", data={CONF_API_KEY: api_key}) + + def _import_issue(self, error_type: str) -> FlowResult: + """Create an issue about a YAML import failure.""" + async_create_issue( + self.hass, + DOMAIN, + f"deprecated_yaml_import_issue_{error_type}", + breaks_in_ha_version="2024.4.0", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="deprecated_yaml_import_issue", + translation_placeholders={"error_type": error_type}, + ) + return self.async_abort(reason=error_type) + + def _deprecated_yaml_issue(self) -> None: + """Create an issue about YAML deprecation.""" + async_create_issue( + self.hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.4.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Hydrawise", + }, + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial setup.""" + if user_input is not None: + api_key = user_input[CONF_API_KEY] + return await self._create_entry(api_key, on_failure=self._show_form) + return self._show_form() + + def _show_form(self, error_type: str | None = None) -> FlowResult: + errors = {} + if error_type is not None: + errors["base"] = error_type + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}), + errors=errors, + ) + + async def async_step_import(self, import_data: dict[str, Any]) -> FlowResult: + """Import data from YAML.""" + try: + result = await self._create_entry( + import_data.get(CONF_API_KEY, ""), + on_failure=self._import_issue, + ) + except AbortFlow: + self._deprecated_yaml_issue() + raise + + if result["type"] == FlowResultType.CREATE_ENTRY: + self._deprecated_yaml_issue() + return result diff --git a/homeassistant/components/hydrawise/const.py b/homeassistant/components/hydrawise/const.py index 515fdaec2b1..ccf3eb5bac0 100644 --- a/homeassistant/components/hydrawise/const.py +++ b/homeassistant/components/hydrawise/const.py @@ -8,9 +8,6 @@ LOGGER = logging.getLogger(__package__) ALLOWED_WATERING_TIME = [5, 10, 15, 30, 45, 60] CONF_WATERING_TIME = "watering_minutes" -NOTIFICATION_ID = "hydrawise_notification" -NOTIFICATION_TITLE = "Hydrawise Setup" - DOMAIN = "hydrawise" DEFAULT_WATERING_TIME = 15 diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index f9de9bf30c9..eea4a0e2ebf 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -2,6 +2,7 @@ "domain": "hydrawise", "name": "Hunter Hydrawise", "codeowners": ["@dknowles2", "@ptcryan"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hydrawise", "iot_class": "cloud_polling", "loggers": ["pydrawise"], diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index fa82c058f5b..bcf178744c8 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -1,7 +1,6 @@ """Support for Hydrawise sprinkler sensors.""" from __future__ import annotations -from pydrawise.legacy import LegacyHydrawise import voluptuous as vol from homeassistant.components.sensor import ( @@ -10,6 +9,7 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MONITORED_CONDITIONS, UnitOfTime from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -37,6 +37,8 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] +# Deprecated since Home Assistant 2023.10.0 +# Can be removed completely in 2024.4.0 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( @@ -56,18 +58,25 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up a sensor for a Hydrawise device.""" - coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN] - hydrawise: LegacyHydrawise = coordinator.api - monitored_conditions = config[CONF_MONITORED_CONDITIONS] + # We don't need to trigger import flow from here as it's triggered from `__init__.py` + return + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Hydrawise sensor platform.""" + coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] entities = [ HydrawiseSensor(data=zone, coordinator=coordinator, description=description) - for zone in hydrawise.relays + for zone in coordinator.api.relays for description in SENSOR_TYPES - if description.key in monitored_conditions ] - - add_entities(entities, True) + async_add_entities(entities) class HydrawiseSensor(HydrawiseEntity, SensorEntity): diff --git a/homeassistant/components/hydrawise/strings.json b/homeassistant/components/hydrawise/strings.json new file mode 100644 index 00000000000..50d3fbaf4c3 --- /dev/null +++ b/homeassistant/components/hydrawise/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + }, + "issues": { + "deprecated_yaml_import_issue": { + "title": "The Hydrawise YAML configuration import failed", + "description": "Configuring Hydrawise using YAML is being removed but there was an {error_type} error importing your YAML configuration.\n\nEnsure connection to Hydrawise works and restart Home Assistant to try again or remove the Hydrawise YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + } + } +} diff --git a/homeassistant/components/hydrawise/switch.py b/homeassistant/components/hydrawise/switch.py index 0dd694a47d6..88112d8e27a 100644 --- a/homeassistant/components/hydrawise/switch.py +++ b/homeassistant/components/hydrawise/switch.py @@ -3,7 +3,6 @@ from __future__ import annotations from typing import Any -from pydrawise.legacy import LegacyHydrawise import voluptuous as vol from homeassistant.components.switch import ( @@ -12,6 +11,7 @@ from homeassistant.components.switch import ( SwitchEntity, SwitchEntityDescription, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -43,6 +43,8 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( SWITCH_KEYS: list[str] = [desc.key for desc in SWITCH_TYPES] +# Deprecated since Home Assistant 2023.10.0 +# Can be removed completely in 2024.4.0 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_MONITORED_CONDITIONS, default=SWITCH_KEYS): vol.All( @@ -62,10 +64,20 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up a sensor for a Hydrawise device.""" - coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN] - hydrawise: LegacyHydrawise = coordinator.api - monitored_conditions: list[str] = config[CONF_MONITORED_CONDITIONS] - default_watering_timer: int = config[CONF_WATERING_TIME] + # We don't need to trigger import flow from here as it's triggered from `__init__.py` + return + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Hydrawise switch platform.""" + coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + default_watering_timer = DEFAULT_WATERING_TIME entities = [ HydrawiseSwitch( @@ -74,12 +86,11 @@ def setup_platform( description=description, default_watering_timer=default_watering_timer, ) - for zone in hydrawise.relays + for zone in coordinator.api.relays for description in SWITCH_TYPES - if description.key in monitored_conditions ] - add_entities(entities, True) + async_add_entities(entities) class HydrawiseSwitch(HydrawiseEntity, SwitchEntity): diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 54089723e21..f229d753fec 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -206,6 +206,7 @@ FLOWS = { "huisbaasje", "hunterdouglas_powerview", "hvv_departures", + "hydrawise", "hyperion", "ialarm", "iaqualink", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index aac00cdd0d8..ef79e680ea2 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2501,7 +2501,7 @@ "hydrawise": { "name": "Hunter Hydrawise", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "cloud_polling" }, "hyperion": { diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21065157095..bf22ba34a65 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1244,6 +1244,9 @@ pydexcom==0.2.3 # homeassistant.components.discovergy pydiscovergy==2.0.3 +# homeassistant.components.hydrawise +pydrawise==2023.8.0 + # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 diff --git a/tests/components/hydrawise/__init__.py b/tests/components/hydrawise/__init__.py new file mode 100644 index 00000000000..582d20ba2df --- /dev/null +++ b/tests/components/hydrawise/__init__.py @@ -0,0 +1 @@ +"""Tests for the Hydrawise integration.""" diff --git a/tests/components/hydrawise/conftest.py b/tests/components/hydrawise/conftest.py new file mode 100644 index 00000000000..b6e22ec7b80 --- /dev/null +++ b/tests/components/hydrawise/conftest.py @@ -0,0 +1,15 @@ +"""Common fixtures for the Hydrawise tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.hydrawise.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/hydrawise/test_config_flow.py b/tests/components/hydrawise/test_config_flow.py new file mode 100644 index 00000000000..c9efbea507e --- /dev/null +++ b/tests/components/hydrawise/test_config_flow.py @@ -0,0 +1,213 @@ +"""Test the Hydrawise config flow.""" + +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from requests.exceptions import ConnectTimeout, HTTPError + +from homeassistant import config_entries +from homeassistant.components.hydrawise.const import DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_SCAN_INTERVAL +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +import homeassistant.helpers.issue_registry as ir + +from tests.common import MockConfigEntry + +pytestmark = pytest.mark.usefixtures("mock_setup_entry") + + +@patch("pydrawise.legacy.LegacyHydrawise") +async def test_form( + mock_api: MagicMock, hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"api_key": "abc123"} + ) + mock_api.return_value.customer_id = 12345 + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Hydrawise" + assert result2["data"] == {"api_key": "abc123"} + assert len(mock_setup_entry.mock_calls) == 1 + + +@patch("pydrawise.legacy.LegacyHydrawise") +async def test_form_api_error(mock_api: MagicMock, hass: HomeAssistant) -> None: + """Test we handle API errors.""" + mock_api.side_effect = HTTPError + init_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + data = {"api_key": "abc123"} + result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], data + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + + mock_api.side_effect = None + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) + assert result2["type"] == FlowResultType.CREATE_ENTRY + + +@patch("pydrawise.legacy.LegacyHydrawise") +async def test_form_connect_timeout(mock_api: MagicMock, hass: HomeAssistant) -> None: + """Test we handle API errors.""" + mock_api.side_effect = ConnectTimeout + init_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + data = {"api_key": "abc123"} + result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], data + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "timeout_connect"} + + mock_api.side_effect = None + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) + assert result2["type"] == FlowResultType.CREATE_ENTRY + + +@patch("pydrawise.legacy.LegacyHydrawise") +async def test_flow_import_success(mock_api: MagicMock, hass: HomeAssistant) -> None: + """Test that we can import a YAML config.""" + mock_api.return_value.status = "All good!" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "__api_key__", + CONF_SCAN_INTERVAL: 120, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Hydrawise" + assert result["data"] == { + CONF_API_KEY: "__api_key__", + } + + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue( + HOMEASSISTANT_DOMAIN, "deprecated_yaml_hydrawise" + ) + assert issue.translation_key == "deprecated_yaml" + + +@patch("pydrawise.legacy.LegacyHydrawise", side_effect=HTTPError) +async def test_flow_import_api_error(mock_api: MagicMock, hass: HomeAssistant) -> None: + """Test that we handle API errors on YAML import.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "__api_key__", + CONF_SCAN_INTERVAL: 120, + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue( + DOMAIN, "deprecated_yaml_import_issue_cannot_connect" + ) + assert issue.translation_key == "deprecated_yaml_import_issue" + + +@patch("pydrawise.legacy.LegacyHydrawise", side_effect=ConnectTimeout) +async def test_flow_import_connect_timeout( + mock_api: MagicMock, hass: HomeAssistant +) -> None: + """Test that we handle connection timeouts on YAML import.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "__api_key__", + CONF_SCAN_INTERVAL: 120, + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "timeout_connect" + + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue( + DOMAIN, "deprecated_yaml_import_issue_timeout_connect" + ) + assert issue.translation_key == "deprecated_yaml_import_issue" + + +@patch("pydrawise.legacy.LegacyHydrawise") +async def test_flow_import_no_status(mock_api: MagicMock, hass: HomeAssistant) -> None: + """Test we handle a lack of API status on YAML import.""" + mock_api.return_value.status = None + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "__api_key__", + CONF_SCAN_INTERVAL: 120, + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "unknown" + + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue( + DOMAIN, "deprecated_yaml_import_issue_unknown" + ) + assert issue.translation_key == "deprecated_yaml_import_issue" + + +@patch("pydrawise.legacy.LegacyHydrawise") +async def test_flow_import_already_imported( + mock_api: MagicMock, hass: HomeAssistant +) -> None: + """Test that we can handle a YAML config already imported.""" + mock_config_entry = MockConfigEntry( + title="Hydrawise", + domain=DOMAIN, + data={ + CONF_API_KEY: "__api_key__", + }, + unique_id="hydrawise-CUSTOMER_ID", + ) + mock_config_entry.add_to_hass(hass) + + mock_api.return_value.customer_id = "CUSTOMER_ID" + mock_api.return_value.status = "All good!" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "__api_key__", + CONF_SCAN_INTERVAL: 120, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result.get("reason") == "already_configured" + + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue( + HOMEASSISTANT_DOMAIN, "deprecated_yaml_hydrawise" + ) + assert issue.translation_key == "deprecated_yaml"