diff --git a/CODEOWNERS b/CODEOWNERS index 949b8ff0b91..5bb50ed888e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1809,6 +1809,7 @@ build.json @home-assistant/supervisor /homeassistant/components/water_heater/ @home-assistant/core /tests/components/water_heater/ @home-assistant/core /homeassistant/components/waterfurnace/ @sdague @masterkoppa +/tests/components/waterfurnace/ @sdague @masterkoppa /homeassistant/components/watergate/ @adam-the-hero /tests/components/watergate/ @adam-the-hero /homeassistant/components/watson_tts/ @rutkai diff --git a/homeassistant/components/waterfurnace/__init__.py b/homeassistant/components/waterfurnace/__init__.py index 4c150b99f43..d1681f00028 100644 --- a/homeassistant/components/waterfurnace/__init__.py +++ b/homeassistant/components/waterfurnace/__init__.py @@ -1,4 +1,6 @@ -"""Support for Waterfurnaces.""" +"""Support for WaterFurnace geothermal systems.""" + +from __future__ import annotations from datetime import timedelta import logging @@ -9,62 +11,121 @@ import voluptuous as vol from waterfurnace.waterfurnace import WaterFurnace, WFCredentialError, WFException from homeassistant.components import persistent_notification +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv, issue_registry as ir from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.typing import ConfigType +from .const import DOMAIN, INTEGRATION_TITLE, MAX_FAILS + _LOGGER = logging.getLogger(__name__) -DOMAIN = "waterfurnace" +PLATFORMS = [Platform.SENSOR] + UPDATE_TOPIC = f"{DOMAIN}_update" SCAN_INTERVAL = timedelta(seconds=10) ERROR_INTERVAL = timedelta(seconds=300) -MAX_FAILS = 10 NOTIFICATION_ID = "waterfurnace_website_notification" NOTIFICATION_TITLE = "WaterFurnace website status" - - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { - vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, } ) }, extra=vol.ALLOW_EXTRA, ) +type WaterFurnaceConfigEntry = ConfigEntry[WaterFurnaceData] -def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: - """Set up waterfurnace platform.""" +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Import the WaterFurnace configuration from YAML.""" + if DOMAIN not in config: + return True - config = base_config[DOMAIN] + hass.async_create_task(_async_setup(hass, config)) - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] + return True + + +async def _async_setup(hass: HomeAssistant, config: ConfigType) -> None: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config[DOMAIN], + ) + if ( + result.get("type") is FlowResultType.ABORT + and result.get("reason") != "already_configured" + ): + ir.async_create_issue( + hass, + DOMAIN, + f"deprecated_yaml_import_issue_{result.get('reason')}", + breaks_in_ha_version="2026.8.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=ir.IssueSeverity.WARNING, + translation_key=f"deprecated_yaml_import_issue_{result.get('reason')}", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": INTEGRATION_TITLE, + }, + ) + return + + ir.async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2026.8.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": INTEGRATION_TITLE, + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, entry: WaterFurnaceConfigEntry +) -> bool: + """Set up WaterFurnace from a config entry.""" + username = entry.data[CONF_USERNAME] + password = entry.data[CONF_PASSWORD] + + client = WaterFurnace(username, password) - wfconn = WaterFurnace(username, password) - # NOTE(sdague): login will throw an exception if this doesn't - # work, which will abort the setup. try: - wfconn.login() - except WFCredentialError: - _LOGGER.error("Invalid credentials for waterfurnace login") - return False + await hass.async_add_executor_job(client.login) + except WFCredentialError as err: + raise ConfigEntryAuthFailed( + "Authentication failed. Please update your credentials." + ) from err - hass.data[DOMAIN] = WaterFurnaceData(hass, wfconn) - hass.data[DOMAIN].start() + if not client.gwid: + raise ConfigEntryNotReady( + "Failed to connect to WaterFurnace service: No GWID found for device" + ) + + entry.runtime_data = client + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - discovery.load_platform(hass, Platform.SENSOR, DOMAIN, {}, config) return True @@ -79,7 +140,7 @@ class WaterFurnaceData(threading.Thread): to do. """ - def __init__(self, hass, client): + def __init__(self, hass: HomeAssistant, client) -> None: """Initialize the data object.""" super().__init__() self.hass = hass diff --git a/homeassistant/components/waterfurnace/config_flow.py b/homeassistant/components/waterfurnace/config_flow.py new file mode 100644 index 00000000000..bf5f7f764c5 --- /dev/null +++ b/homeassistant/components/waterfurnace/config_flow.py @@ -0,0 +1,105 @@ +"""Config flow for WaterFurnace integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +import voluptuous as vol +from waterfurnace.waterfurnace import WaterFurnace, WFCredentialError, WFException + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +class WaterFurnaceConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for WaterFurnace.""" + + VERSION = 1 + MINOR_VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + + if user_input is not None: + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + + client = WaterFurnace(username, password) + + try: + # Login is a blocking call, run in executor + await self.hass.async_add_executor_job(client.login) + except WFCredentialError: + errors["base"] = "invalid_auth" + except WFException: + errors["base"] = "cannot_connect" + except Exception: + _LOGGER.exception("Unexpected error connecting to WaterFurnace") + errors["base"] = "unknown" + + gwid = client.gwid + if not gwid: + errors["base"] = "cannot_connect" + + if not errors: + # Set unique ID based on GWID + await self.async_set_unique_id(gwid) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=f"WaterFurnace {username}", + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_DATA_SCHEMA, + errors=errors, + ) + + async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: + """Handle import from YAML configuration.""" + username = import_data[CONF_USERNAME] + password = import_data[CONF_PASSWORD] + + client = WaterFurnace(username, password) + + try: + # Login is a blocking call, run in executor + await self.hass.async_add_executor_job(client.login) + except WFCredentialError: + return self.async_abort(reason="invalid_auth") + except WFException: + return self.async_abort(reason="cannot_connect") + except Exception: + _LOGGER.exception("Unexpected error importing WaterFurnace configuration") + return self.async_abort(reason="unknown") + + gwid = client.gwid + if not gwid: + # This likely indicates a server-side change, or an implementation bug + return self.async_abort(reason="cannot_connect") + + # Set unique ID based on GWID + await self.async_set_unique_id(gwid) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=f"WaterFurnace {username}", + data=import_data, + ) diff --git a/homeassistant/components/waterfurnace/const.py b/homeassistant/components/waterfurnace/const.py new file mode 100644 index 00000000000..d1291dc43ad --- /dev/null +++ b/homeassistant/components/waterfurnace/const.py @@ -0,0 +1,10 @@ +"""Constants for the WaterFurnace integration.""" + +from typing import Final + +DOMAIN: Final = "waterfurnace" +INTEGRATION_TITLE: Final = "WaterFurnace" + + +# Connection settings +MAX_FAILS: Final = 10 diff --git a/homeassistant/components/waterfurnace/manifest.json b/homeassistant/components/waterfurnace/manifest.json index 15ac301a739..2bdf1c4a56e 100644 --- a/homeassistant/components/waterfurnace/manifest.json +++ b/homeassistant/components/waterfurnace/manifest.json @@ -2,6 +2,7 @@ "domain": "waterfurnace", "name": "WaterFurnace", "codeowners": ["@sdague", "@masterkoppa"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/waterfurnace", "integration_type": "device", "iot_class": "cloud_polling", diff --git a/homeassistant/components/waterfurnace/sensor.py b/homeassistant/components/waterfurnace/sensor.py index b51aa2b2909..91a2968404e 100644 --- a/homeassistant/components/waterfurnace/sensor.py +++ b/homeassistant/components/waterfurnace/sensor.py @@ -12,11 +12,10 @@ from homeassistant.components.sensor import ( from homeassistant.const import PERCENTAGE, UnitOfPower, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util import slugify -from . import DOMAIN, UPDATE_TOPIC, WaterFurnaceData +from . import UPDATE_TOPIC, WaterFurnaceConfigEntry, WaterFurnaceData SENSORS = [ SensorEntityDescription(name="Furnace Mode", key="mode", icon="mdi:gauge"), @@ -106,19 +105,19 @@ SENSORS = [ ] -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + config_entry: WaterFurnaceConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up the Waterfurnace sensor.""" - if discovery_info is None: - return + """Set up Waterfurnace sensors from a config entry.""" - client = hass.data[DOMAIN] + data_collector = WaterFurnaceData(hass, config_entry.runtime_data) + data_collector.start() - add_entities(WaterFurnaceSensor(client, description) for description in SENSORS) + async_add_entities( + WaterFurnaceSensor(data_collector, description) for description in SENSORS + ) class WaterFurnaceSensor(SensorEntity): diff --git a/homeassistant/components/waterfurnace/strings.json b/homeassistant/components/waterfurnace/strings.json new file mode 100644 index 00000000000..db369879e25 --- /dev/null +++ b/homeassistant/components/waterfurnace/strings.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "cannot_connect": "Please verify your credentials.", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "Unexpected error, please try again." + }, + "error": { + "cannot_connect": "Failed to connect to WaterFurnace service", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "user": { + "data": { + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" + }, + "data_description": { + "password": "Your WaterFurnace Symphony account password", + "username": "Your WaterFurnace Symphony account username" + }, + "description": "Enter your WaterFurnace Symphony account credentials.", + "title": "Connect to WaterFurnace" + } + } + }, + "issues": { + "deprecated_yaml_import_issue_cannot_connect": { + "description": "Configuring {integration_title} via YAML is deprecated and will be removed in a future release. While importing your configuration, we could not connect to {integration_title}. Please check your YAML configuration and restart Home Assistant, or remove the {domain} key from your configuration and configure the integration via the UI.", + "title": "[%key:component::waterfurnace::issues::deprecated_yaml_import_issue_unknown::title%]" + }, + "deprecated_yaml_import_issue_invalid_auth": { + "description": "Configuring {integration_title} via YAML is deprecated and will be removed in a future release. While importing your configuration, invalid authentication details were found. Please correct your YAML configuration and restart Home Assistant, or remove the {domain} key from your configuration and configure the integration via the UI.", + "title": "[%key:component::waterfurnace::issues::deprecated_yaml_import_issue_unknown::title%]" + }, + "deprecated_yaml_import_issue_unknown": { + "description": "Configuring {integration_title} via YAML is deprecated and will be removed in a future release. While importing your configuration an unknown exception has been encountered. Please correct your YAML configuration and restart Home Assistant, or remove the {domain} key from your configuration and configure the integration via the UI.", + "title": "WaterFurnace YAML configuration import failed" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 83f00f52d54..6c8dbc7af93 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -756,6 +756,7 @@ FLOWS = { "wake_on_lan", "wallbox", "waqi", + "waterfurnace", "watergate", "watts", "watttime", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index b57b45c3d04..39304b19737 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -7477,7 +7477,7 @@ "waterfurnace": { "name": "WaterFurnace", "integration_type": "device", - "config_flow": false, + "config_flow": true, "iot_class": "cloud_polling" }, "watergate": { diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 43c99fe12b3..888e2e151f8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2654,6 +2654,9 @@ wallbox==0.9.0 # homeassistant.components.folder_watcher watchdog==6.0.0 +# homeassistant.components.waterfurnace +waterfurnace==1.4.0 + # homeassistant.components.watergate watergate-local-api==2025.1.0 diff --git a/tests/components/waterfurnace/__init__.py b/tests/components/waterfurnace/__init__.py new file mode 100644 index 00000000000..bd145aa7265 --- /dev/null +++ b/tests/components/waterfurnace/__init__.py @@ -0,0 +1 @@ +"""Tests for the WaterFurnace integration.""" diff --git a/tests/components/waterfurnace/conftest.py b/tests/components/waterfurnace/conftest.py new file mode 100644 index 00000000000..51c4808d5a3 --- /dev/null +++ b/tests/components/waterfurnace/conftest.py @@ -0,0 +1,53 @@ +"""Fixtures for WaterFurnace integration tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, Mock, patch + +import pytest +from waterfurnace.waterfurnace import WFReading + +from homeassistant.components.waterfurnace.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry, load_json_object_fixture + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.waterfurnace.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup + + +@pytest.fixture +def mock_waterfurnace_client() -> Generator[Mock]: + """Mock WaterFurnace client.""" + with patch( + "homeassistant.components.waterfurnace.config_flow.WaterFurnace", + autospec=True, + ) as mock_client_class: + mock_client = mock_client_class.return_value + + mock_client.gwid = "TEST_GWID_12345" + + device_data = WFReading(load_json_object_fixture("device_data.json", DOMAIN)) + + mock_client.read.return_value = device_data + + yield mock_client + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return a mock config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="WaterFurnace test_user", + data={ + CONF_USERNAME: "test_user", + CONF_PASSWORD: "test_password", + }, + unique_id="TEST_GWID_12345", + ) diff --git a/tests/components/waterfurnace/fixtures/device_data.json b/tests/components/waterfurnace/fixtures/device_data.json new file mode 100644 index 00000000000..41fd4a6ec08 --- /dev/null +++ b/tests/components/waterfurnace/fixtures/device_data.json @@ -0,0 +1,16 @@ +{ + "modeofoperation": 6, + "totalunitpower": 1500, + "tstatactivesetpoint": 72, + "leavingairtemp": 110.5, + "tstatroomtemp": 70.2, + "enteringwatertemp": 42.8, + "tstathumidsetpoint": 45, + "tstatrelativehumidity": 43, + "compressorpower": 800, + "fanpower": 150, + "auxpower": 0, + "looppumppower": 50, + "actualcompressorspeed": 1200, + "airflowcurrentspeed": 850 +} diff --git a/tests/components/waterfurnace/test_config_flow.py b/tests/components/waterfurnace/test_config_flow.py new file mode 100644 index 00000000000..7708d3e4ac9 --- /dev/null +++ b/tests/components/waterfurnace/test_config_flow.py @@ -0,0 +1,224 @@ +"""Test the WaterFurnace config flow.""" + +from unittest.mock import AsyncMock, Mock + +import pytest +from waterfurnace.waterfurnace import WFCredentialError, WFException + +from homeassistant.components.waterfurnace.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_user_flow_success( + hass: HomeAssistant, mock_waterfurnace_client: Mock, mock_setup_entry: AsyncMock +) -> None: + """Test successful user flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "test_user", CONF_PASSWORD: "test_password"}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "WaterFurnace test_user" + assert result["data"] == { + CONF_USERNAME: "test_user", + CONF_PASSWORD: "test_password", + } + assert result["result"].unique_id == "TEST_GWID_12345" + + # Verify login was called (once during config flow, once during setup) + assert mock_waterfurnace_client.login.called + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (WFCredentialError("Invalid credentials"), "invalid_auth"), + (WFException("Connection failed"), "cannot_connect"), + (Exception("Unexpected error"), "unknown"), + ], +) +async def test_user_flow_exceptions( + hass: HomeAssistant, + mock_waterfurnace_client: Mock, + mock_setup_entry: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test user flow with invalid credentials.""" + mock_waterfurnace_client.login.side_effect = exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "bad_user", CONF_PASSWORD: "bad_password"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + # Verify we can recover from the error + mock_waterfurnace_client.login.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "test_user", CONF_PASSWORD: "test_password"}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_user_flow_no_gwid( + hass: HomeAssistant, mock_waterfurnace_client: Mock, mock_setup_entry: AsyncMock +) -> None: + """Test user flow with invalid credentials.""" + mock_waterfurnace_client.gwid = None + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "bad_user", + CONF_PASSWORD: "bad_password", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + + mock_waterfurnace_client.gwid = "TEST_GWID_12345" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test_user", + CONF_PASSWORD: "test_password", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_user_flow_already_configured( + hass: HomeAssistant, + mock_waterfurnace_client: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test user flow when device is already configured.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test_user", + CONF_PASSWORD: "test_password", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_import_flow_success( + hass: HomeAssistant, mock_waterfurnace_client: Mock, mock_setup_entry: AsyncMock +) -> None: + """Test successful import flow from YAML.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_USERNAME: "test_user", CONF_PASSWORD: "test_password"}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "WaterFurnace test_user" + assert result["data"] == { + CONF_USERNAME: "test_user", + CONF_PASSWORD: "test_password", + } + assert result["result"].unique_id == "TEST_GWID_12345" + + +async def test_import_flow_already_configured( + hass: HomeAssistant, + mock_waterfurnace_client: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test import flow when device is already configured.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_USERNAME: "test_user", CONF_PASSWORD: "test_password"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + ("exception", "reason"), + [ + (WFCredentialError("Invalid credentials"), "invalid_auth"), + (WFException("Connection failed"), "cannot_connect"), + (Exception("Unexpected error"), "unknown"), + ], +) +async def test_import_flow_exceptions( + hass: HomeAssistant, + mock_waterfurnace_client: Mock, + exception: Exception, + reason: str, +) -> None: + """Test import flow with connection error.""" + mock_waterfurnace_client.login.side_effect = exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_USERNAME: "test_user", CONF_PASSWORD: "test_password"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == reason + + +async def test_import_flow_no_gwid( + hass: HomeAssistant, mock_waterfurnace_client: Mock +) -> None: + """Test import flow with connection error.""" + mock_waterfurnace_client.gwid = None + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_USERNAME: "test_user", CONF_PASSWORD: "test_password"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "cannot_connect"