diff --git a/homeassistant/components/senseme/config_flow.py b/homeassistant/components/senseme/config_flow.py index 8bf0eec3a96..151a251c8a2 100644 --- a/homeassistant/components/senseme/config_flow.py +++ b/homeassistant/components/senseme/config_flow.py @@ -8,6 +8,7 @@ from aiosenseme import SensemeDevice, async_get_device_by_ip_address import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import dhcp from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.typing import DiscoveryInfoType @@ -28,6 +29,19 @@ class SensemeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._discovered_devices: list[SensemeDevice] | None = None self._discovered_device: SensemeDevice | None = None + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Handle discovery via dhcp.""" + # If discovery is already running, it takes precedence since its more efficient + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + if device := await async_get_device_by_ip_address(discovery_info.ip): + device.stop() + if device is None or not device.uuid: + return self.async_abort(reason="cannot_connect") + await self.async_set_unique_id(device.uuid) + self._discovered_device = device + return await self.async_step_discovery_confirm() + async def async_step_discovery( self, discovery_info: DiscoveryInfoType ) -> FlowResult: diff --git a/homeassistant/components/senseme/manifest.json b/homeassistant/components/senseme/manifest.json index bb9942bc5f6..7eba9eb4bda 100644 --- a/homeassistant/components/senseme/manifest.json +++ b/homeassistant/components/senseme/manifest.json @@ -9,5 +9,6 @@ "codeowners": [ "@mikelawrence", "@bdraco" ], + "dhcp": [{"macaddress":"20F85E*"}], "iot_class": "local_push" } diff --git a/homeassistant/components/senseme/strings.json b/homeassistant/components/senseme/strings.json index 52d8afb443a..9241c9058ad 100644 --- a/homeassistant/components/senseme/strings.json +++ b/homeassistant/components/senseme/strings.json @@ -19,7 +19,8 @@ } }, "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": { "invalid_host": "[%key:common::config_flow::error::invalid_host%]", diff --git a/homeassistant/components/senseme/translations/en.json b/homeassistant/components/senseme/translations/en.json index 0136d4d29cb..cb21f8ff193 100644 --- a/homeassistant/components/senseme/translations/en.json +++ b/homeassistant/components/senseme/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect" }, "error": { "cannot_connect": "Failed to connect", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 5467571a0d4..6c9e0d87499 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -290,6 +290,10 @@ DHCP = [ "hostname": "sense-*", "macaddress": "A4D578*" }, + { + "domain": "senseme", + "macaddress": "20F85E*" + }, { "domain": "simplisafe", "hostname": "simplisafe*", diff --git a/tests/components/senseme/__init__.py b/tests/components/senseme/__init__.py index 2286b1ad890..5c586d88fd5 100644 --- a/tests/components/senseme/__init__.py +++ b/tests/components/senseme/__init__.py @@ -10,6 +10,7 @@ from homeassistant.components.senseme import config_flow MOCK_NAME = "Haiku Fan" MOCK_UUID = "77a6b7b3-925d-4695-a415-76d76dca4444" MOCK_ADDRESS = "127.0.0.1" +MOCK_MAC = "20:F8:5E:92:5A:75" device = MagicMock(auto_spec=SensemeDevice) device.async_update = AsyncMock() @@ -29,7 +30,7 @@ device.address = MOCK_ADDRESS device.get_device_info = { "name": MOCK_NAME, "uuid": MOCK_UUID, - "mac": "20:F8:5E:92:5A:75", + "mac": MOCK_ADDRESS, "address": MOCK_ADDRESS, "base_model": "FAN,HAIKU,HSERIES", "has_light": False, @@ -94,9 +95,14 @@ device2.get_device_info = { "is_light": False, } +device_no_uuid = MagicMock(auto_spec=SensemeDevice) +device_no_uuid.uuid = None + + MOCK_DEVICE = device MOCK_DEVICE_ALTERNATE_IP = device_alternate_ip MOCK_DEVICE2 = device2 +MOCK_DEVICE_NO_UUID = device_no_uuid def _patch_discovery(device=None, no_device=None): diff --git a/tests/components/senseme/test_config_flow.py b/tests/components/senseme/test_config_flow.py index 71ce385e543..93a42d1e8ab 100644 --- a/tests/components/senseme/test_config_flow.py +++ b/tests/components/senseme/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import patch from homeassistant import config_entries +from homeassistant.components import dhcp from homeassistant.components.senseme.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_ID from homeassistant.core import HomeAssistant @@ -16,12 +17,20 @@ from . import ( MOCK_DEVICE, MOCK_DEVICE2, MOCK_DEVICE_ALTERNATE_IP, + MOCK_DEVICE_NO_UUID, + MOCK_MAC, MOCK_UUID, _patch_discovery, ) from tests.common import MockConfigEntry +DHCP_DISCOVERY = dhcp.DhcpServiceInfo( + hostname="any", + ip=MOCK_ADDRESS, + macaddress=MOCK_MAC, +) + async def test_form_user(hass: HomeAssistant) -> None: """Test we get the form as a user.""" @@ -280,3 +289,77 @@ async def test_discovery_existing_device_ip_change(hass: HomeAssistant) -> None: assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" assert entry.data["info"]["address"] == "127.0.0.8" + + +async def test_dhcp_discovery_existing_config_entry(hass: HomeAssistant) -> None: + """Test dhcp discovery is aborted if there is an existing config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "info": MOCK_DEVICE2.get_device_info, + }, + unique_id=MOCK_DEVICE2.uuid, + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_dhcp_discovery(hass: HomeAssistant) -> None: + """Test we can setup a dhcp discovered device.""" + with _patch_discovery(), patch( + "homeassistant.components.senseme.config_flow.async_get_device_by_ip_address", + return_value=MOCK_DEVICE, + ), patch( + "homeassistant.components.senseme.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + assert result["type"] == RESULT_TYPE_FORM + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "device": MOCK_UUID, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "Haiku Fan" + assert result2["data"] == { + "info": MOCK_DEVICE.get_device_info, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_dhcp_discovery_cannot_connect(hass: HomeAssistant) -> None: + """Test we abort if we cannot cannot to a dhcp discovered device.""" + with _patch_discovery(), patch( + "homeassistant.components.senseme.config_flow.async_get_device_by_ip_address", + return_value=None, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def test_dhcp_discovery_cannot_connect_no_uuid(hass: HomeAssistant) -> None: + """Test we abort if the discovered device has no uuid.""" + with _patch_discovery(), patch( + "homeassistant.components.senseme.config_flow.async_get_device_by_ip_address", + return_value=MOCK_DEVICE_NO_UUID, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect"