Add MAC connection through DHCP discovery to Home Connect devices (#144611)

* Add MAC connection through DHCP discovery to Home Connect devices

* Update snapshots
This commit is contained in:
J. Diego Rodríguez Royo 2025-05-12 21:11:12 +02:00 committed by GitHub
parent b5445c0061
commit 15a4514c7d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 123 additions and 50 deletions

View File

@ -8,7 +8,8 @@ import jwt
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers import config_entry_oauth2_flow, device_registry as dr
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from .const import DOMAIN from .const import DOMAIN
@ -58,3 +59,22 @@ class OAuth2FlowHandler(
) )
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
return await super().async_oauth_create_entry(data) return await super().async_oauth_create_entry(data)
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle a DHCP discovery."""
device_registry = dr.async_get(self.hass)
if device_entry := device_registry.async_get_device(
identifiers={
(DOMAIN, discovery_info.hostname),
(DOMAIN, discovery_info.hostname.split("-")[-1]),
}
):
device_registry.async_update_device(
device_entry.id,
new_connections={
(dr.CONNECTION_NETWORK_MAC, discovery_info.macaddress)
},
)
return await super().async_step_dhcp(discovery_info)

View File

@ -97,7 +97,7 @@
"connected": true, "connected": true,
"type": "Hob", "type": "Hob",
"enumber": "HCS000000/05", "enumber": "HCS000000/05",
"haId": "BOSCH-HCS000000-D00000000005" "haId": "BOSCH-HCS000000-68A40E000000"
}, },
{ {
"name": "CookProcessor", "name": "CookProcessor",
@ -106,7 +106,7 @@
"connected": true, "connected": true,
"type": "CookProcessor", "type": "CookProcessor",
"enumber": "HCS000000/06", "enumber": "HCS000000/06",
"haId": "BOSCH-HCS000000-D00000000006" "haId": "123456789012345678"
}, },
{ {
"name": "DNE", "name": "DNE",

View File

@ -1,6 +1,26 @@
# serializer version: 1 # serializer version: 1
# name: test_async_get_config_entry_diagnostics # name: test_async_get_config_entry_diagnostics
dict({ dict({
'123456789012345678': dict({
'brand': 'BOSCH',
'connected': True,
'e_number': 'HCS000000/06',
'ha_id': '123456789012345678',
'name': 'CookProcessor',
'programs': list([
]),
'settings': dict({
}),
'status': dict({
'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed',
'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready',
'BSH.Common.Status.RemoteControlActive': True,
'BSH.Common.Status.RemoteControlStartAllowed': True,
'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open',
}),
'type': 'CookProcessor',
'vib': 'HCS000006',
}),
'BOSCH-000000000-000000000000': dict({ 'BOSCH-000000000-000000000000': dict({
'brand': 'BOSCH', 'brand': 'BOSCH',
'connected': True, 'connected': True,
@ -21,6 +41,26 @@
'type': 'DNE', 'type': 'DNE',
'vib': 'HCS000000', 'vib': 'HCS000000',
}), }),
'BOSCH-HCS000000-68A40E000000': dict({
'brand': 'BOSCH',
'connected': True,
'e_number': 'HCS000000/05',
'ha_id': 'BOSCH-HCS000000-68A40E000000',
'name': 'Hob',
'programs': list([
]),
'settings': dict({
}),
'status': dict({
'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed',
'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready',
'BSH.Common.Status.RemoteControlActive': True,
'BSH.Common.Status.RemoteControlStartAllowed': True,
'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open',
}),
'type': 'Hob',
'vib': 'HCS000005',
}),
'BOSCH-HCS000000-D00000000001': dict({ 'BOSCH-HCS000000-D00000000001': dict({
'brand': 'BOSCH', 'brand': 'BOSCH',
'connected': True, 'connected': True,
@ -114,46 +154,6 @@
'type': 'Hood', 'type': 'Hood',
'vib': 'HCS000004', 'vib': 'HCS000004',
}), }),
'BOSCH-HCS000000-D00000000005': dict({
'brand': 'BOSCH',
'connected': True,
'e_number': 'HCS000000/05',
'ha_id': 'BOSCH-HCS000000-D00000000005',
'name': 'Hob',
'programs': list([
]),
'settings': dict({
}),
'status': dict({
'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed',
'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready',
'BSH.Common.Status.RemoteControlActive': True,
'BSH.Common.Status.RemoteControlStartAllowed': True,
'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open',
}),
'type': 'Hob',
'vib': 'HCS000005',
}),
'BOSCH-HCS000000-D00000000006': dict({
'brand': 'BOSCH',
'connected': True,
'e_number': 'HCS000000/06',
'ha_id': 'BOSCH-HCS000000-D00000000006',
'name': 'CookProcessor',
'programs': list([
]),
'settings': dict({
}),
'status': dict({
'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed',
'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready',
'BSH.Common.Status.RemoteControlActive': True,
'BSH.Common.Status.RemoteControlStartAllowed': True,
'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open',
}),
'type': 'CookProcessor',
'vib': 'HCS000006',
}),
'BOSCH-HCS01OVN1-43E0065FE245': dict({ 'BOSCH-HCS01OVN1-43E0065FE245': dict({
'brand': 'BOSCH', 'brand': 'BOSCH',
'connected': True, 'connected': True,

View File

@ -1,9 +1,11 @@
"""Test the Home Connect config flow.""" """Test the Home Connect config flow."""
from collections.abc import Awaitable, Callable
from http import HTTPStatus from http import HTTPStatus
from unittest.mock import patch from unittest.mock import MagicMock, patch
from aiohomeconnect.const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN from aiohomeconnect.const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN
from aiohomeconnect.model import HomeAppliance
import pytest import pytest
from homeassistant import config_entries, setup from homeassistant import config_entries, setup
@ -11,7 +13,7 @@ from homeassistant.components.home_connect.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers import config_entry_oauth2_flow, device_registry as dr
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from .conftest import FAKE_ACCESS_TOKEN, FAKE_REFRESH_TOKEN from .conftest import FAKE_ACCESS_TOKEN, FAKE_REFRESH_TOKEN
@ -337,17 +339,17 @@ async def test_zeroconf_flow_already_setup(
@pytest.mark.usefixtures("current_request_with_host") @pytest.mark.usefixtures("current_request_with_host")
@pytest.mark.parametrize("dchp_discovery", DHCP_DISCOVERY) @pytest.mark.parametrize("dhcp_discovery", DHCP_DISCOVERY)
async def test_dhcp_flow( async def test_dhcp_flow(
hass: HomeAssistant, hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator, hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker, aioclient_mock: AiohttpClientMocker,
dchp_discovery: DhcpServiceInfo, dhcp_discovery: DhcpServiceInfo,
) -> None: ) -> None:
"""Test DHCP discovery.""" """Test DHCP discovery."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dchp_discovery DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_discovery
) )
state = config_entry_oauth2_flow._encode_jwt( state = config_entry_oauth2_flow._encode_jwt(
hass, hass,
@ -391,8 +393,6 @@ async def test_dhcp_flow(
@pytest.mark.usefixtures("current_request_with_host") @pytest.mark.usefixtures("current_request_with_host")
async def test_dhcp_flow_already_setup( async def test_dhcp_flow_already_setup(
hass: HomeAssistant, hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
) -> None: ) -> None:
"""Test DHCP discovery with already setup device.""" """Test DHCP discovery with already setup device."""
@ -403,3 +403,56 @@ async def test_dhcp_flow_already_setup(
) )
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"
@pytest.mark.usefixtures("current_request_with_host")
@pytest.mark.parametrize(
("dhcp_discovery", "appliance"),
[
(
DhcpServiceInfo(
ip="1.1.1.1",
hostname="bosch-cookprocessor-123456789012345678",
macaddress="c8:d7:78:00:00:00",
),
"CookProcessor",
),
(
DhcpServiceInfo(
ip="1.1.1.1",
hostname="BOSCH-HCS000000-68A40E000000",
macaddress="68:a4:0e:00:00:00",
),
"Hob",
),
],
indirect=["appliance"],
)
async def test_dhcp_flow_complete_device_information(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
client: MagicMock,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
dhcp_discovery: DhcpServiceInfo,
appliance: HomeAppliance,
) -> None:
"""Test DHCP discovery with complete device information."""
assert await integration_setup(client)
assert config_entry.state is ConfigEntryState.LOADED
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)})
assert device
assert device.connections == set()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_discovery
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)})
assert device
assert device.connections == {
(dr.CONNECTION_NETWORK_MAC, dhcp_discovery.macaddress)
}