From 7bc57173571be81027968ae786bd42d6e3ba85d9 Mon Sep 17 00:00:00 2001 From: Glenn de Haan Date: Sun, 25 Jan 2026 07:10:40 +0100 Subject: [PATCH] Add HDFury discovery (#161523) --- .../components/hdfury/config_flow.py | 42 ++++++++++ homeassistant/components/hdfury/manifest.json | 7 +- .../components/hdfury/quality_scale.yaml | 4 +- homeassistant/components/hdfury/strings.json | 7 +- homeassistant/generated/zeroconf.py | 12 +++ tests/components/hdfury/test_config_flow.py | 78 ++++++++++++++++++- 6 files changed, 145 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hdfury/config_flow.py b/homeassistant/components/hdfury/config_flow.py index 3b8e7d1cfc3..01852388849 100644 --- a/homeassistant/components/hdfury/config_flow.py +++ b/homeassistant/components/hdfury/config_flow.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from .const import DOMAIN @@ -15,6 +16,47 @@ from .const import DOMAIN class HDFuryConfigFlow(ConfigFlow, domain=DOMAIN): """Handle Config Flow for HDFury.""" + def __init__(self) -> None: + """Initialize the config flow.""" + self.data: dict[str, Any] = {} + + async def async_step_zeroconf( + self, discovery_info: ZeroconfServiceInfo + ) -> ConfigFlowResult: + """Handle zeroconf discovery.""" + self.data[CONF_HOST] = host = discovery_info.host + + serial = await self._validate_connection(host) + if serial is not None: + await self.async_set_unique_id(serial) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + + self.context["title_placeholders"] = { + CONF_HOST: self.data[CONF_HOST], + } + + return await self.async_step_discovery_confirm() + + return self.async_abort(reason="cannot_connect") + + async def async_step_discovery_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm discovery.""" + if user_input is not None: + return self.async_create_entry( + title=f"HDFury ({self.data[CONF_HOST]})", + data={CONF_HOST: self.data[CONF_HOST]}, + ) + + self._set_confirm_only() + return self.async_show_form( + step_id="discovery_confirm", + description_placeholders={ + CONF_HOST: self.data[CONF_HOST], + }, + ) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: diff --git a/homeassistant/components/hdfury/manifest.json b/homeassistant/components/hdfury/manifest.json index 44b230b8dc9..ddb5070d4c9 100644 --- a/homeassistant/components/hdfury/manifest.json +++ b/homeassistant/components/hdfury/manifest.json @@ -7,5 +7,10 @@ "integration_type": "device", "iot_class": "local_polling", "quality_scale": "silver", - "requirements": ["hdfury==1.4.2"] + "requirements": ["hdfury==1.4.2"], + "zeroconf": [ + { "name": "diva-*", "type": "_http._tcp.local." }, + { "name": "vertex2-*", "type": "_http._tcp.local." }, + { "name": "vrroom-*", "type": "_http._tcp.local." } + ] } diff --git a/homeassistant/components/hdfury/quality_scale.yaml b/homeassistant/components/hdfury/quality_scale.yaml index 02cae0ebd0c..ab135a9fd51 100644 --- a/homeassistant/components/hdfury/quality_scale.yaml +++ b/homeassistant/components/hdfury/quality_scale.yaml @@ -44,8 +44,8 @@ rules: # Gold devices: done diagnostics: done - discovery-update-info: todo - discovery: todo + discovery-update-info: done + discovery: done docs-data-update: todo docs-examples: todo docs-known-limitations: todo diff --git a/homeassistant/components/hdfury/strings.json b/homeassistant/components/hdfury/strings.json index d2f1746b211..9bd2ee20bac 100644 --- a/homeassistant/components/hdfury/strings.json +++ b/homeassistant/components/hdfury/strings.json @@ -1,12 +1,17 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, + "flow_title": "HDFury ({host})", "step": { + "discovery_confirm": { + "description": "Do you want to set up {host}?" + }, "user": { "data": { "host": "[%key:common::config_flow::data::host%]" diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 8d7f22d25ca..c643496f21d 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -581,6 +581,18 @@ ZEROCONF = { "domain": "eheimdigital", "name": "eheimdigital._http._tcp.local.", }, + { + "domain": "hdfury", + "name": "diva-*", + }, + { + "domain": "hdfury", + "name": "vertex2-*", + }, + { + "domain": "hdfury", + "name": "vrroom-*", + }, { "domain": "lektrico", "name": "lektrico*", diff --git a/tests/components/hdfury/test_config_flow.py b/tests/components/hdfury/test_config_flow.py index aeed0504600..5a2567bc80b 100644 --- a/tests/components/hdfury/test_config_flow.py +++ b/tests/components/hdfury/test_config_flow.py @@ -1,17 +1,31 @@ """Test the HDFury config flow.""" +from ipaddress import ip_address from unittest.mock import AsyncMock from hdfury import HDFuryError from homeassistant.components.hdfury.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import MockConfigEntry +ZEROCONF_DISCOVERY = ZeroconfServiceInfo( + ip_address=ip_address("192.168.1.123"), + ip_addresses=[ip_address("192.168.1.123")], + hostname="VRROOM-02.local.", + name="VRROOM-02._http._tcp.local.", + port=80, + type="_http._tcp.local.", + properties={ + "path": "/", + }, +) + async def test_async_step_user_gets_form_and_creates_entry( hass: HomeAssistant, @@ -94,3 +108,65 @@ async def test_successful_recovery_after_connection_error( CONF_HOST: "192.168.1.123", } assert result["result"].unique_id == "000123456789" + + +async def test_zeroconf_flow( + hass: HomeAssistant, + mock_hdfury_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test zeroconf flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZEROCONF_DISCOVERY, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "discovery_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_HOST: "192.168.1.123", + } + assert result["result"].unique_id == "000123456789" + + +async def test_zeroconf_flow_failure( + hass: HomeAssistant, + mock_hdfury_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test zeroconf flow failure.""" + + # Simulate a connection error by raising a HDFuryError + mock_hdfury_client.get_board.side_effect = HDFuryError() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZEROCONF_DISCOVERY, + ) + + await hass.async_block_till_done() + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + +async def test_zeroconf_flow_abort_duplicate( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test zeroconf flow aborts with duplicate.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZEROCONF_DISCOVERY, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured"