diff --git a/homeassistant/components/fumis/config_flow.py b/homeassistant/components/fumis/config_flow.py index dafb5c14aa99..d5916a849fa2 100644 --- a/homeassistant/components/fumis/config_flow.py +++ b/homeassistant/components/fumis/config_flow.py @@ -22,6 +22,7 @@ from homeassistant.helpers.selector import ( TextSelectorConfig, TextSelectorType, ) +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from .const import DOMAIN, LOGGER @@ -29,6 +30,64 @@ from .const import DOMAIN, LOGGER class FumisFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Fumis config flow.""" + _discovered_mac: str + + async def async_step_dhcp( + self, discovery_info: DhcpServiceInfo + ) -> ConfigFlowResult: + """Handle DHCP discovery of a Fumis WiRCU module.""" + mac = discovery_info.macaddress.replace(":", "").replace("-", "").upper() + + await self.async_set_unique_id(format_mac(mac)) + self._abort_if_unique_id_configured() + + self._discovered_mac = mac + return await self.async_step_dhcp_confirm() + + async def async_step_dhcp_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle DHCP discovery confirmation.""" + errors: dict[str, str] = {} + + if user_input is not None: + fumis = Fumis( + mac=self._discovered_mac, + password=user_input[CONF_PIN], + session=async_get_clientsession(self.hass), + ) + try: + info = await fumis.update_info() + except FumisAuthenticationError: + errors[CONF_PIN] = "invalid_auth" + except FumisStoveOfflineError: + errors["base"] = "device_offline" + except FumisConnectionError: + errors["base"] = "cannot_connect" + except Exception: # noqa: BLE001 + LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=info.controller.model_name or "Fumis", + data={ + CONF_MAC: self._discovered_mac, + CONF_PIN: user_input[CONF_PIN], + }, + ) + + return self.async_show_form( + step_id="dhcp_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_PIN): TextSelector( + TextSelectorConfig(type=TextSelectorType.PASSWORD) + ), + } + ), + errors=errors, + ) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: diff --git a/homeassistant/components/fumis/manifest.json b/homeassistant/components/fumis/manifest.json index e3d6237626c7..d037fff422c0 100644 --- a/homeassistant/components/fumis/manifest.json +++ b/homeassistant/components/fumis/manifest.json @@ -3,6 +3,11 @@ "name": "Fumis", "codeowners": ["@frenck"], "config_flow": true, + "dhcp": [ + { + "macaddress": "0016D0*" + } + ], "documentation": "https://www.home-assistant.io/integrations/fumis", "integration_type": "device", "iot_class": "cloud_polling", diff --git a/homeassistant/components/fumis/quality_scale.yaml b/homeassistant/components/fumis/quality_scale.yaml index 1409a9ccf927..47ea341873bd 100644 --- a/homeassistant/components/fumis/quality_scale.yaml +++ b/homeassistant/components/fumis/quality_scale.yaml @@ -42,12 +42,10 @@ rules: # Gold devices: done diagnostics: todo - discovery: - status: todo - comment: DHCP discovery can be added. + discovery: done discovery-update-info: - status: todo - comment: DHCP discovery based update can be added. + status: exempt + comment: Cloud-only API, no local device information to update. docs-data-update: done docs-examples: done docs-known-limitations: done diff --git a/homeassistant/components/fumis/strings.json b/homeassistant/components/fumis/strings.json index 3b2a0ba142e1..3d72a1692366 100644 --- a/homeassistant/components/fumis/strings.json +++ b/homeassistant/components/fumis/strings.json @@ -11,6 +11,15 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "step": { + "dhcp_confirm": { + "data": { + "pin": "[%key:component::fumis::config::step::user::data::pin%]" + }, + "data_description": { + "pin": "[%key:component::fumis::config::step::user::data_description::pin%]" + }, + "description": "A Fumis WiRCU Wi-Fi module was discovered on your network. Enter the PIN code from the label on the module to set up your pellet stove." + }, "reauth_confirm": { "data": { "pin": "[%key:component::fumis::config::step::user::data::pin%]" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 97625caa89be..1935a1b618b7 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -266,6 +266,10 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "domain": "fully_kiosk", "registered_devices": True, }, + { + "domain": "fumis", + "macaddress": "0016D0*", + }, { "domain": "fyta", "hostname": "fyta*", diff --git a/tests/components/fumis/test_config_flow.py b/tests/components/fumis/test_config_flow.py index a9929aeb84d7..6e6a07183101 100644 --- a/tests/components/fumis/test_config_flow.py +++ b/tests/components/fumis/test_config_flow.py @@ -6,10 +6,11 @@ from fumis import FumisAuthenticationError, FumisConnectionError, FumisStoveOffl import pytest from homeassistant.components.fumis.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER from homeassistant.const import CONF_MAC, CONF_PIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from tests.common import MockConfigEntry @@ -205,3 +206,97 @@ async def test_reauth_flow_errors( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" + + +@pytest.mark.usefixtures("mock_fumis") +async def test_dhcp_discovery(hass: HomeAssistant) -> None: + """Test DHCP discovery of a Fumis WiRCU module.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + ip="192.168.1.2", + macaddress="0016d0aabbcc", + hostname="wircu", + ), + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "dhcp_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PIN: "1234"}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"][CONF_MAC] == "0016D0AABBCC" + assert result["data"][CONF_PIN] == "1234" + assert result["result"].unique_id == "00:16:d0:aa:bb:cc" + + +@pytest.mark.parametrize( + ("side_effect", "expected_error"), + [ + (FumisAuthenticationError, {CONF_PIN: "invalid_auth"}), + (FumisStoveOfflineError, {"base": "device_offline"}), + (FumisConnectionError, {"base": "cannot_connect"}), + (Exception, {"base": "unknown"}), + ], +) +async def test_dhcp_discovery_errors( + hass: HomeAssistant, + mock_fumis: MagicMock, + side_effect: type[Exception], + expected_error: dict[str, str], +) -> None: + """Test DHCP discovery with errors.""" + mock_fumis.update_info.side_effect = side_effect + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + ip="192.168.1.2", + macaddress="0016d0aabbcc", + hostname="wircu", + ), + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PIN: "1234"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == expected_error + + mock_fumis.update_info.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PIN: "1234"}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + + +@pytest.mark.usefixtures("mock_fumis") +async def test_dhcp_discovery_already_configured( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test DHCP discovery when the device is already configured.""" + mock_config_entry.add_to_hass(hass) + + discovery = DhcpServiceInfo( + ip="192.168.1.99", + macaddress="aabbccddeeff", + hostname="wircu", + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=discovery + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured"