Add dhcp discovery to Roborock (#141148)

* Add discovery to Roborock

* Update homeassistant/components/roborock/config_flow.py

Co-authored-by: Allen Porter <allen.porter@gmail.com>

* MR comments

* go back to removing the ":"

* change method of getting devices

---------

Co-authored-by: Allen Porter <allen.porter@gmail.com>
This commit is contained in:
Luke Lashley 2025-03-23 00:21:43 -04:00 committed by GitHub
parent ddd67a7e58
commit e2e80a850c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 121 additions and 8 deletions

View File

@ -28,7 +28,9 @@ from homeassistant.config_entries import (
)
from homeassistant.const import CONF_USERNAME
from homeassistant.core import callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from .const import (
CONF_BASE_URL,
@ -137,6 +139,22 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle a flow started by a dhcp discovery."""
device_registry = dr.async_get(self.hass)
device = device_registry.async_get_device(
connections={
(dr.CONNECTION_NETWORK_MAC, dr.format_mac(discovery_info.macaddress))
}
)
if device is not None and any(
identifier[0] == DOMAIN for identifier in device.identifiers
):
return self.async_abort(reason="already_configured")
return await self.async_step_user()
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:

View File

@ -129,7 +129,9 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
self.current_map: int | None = None
if mac := self.roborock_device_info.network_info.mac:
self.device_info[ATTR_CONNECTIONS] = {(dr.CONNECTION_NETWORK_MAC, mac)}
self.device_info[ATTR_CONNECTIONS] = {
(dr.CONNECTION_NETWORK_MAC, dr.format_mac(mac))
}
# Maps from map flag to map name
self.maps: dict[int, RoborockMapInfo] = {}
self._home_data_rooms = {str(room.id): room.name for room in home_data_rooms}

View File

@ -3,6 +3,17 @@
"name": "Roborock",
"codeowners": ["@Lash-L", "@allenporter"],
"config_flow": true,
"dhcp": [
{
"macaddress": "249E7D*"
},
{
"macaddress": "B04A39*"
},
{
"hostname": "roborock-*"
}
],
"documentation": "https://www.home-assistant.io/integrations/roborock",
"iot_class": "local_polling",
"loggers": ["roborock"],

View File

@ -34,9 +34,7 @@ rules:
# Gold
devices: done
diagnostics: done
discovery:
status: todo
comment: Determine if these devices can support discovery
discovery: done
discovery-update-info:
status: exempt
comment: Devices do not support discovery.

View File

@ -498,6 +498,18 @@ DHCP: Final[list[dict[str, str | bool]]] = [
"hostname": "ring*",
"macaddress": "341513*",
},
{
"domain": "roborock",
"macaddress": "249E7D*",
},
{
"domain": "roborock",
"macaddress": "B04A39*",
},
{
"domain": "roborock",
"hostname": "roborock-*",
},
{
"domain": "roomba",
"hostname": "irobot-*",

View File

@ -229,7 +229,13 @@ async def setup_entry(
@pytest.fixture(autouse=True)
async def cleanup_map_storage(
async def cleanup_map_storage(cleanup_map_storage_manual) -> Generator[pathlib.Path]:
"""Test cleanup, remove any map storage persisted during the test."""
return cleanup_map_storage_manual
@pytest.fixture
async def cleanup_map_storage_manual(
hass: HomeAssistant, mock_roborock_entry: MockConfigEntry
) -> Generator[pathlib.Path]:
"""Test cleanup, remove any map storage persisted during the test."""

View File

@ -1120,10 +1120,10 @@ PROP = DeviceProp(
)
NETWORK_INFO = NetworkInfo(
ip="123.232.12.1", ssid="wifi", mac="ac:cc:cc:cc:cc", bssid="bssid", rssi=90
ip="123.232.12.1", ssid="wifi", mac="ac:cc:cc:cc:cc:cc", bssid="bssid", rssi=90
)
NETWORK_INFO_2 = NetworkInfo(
ip="123.232.12.2", ssid="wifi", mac="ac:cc:cc:cc:cd", bssid="bssid", rssi=90
ip="123.232.12.2", ssid="wifi", mac="ac:cc:cc:cc:cd:cc", bssid="bssid", rssi=90
)
MULTI_MAP_LIST = MultiMapsList.from_dict(

View File

@ -19,8 +19,9 @@ from homeassistant.components.roborock.const import CONF_ENTRY_CODE, DOMAIN, DRA
from homeassistant.const import CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from .mock_data import MOCK_CONFIG, USER_DATA, USER_EMAIL
from .mock_data import MOCK_CONFIG, NETWORK_INFO, USER_DATA, USER_EMAIL
from tests.common import MockConfigEntry
@ -281,3 +282,68 @@ async def test_account_already_configured(
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured_account"
async def test_discovery_not_setup(
hass: HomeAssistant,
bypass_api_fixture,
) -> None:
"""Handle the config flow and make sure it succeeds."""
with (
patch("homeassistant.components.roborock.async_setup_entry", return_value=True),
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data=DhcpServiceInfo(
ip=NETWORK_INFO.ip,
macaddress=NETWORK_INFO.mac.replace(":", ""),
hostname="roborock-vacuum-a72",
),
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
with patch(
"homeassistant.components.roborock.config_flow.RoborockApiClient.request_code"
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_USERNAME: USER_EMAIL}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "code"
assert result["errors"] == {}
with patch(
"homeassistant.components.roborock.config_flow.RoborockApiClient.code_login",
return_value=USER_DATA,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_ENTRY_CODE: "123456"}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == USER_EMAIL
assert result["data"] == MOCK_CONFIG
assert result["result"]
async def test_discovery_already_setup(
hass: HomeAssistant,
bypass_api_fixture,
mock_roborock_entry: MockConfigEntry,
cleanup_map_storage_manual,
) -> None:
"""Handle aborting if the device is already setup."""
await hass.config_entries.async_setup(mock_roborock_entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data=DhcpServiceInfo(
ip=NETWORK_INFO.ip,
macaddress=NETWORK_INFO.mac.replace(":", ""),
hostname="roborock-vacuum-a72",
),
)
assert result["type"] is FlowResultType.ABORT