Skip to content

Commit 6e01db0

Browse files
committed
Support importing legacy YAML config
Add support for importing legacy login information from YAML config file as part of the config flow upgrade process. Co-authored-by: Claude Sonnet 4.5 [email protected]
1 parent 62e047a commit 6e01db0

File tree

4 files changed

+170
-1
lines changed

4 files changed

+170
-1
lines changed

homeassistant/components/waterfurnace/__init__.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
import threading
88
import time
99

10+
import voluptuous as vol
1011
from waterfurnace.waterfurnace import WaterFurnace, WFCredentialError, WFException
1112

1213
from homeassistant.components import persistent_notification
14+
from homeassistant.config_entries import SOURCE_IMPORT
1315
from homeassistant.const import (
1416
CONF_PASSWORD,
1517
CONF_USERNAME,
@@ -18,8 +20,9 @@
1820
)
1921
from homeassistant.core import HomeAssistant, callback
2022
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
21-
from homeassistant.helpers import discovery
23+
from homeassistant.helpers import config_validation as cv, discovery
2224
from homeassistant.helpers.dispatcher import dispatcher_send
25+
from homeassistant.helpers.typing import ConfigType
2326

2427
from .models import WaterFurnaceConfigEntry
2528

@@ -34,6 +37,30 @@
3437
MAX_FAILS = 10
3538
NOTIFICATION_ID = "waterfurnace_website_notification"
3639
NOTIFICATION_TITLE = "WaterFurnace website status"
40+
CONFIG_SCHEMA = vol.Schema(
41+
{
42+
DOMAIN: vol.Schema(
43+
{
44+
vol.Required(CONF_USERNAME): cv.string,
45+
vol.Required(CONF_PASSWORD): cv.string,
46+
}
47+
)
48+
},
49+
extra=vol.ALLOW_EXTRA,
50+
)
51+
52+
53+
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
54+
"""Set up WaterFurnace from yaml configuration."""
55+
if DOMAIN in config:
56+
hass.async_create_task(
57+
hass.config_entries.flow.async_init(
58+
DOMAIN,
59+
context={"source": SOURCE_IMPORT},
60+
data=config[DOMAIN],
61+
)
62+
)
63+
return True
3764

3865

