diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index 657d2fcca964..e4bc98cc0f87 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -9,10 +9,12 @@ from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkErr import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import dhcp from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.device_registry import format_mac from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DOMAIN from .exceptions import ReolinkException, UserNotAdmin @@ -87,6 +89,21 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user() return self.async_show_form(step_id="reauth_confirm") + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Handle discovery via dhcp.""" + mac_address = format_mac(discovery_info.macaddress) + await self.async_set_unique_id(mac_address) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) + + short_mac = mac_address[-8:].upper() + self.context["title_placeholders"] = { + "short_mac": short_mac, + "ip_address": discovery_info.ip, + } + + self._host = discovery_info.ip + return await self.async_step_user() + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -95,6 +112,9 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): placeholders = {"error": ""} if user_input is not None: + if CONF_HOST not in user_input: + user_input[CONF_HOST] = self._host + host = ReolinkHost(self.hass, user_input, DEFAULT_OPTIONS) try: await host.async_init() @@ -144,9 +164,14 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): { vol.Required(CONF_USERNAME, default=self._username): str, vol.Required(CONF_PASSWORD, default=self._password): str, - vol.Required(CONF_HOST, default=self._host): str, } ) + if self._host is None or errors: + data_schema = data_schema.extend( + { + vol.Required(CONF_HOST, default=self._host): str, + } + ) if errors: data_schema = data_schema.extend( { diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 1b746d98761c..88e2e3b77309 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -7,5 +7,11 @@ "dependencies": ["webhook"], "codeowners": ["@starkillerOG"], "iot_class": "local_polling", - "loggers": ["reolink_aio"] + "loggers": ["reolink_aio"], + "dhcp": [ + { + "hostname": "reolink*", + "macaddress": "EC71DB*" + } + ] } diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 1c82a43c8a20..8cd78e2ed7b9 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{short_mac} ({ip_address})", "step": { "user": { "description": "{error}", diff --git a/homeassistant/components/reolink/translations/en.json b/homeassistant/components/reolink/translations/en.json index beb366e8b396..7ea0e2df1e8f 100644 --- a/homeassistant/components/reolink/translations/en.json +++ b/homeassistant/components/reolink/translations/en.json @@ -11,6 +11,7 @@ "not_admin": "User needs to be admin, user ''{username}'' has authorisation level ''{userlevel}''", "unknown": "Unexpected error" }, + "flow_title": "{short_mac} ({ip_address})", "step": { "reauth_confirm": { "description": "The Reolink integration needs to re-authenticate your connection details", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index d1aa50a0faf7..8956085a5abf 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -384,6 +384,11 @@ DHCP: list[dict[str, str | bool]] = [ "domain": "rainforest_eagle", "macaddress": "D8D5B9*", }, + { + "domain": "reolink", + "hostname": "reolink*", + "macaddress": "EC71DB*", + }, { "domain": "ring", "hostname": "ring*", diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index 090ae6f694ba..36c1862eaeae 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -6,6 +6,7 @@ import pytest from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkError from homeassistant import config_entries, data_entry_flow +from homeassistant.components import dhcp from homeassistant.components.reolink import const from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME @@ -314,7 +315,7 @@ async def test_reauth(hass): data=config_entry.data, ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -322,21 +323,94 @@ async def test_reauth(hass): {}, ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_HOST: TEST_HOST2, CONF_USERNAME: TEST_USERNAME2, CONF_PASSWORD: TEST_PASSWORD2, }, ) - assert result["type"] == "abort" + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" - assert config_entry.data[CONF_HOST] == TEST_HOST2 + assert config_entry.data[CONF_HOST] == TEST_HOST assert config_entry.data[CONF_USERNAME] == TEST_USERNAME2 assert config_entry.data[CONF_PASSWORD] == TEST_PASSWORD2 + + +async def test_dhcp_flow(hass): + """Successful flow from DHCP discovery.""" + dhcp_data = dhcp.DhcpServiceInfo( + ip=TEST_HOST, + hostname="Reolink", + macaddress=TEST_MAC, + ) + + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + + assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_NVR_NAME + assert result["data"] == { + CONF_HOST: TEST_HOST, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_PORT: TEST_PORT, + const.CONF_USE_HTTPS: TEST_USE_HTTPS, + } + assert result["options"] == { + const.CONF_PROTOCOL: DEFAULT_PROTOCOL, + } + + +async def test_dhcp_abort_flow(hass): + """Test dhcp discovery aborts if already configured.""" + config_entry = MockConfigEntry( + domain=const.DOMAIN, + unique_id=format_mac(TEST_MAC), + data={ + CONF_HOST: TEST_HOST, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_PORT: TEST_PORT, + const.CONF_USE_HTTPS: TEST_USE_HTTPS, + }, + options={ + const.CONF_PROTOCOL: DEFAULT_PROTOCOL, + }, + title=TEST_NVR_NAME, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + dhcp_data = dhcp.DhcpServiceInfo( + ip=TEST_HOST, + hostname="Reolink", + macaddress=TEST_MAC, + ) + + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data + ) + + assert result["type"] is data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured"