diff --git a/homeassistant/components/intellifire/config_flow.py b/homeassistant/components/intellifire/config_flow.py index 869504d22a2..a8d9fa135d2 100644 --- a/homeassistant/components/intellifire/config_flow.py +++ b/homeassistant/components/intellifire/config_flow.py @@ -1,6 +1,7 @@ """Config flow for IntelliFire integration.""" from __future__ import annotations +from dataclasses import dataclass from typing import Any from aiohttp import ClientConnectionError @@ -8,6 +9,7 @@ from intellifire4py import AsyncUDPFireplaceFinder, IntellifireAsync import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.const import CONF_HOST from homeassistant.data_entry_flow import FlowResult @@ -18,6 +20,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) MANUAL_ENTRY_STRING = "IP Address" # Simplified so it does not have to be translated +@dataclass +class DiscoveredHostInfo: + """Host info for discovery.""" + + ip: str + serial: str | None + + async def validate_host_input(host: str) -> str: """Validate the user input allows us to connect. @@ -39,7 +49,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the Config Flow Handler.""" self._config_context = {} - self._not_configured_hosts: list[str] = [] + self._not_configured_hosts: list[DiscoveredHostInfo] = [] + self._discovered_host: DiscoveredHostInfo async def _find_fireplaces(self): """Perform UDP discovery.""" @@ -52,7 +63,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } self._not_configured_hosts = [ - ip for ip in discovered_hosts if ip not in configured_hosts + DiscoveredHostInfo(ip, None) + for ip in discovered_hosts + if ip not in configured_hosts ] LOGGER.debug("Discovered Hosts: %s", discovered_hosts) LOGGER.debug("Configured Hosts: %s", configured_hosts) @@ -62,7 +75,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Validate and create the entry.""" self._async_abort_entries_match({CONF_HOST: host}) serial = await validate_host_input(host) - await self.async_set_unique_id(serial) + await self.async_set_unique_id(serial, raise_on_progress=False) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) return self.async_create_entry( title=f"Fireplace {serial}", @@ -108,7 +121,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data_schema=vol.Schema( { vol.Required(CONF_HOST): vol.In( - self._not_configured_hosts + [MANUAL_ENTRY_STRING] + [host.ip for host in self._not_configured_hosts] + + [MANUAL_ENTRY_STRING] ) } ), @@ -127,3 +141,44 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_pick_device() LOGGER.debug("Running Step: manual_device_entry") return await self.async_step_manual_device_entry() + + async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: + """Handle DHCP Discovery.""" + + # Run validation logic on ip + host = discovery_info.ip + + self._async_abort_entries_match({CONF_HOST: host}) + try: + serial = await validate_host_input(host) + except (ConnectionError, ClientConnectionError): + return self.async_abort(reason="not_intellifire_device") + + await self.async_set_unique_id(serial) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + self._discovered_host = DiscoveredHostInfo(ip=host, serial=serial) + + placeholders = {CONF_HOST: host, "serial": serial} + self.context["title_placeholders"] = placeholders + self._set_confirm_only() + + return await self.async_step_dhcp_confirm() + + async def async_step_dhcp_confirm(self, user_input=None): + """Attempt to confirm.""" + + # Add the hosts one by one + host = self._discovered_host.ip + serial = self._discovered_host.serial + + if user_input is None: + # Show the confirmation dialog + return self.async_show_form( + step_id="dhcp_confirm", + description_placeholders={CONF_HOST: host, "serial": serial}, + ) + + return self.async_create_entry( + title=f"Fireplace {serial}", + data={CONF_HOST: host}, + ) diff --git a/homeassistant/components/intellifire/manifest.json b/homeassistant/components/intellifire/manifest.json index 75d4ee2e75f..5809748787c 100644 --- a/homeassistant/components/intellifire/manifest.json +++ b/homeassistant/components/intellifire/manifest.json @@ -6,5 +6,7 @@ "requirements": ["intellifire4py==1.0.1"], "codeowners": ["@jeeftor"], "iot_class": "local_polling", - "loggers": ["intellifire4py"] + "loggers": ["intellifire4py"], + "dhcp": [{"hostname": "zentrios-*"}] + } diff --git a/homeassistant/components/intellifire/strings.json b/homeassistant/components/intellifire/strings.json index d5d3f344c8e..a85b807c4c0 100644 --- a/homeassistant/components/intellifire/strings.json +++ b/homeassistant/components/intellifire/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{serial} ({host})", "step": { "manual_device_entry": { "description": "Local Configuration", @@ -7,6 +8,9 @@ "host": "[%key:common::config_flow::data::host%]" } }, + "dhcp_confirm": { + "description": "Do you want to setup {host}\nSerial: {serial}?" + }, "pick_device": { "data": { "host": "[%key:common::config_flow::data::host%]" @@ -17,7 +21,8 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "not_intellifire_device":"Not an IntelliFire Device." } } } diff --git a/homeassistant/components/intellifire/translations/en.json b/homeassistant/components/intellifire/translations/en.json index 0f7538e7413..844d77427ca 100644 --- a/homeassistant/components/intellifire/translations/en.json +++ b/homeassistant/components/intellifire/translations/en.json @@ -1,27 +1,28 @@ { "config": { - "abort": { - "already_configured": "Device is already configured" - }, - "error": { - "cannot_connect": "Could not connect to a fireplace endpoint at url: http://{host}/poll\nVerify IP address and try again" - }, - "step": { - "manual_device_entry": { - "title": "IntelliFire - Local Config", - "description": "Enter the IP address of the IntelliFire unit on your local network.", - "data": { - "host": "Host (IP Address)" - } + "abort": { + "already_configured": "Device is already configured", + "not_intellifire_device": "Not an IntelliFire Device." }, - "user": { - "description": "Username and password are the same information used in your IntelliFire Android/iOS application.", - "title": "IntelliFire Config" + "error": { + "cannot_connect": "Failed to connect" }, - "pick_device": { - "title": "Device Selection", - "description": "The following IntelliFire devices were discovered. Please select which you wish to configure." + "flow_title": "{serial} ({host})", + "step": { + "dhcp_confirm": { + "description": "Do you want to setup {host}\nSerial: {serial}?" + }, + "manual_device_entry": { + "data": { + "host": "Host" + }, + "description": "Local Configuration" + }, + "pick_device": { + "data": { + "host": "Host" + } + } } - } } - } \ No newline at end of file +} \ No newline at end of file diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 8633534e976..4b4c2d7b979 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -49,6 +49,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'hunterdouglas_powerview', 'hostname': 'hunter*', 'macaddress': '002674*'}, + {'domain': 'intellifire', 'hostname': 'zentrios-*'}, {'domain': 'isy994', 'registered_devices': True}, {'domain': 'isy994', 'hostname': 'isy*', 'macaddress': '0021B9*'}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': '48A2E6*'}, diff --git a/tests/components/intellifire/test_config_flow.py b/tests/components/intellifire/test_config_flow.py index 2e130bfa14e..1283c9db0b2 100644 --- a/tests/components/intellifire/test_config_flow.py +++ b/tests/components/intellifire/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from homeassistant import config_entries +from homeassistant.components import dhcp from homeassistant.components.intellifire.config_flow import MANUAL_ENTRY_STRING from homeassistant.components.intellifire.const import DOMAIN from homeassistant.const import CONF_HOST @@ -203,3 +204,53 @@ async def test_picker_already_discovered( assert result2["title"] == "Fireplace 12345" assert result2["data"] == {CONF_HOST: "192.168.1.4"} assert len(mock_setup_entry.mock_calls) == 2 + + +async def test_dhcp_discovery_intellifire_device( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_intellifire_config_flow: MagicMock, +) -> None: + """Test successful DHCP Discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="zentrios-Test", + ), + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "dhcp_confirm" + result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "dhcp_confirm" + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], user_input={} + ) + assert result3["title"] == "Fireplace 12345" + assert result3["data"] == {"host": "1.1.1.1"} + + +async def test_dhcp_discovery_non_intellifire_device( + hass: HomeAssistant, + mock_intellifire_config_flow: MagicMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test failed DHCP Discovery.""" + + mock_intellifire_config_flow.poll.side_effect = ConnectionError + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="zentrios-Evil", + ), + ) + + assert result["type"] == "abort" + assert result["reason"] == "not_intellifire_device"