3966
async def async_setup_entry(

homeassistant/components/waterfurnace/config_flow.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,29 @@ async def async_step_reauth_confirm(
152152
errors=errors,
153153
)
154154

155+
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
156+
"""Handle import from YAML configuration."""
157+
try:
158+
info = await validate_input(self.hass, import_data)
159+
except (CannotConnect, InvalidAuth):
160+
_LOGGER.error(
161+
"Failed to import WaterFurnace configuration from YAML. "
162+
"Please verify your credentials and set up the integration via the UI"
163+
)
164+
return self.async_abort(reason="cannot_connect")
165+
except Exception:
166+
_LOGGER.exception("Unexpected error importing WaterFurnace configuration")
167+
return self.async_abort(reason="unknown")
168+
169+
# Set unique ID based on GWID
170+
await self.async_set_unique_id(info["gwid"])
171+
self._abort_if_unique_id_configured()
172+
173+
return self.async_create_entry(
174+
title=info["title"],
175+
data=import_data,
176+
)
177+
155178

156179
class CannotConnect(HomeAssistantError):
157180
"""Error to indicate we cannot connect."""

homeassistant/components/waterfurnace/strings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
"config": {
33
"abort": {
44
"already_configured": "This device is already configured",
5+
"cannot_connect": "Failed to import configuration from YAML. Please verify your credentials and set up the integration via the UI",
56
"no_devices": "No devices found on this account",
67
"reauth_successful": "Reauthentication successful",
8+
"unknown": "Failed to import configuration from YAML due to an unexpected error. Please set up the integration via the UI",
79
"wrong_account": "The credentials provided are for a different account"
810
},
911
"error": {

tests/components/waterfurnace/test_config_flow.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,3 +409,120 @@ async def test_reauth_flow_unexpected_error(
409409

410410
assert result["type"] is FlowResultType.FORM
411411
assert result["errors"] == {"base": "cannot_connect"}
412+
413+
414+
async def test_import_flow_success(
415+
hass: HomeAssistant, mock_waterfurnace_client: Mock
416+
) -> None:
417+
"""Test successful import flow from YAML."""
418+
result = cast(
419+
dict[str, Any],
420+
await hass.config_entries.flow.async_init(
421+
DOMAIN,
422+
context={"source": config_entries.SOURCE_IMPORT},
423+
data={
424+
CONF_USERNAME: "test_user",
425+
CONF_PASSWORD: "test_password",
426+
},
427+
),
428+
)
429+
430+
assert result["type"] is FlowResultType.CREATE_ENTRY
431+
assert result["title"] == "WaterFurnace TEST_GWID_12345"
432+
assert result["data"] == {
433+
CONF_USERNAME: "test_user",
434+
CONF_PASSWORD: "test_password",
435+
}
436+
assert result["result"].unique_id == "TEST_GWID_12345"
437+
438+
439+
async def test_import_flow_already_configured(
440+
hass: HomeAssistant,
441+
mock_waterfurnace_client: Mock,
442+
mock_config_entry: MockConfigEntry,
443+
) -> None:
444+
"""Test import flow when device is already configured."""
445+
mock_config_entry.add_to_hass(hass)
446+
447+
result = cast(
448+
dict[str, Any],
449+
await hass.config_entries.flow.async_init(
450+
DOMAIN,
451+
context={"source": config_entries.SOURCE_IMPORT},
452+
data={
453+
CONF_USERNAME: "test_user",
454+
CONF_PASSWORD: "test_password",
455+
},
456+
),
457+
)
458+
459+
assert result["type"] is FlowResultType.ABORT
460+
assert result["reason"] == "already_configured"
461+
462+
463+
async def test_import_flow_cannot_connect(
464+
hass: HomeAssistant, mock_waterfurnace_client: Mock
465+
) -> None:
466+
"""Test import flow with connection error."""
467+
mock_waterfurnace_client.login.side_effect = WFException("Connection failed")
468+
469+
result = cast(
470+
dict[str, Any],
471+
await hass.config_entries.flow.async_init(
472+
DOMAIN,
473+
context={"source": config_entries.SOURCE_IMPORT},
474+
data={
475+
CONF_USERNAME: "test_user",
476+
CONF_PASSWORD: "test_password",
477+
},
478+
),
479+
)
480+
481+
assert result["type"] is FlowResultType.ABORT
482+
assert result["reason"] == "cannot_connect"
483+
484+
485+
async def test_import_flow_invalid_auth(
486+
hass: HomeAssistant, mock_waterfurnace_client: Mock
487+
) -> None:
488+
"""Test import flow with invalid credentials."""
489+
mock_waterfurnace_client.login.side_effect = WFCredentialError(
490+
"Invalid credentials"
491+
)
492+
493+
result = cast(
494+
dict[str, Any],
495+
await hass.config_entries.flow.async_init(
496+
DOMAIN,
497+
context={"source": config_entries.SOURCE_IMPORT},
498+
data={
499+
CONF_USERNAME: "bad_user",
500+
CONF_PASSWORD: "bad_password",
501+
},
502+
),
503+
)
504+
505+
assert result["type"] is FlowResultType.ABORT
506+
assert result["reason"] == "cannot_connect"
507+
508+
509+
async def test_import_flow_unexpected_error(
510+
hass: HomeAssistant, mock_waterfurnace_client: Mock
511+
) -> None:
512+
"""Test import flow with unexpected error."""
513+
mock_waterfurnace_client.login.side_effect = Exception("Unexpected error")
514+
515+
result = cast(
516+
dict[str, Any],
517+
await hass.config_entries.flow.async_init(
518+
DOMAIN,
519+
context={"source": config_entries.SOURCE_IMPORT},
520+
data={
521+
CONF_USERNAME: "test_user",
522+
CONF_PASSWORD: "test_password",
523+
},
524+
),
525+
)
526+
527+
assert result["type"] is FlowResultType.ABORT
528+
assert result["reason"] == "cannot_connect"

0 commit comments

Comments
 (0)