diff --git a/homeassistant/components/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json index 8a608a900be..e550d22e0ca 100644 --- a/homeassistant/components/home_connect/manifest.json +++ b/homeassistant/components/home_connect/manifest.json @@ -4,6 +4,20 @@ "codeowners": ["@DavidMStraub", "@Diegorro98", "@MartinHjelmare"], "config_flow": true, "dependencies": ["application_credentials", "repairs"], + "dhcp": [ + { + "hostname": "balay-*", + "macaddress": "C8D778*" + }, + { + "hostname": "(bosch|siemens)-*", + "macaddress": "68A40E*" + }, + { + "hostname": "siemens-*", + "macaddress": "38B4D3*" + } + ], "documentation": "https://www.home-assistant.io/integrations/home_connect", "iot_class": "cloud_push", "loggers": ["aiohomeconnect"], diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 88fb8e06d02..26302b0ac8b 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -258,6 +258,21 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "hostname": "guardian*", "macaddress": "30AEA4*", }, + { + "domain": "home_connect", + "hostname": "balay-*", + "macaddress": "C8D778*", + }, + { + "domain": "home_connect", + "hostname": "(bosch|siemens)-*", + "macaddress": "68A40E*", + }, + { + "domain": "home_connect", + "hostname": "siemens-*", + "macaddress": "38B4D3*", + }, { "domain": "homewizard", "registered_devices": True, diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py index a8929120acb..73aed382780 100644 --- a/tests/components/home_connect/test_config_flow.py +++ b/tests/components/home_connect/test_config_flow.py @@ -7,15 +7,12 @@ from aiohomeconnect.const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN import pytest from homeassistant import config_entries, setup -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) from homeassistant.components.home_connect.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from .conftest import FAKE_ACCESS_TOKEN, FAKE_REFRESH_TOKEN @@ -26,6 +23,39 @@ from tests.typing import ClientSessionGenerator CLIENT_ID = "1234" CLIENT_SECRET = "5678" +DHCP_DISCOVERY = ( + DhcpServiceInfo( + ip="1.1.1.1", + hostname="balay-dishwasher-000000000000000000", + macaddress="C8:D7:78:00:00:00", + ), + DhcpServiceInfo( + ip="1.1.1.1", + hostname="BOSCH-ABCDE1234-68A40E000000", + macaddress="68:A4:0E:00:00:00", + ), + DhcpServiceInfo( + ip="1.1.1.1", + hostname="SIEMENS-ABCDE1234-68A40E000000", + macaddress="68:A4:0E:00:00:00", + ), + DhcpServiceInfo( + ip="1.1.1.1", + hostname="SIEMENS-ABCDE1234-38B4D3000000", + macaddress="38:B4:D3:00:00:00", + ), + DhcpServiceInfo( + ip="1.1.1.1", + hostname="siemens-dishwasher-000000000000000000", + macaddress="68:A4:0E:00:00:00", + ), + DhcpServiceInfo( + ip="1.1.1.1", + hostname="siemens-dishwasher-000000000000000000", + macaddress="38:B4:D3:00:00:00", + ), +) + @pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( @@ -36,10 +66,6 @@ async def test_full_flow( """Check full flow.""" assert await setup.async_setup_component(hass, "home_connect", {}) - await async_import_client_credential( - hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET) - ) - result = await hass.config_entries.flow.async_init( "home_connect", context={"source": config_entries.SOURCE_USER} ) @@ -95,10 +121,6 @@ async def test_prevent_reconfiguring_same_account( assert await setup.async_setup_component(hass, "home_connect", {}) - await async_import_client_credential( - hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET) - ) - result = await hass.config_entries.flow.async_init( "home_connect", context={"source": config_entries.SOURCE_USER} ) @@ -135,7 +157,7 @@ async def test_prevent_reconfiguring_same_account( result = await hass.config_entries.flow.async_configure(result["flow_id"]) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -241,3 +263,143 @@ async def test_reauth_flow_with_different_account( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "wrong_account" + + +@pytest.mark.usefixtures("current_request_with_host") +async def test_zeroconf_flow( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test zeroconf flow.""" + assert await setup.async_setup_component(hass, "home_connect", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["type"] is FlowResultType.EXTERNAL_STEP + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == HTTPStatus.OK + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": FAKE_REFRESH_TOKEN, + "access_token": FAKE_ACCESS_TOKEN, + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch( + "homeassistant.components.home_connect.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert hass.config_entries.async_entry_for_domain_unique_id(DOMAIN, "1234567890") + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.usefixtures("current_request_with_host") +async def test_zeroconf_flow_already_setup( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + config_entry: MockConfigEntry, +) -> None: + """Test zeroconf discovery with already setup device.""" + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=DHCP_DISCOVERY[0], + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.usefixtures("current_request_with_host") +@pytest.mark.parametrize("dchp_discovery", DHCP_DISCOVERY) +async def test_dhcp_flow( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + dchp_discovery: DhcpServiceInfo, +) -> None: + """Test DHCP discovery.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dchp_discovery + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + assert result["type"] is FlowResultType.EXTERNAL_STEP + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == HTTPStatus.OK + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": FAKE_REFRESH_TOKEN, + "access_token": FAKE_ACCESS_TOKEN, + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch( + "homeassistant.components.home_connect.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert hass.config_entries.async_entry_for_domain_unique_id(DOMAIN, "1234567890") + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.usefixtures("current_request_with_host") +async def test_dhcp_flow_already_setup( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + config_entry: MockConfigEntry, +) -> None: + """Test DHCP discovery with already setup device.""" + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY[0] + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured"