Make sure we can set up OAuth based integrations via discovery (#145144)

This commit is contained in:
Joost Lekkerkerker 2025-05-26 20:48:07 +02:00 committed by GitHub
parent a2b02537a6
commit e2a916ff9d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 130 additions and 7 deletions

View File

@ -11,6 +11,9 @@
"reauth_confirm": { "reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]", "title": "[%key:common::config_flow::title::reauth%]",
"description": "The Home Connect integration needs to re-authenticate your account" "description": "The Home Connect integration needs to re-authenticate your account"
},
"oauth_discovery": {
"description": "Home Assistant has found a Home Connect device on your network. Press **Submit** to continue setting up Home Connect."
} }
}, },
"abort": { "abort": {

View File

@ -7,6 +7,9 @@
"reauth_confirm": { "reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]", "title": "[%key:common::config_flow::title::reauth%]",
"description": "The Lyric integration needs to re-authenticate your account." "description": "The Lyric integration needs to re-authenticate your account."
},
"oauth_discovery": {
"description": "Home Assistant has found a Honeywell Lyric device on your network. Press **Submit** to continue setting up Honeywell Lyric."
} }
}, },
"abort": { "abort": {

View File

@ -13,6 +13,9 @@
"reauth_confirm": { "reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]", "title": "[%key:common::config_flow::title::reauth%]",
"description": "The Miele integration needs to re-authenticate your account" "description": "The Miele integration needs to re-authenticate your account"
},
"oauth_discovery": {
"description": "Home Assistant has found a Miele device on your network. Press **Submit** to continue setting up Miele."
} }
}, },
"abort": { "abort": {

View File

@ -31,6 +31,7 @@ from homeassistant.helpers.selector import (
SelectSelectorConfig, SelectSelectorConfig,
SelectSelectorMode, SelectSelectorMode,
) )
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.util import get_random_string from homeassistant.util import get_random_string
from . import api from . import api
@ -440,3 +441,9 @@ class NestFlowHandler(
if self._structure_config_title: if self._structure_config_title:
title = self._structure_config_title title = self._structure_config_title
return self.async_create_entry(title=title, data=self._data) return self.async_create_entry(title=title, data=self._data)
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle a flow initialized by discovery."""
return await self.async_step_user()

View File

@ -7,6 +7,9 @@
"reauth_confirm": { "reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]", "title": "[%key:common::config_flow::title::reauth%]",
"description": "The Spotify integration needs to re-authenticate with Spotify for account: {account}" "description": "The Spotify integration needs to re-authenticate with Spotify for account: {account}"
},
"oauth_discovery": {
"description": "Home Assistant has found Spotify on your network. Press **Submit** to continue setting up Spotify."
} }
}, },
"abort": { "abort": {

View File

@ -7,6 +7,9 @@
"reauth_confirm": { "reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]", "title": "[%key:common::config_flow::title::reauth%]",
"description": "The Withings integration needs to re-authenticate your account" "description": "The Withings integration needs to re-authenticate your account"
},
"oauth_discovery": {
"description": "Home Assistant has found a Withings device on your network. Press **Submit** to continue setting up Withings."
} }
}, },
"error": { "error": {

View File

@ -22,6 +22,7 @@ import time
from typing import Any, cast from typing import Any, cast
from aiohttp import ClientError, ClientResponseError, client, web from aiohttp import ClientError, ClientResponseError, client, web
from habluetooth import BluetoothServiceInfoBleak
import jwt import jwt
import voluptuous as vol import voluptuous as vol
from yarl import URL from yarl import URL
@ -34,6 +35,9 @@ from homeassistant.util.hass_dict import HassKey
from . import http from . import http
from .aiohttp_client import async_get_clientsession from .aiohttp_client import async_get_clientsession
from .network import NoURLAvailableError from .network import NoURLAvailableError
from .service_info.dhcp import DhcpServiceInfo
from .service_info.ssdp import SsdpServiceInfo
from .service_info.zeroconf import ZeroconfServiceInfo
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -493,6 +497,45 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta):
"""Handle a flow start.""" """Handle a flow start."""
return await self.async_step_pick_implementation(user_input) return await self.async_step_pick_implementation(user_input)
async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfoBleak
) -> config_entries.ConfigFlowResult:
"""Handle a flow initialized by Bluetooth discovery."""
return await self.async_step_oauth_discovery()
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> config_entries.ConfigFlowResult:
"""Handle a flow initialized by DHCP discovery."""
return await self.async_step_oauth_discovery()
async def async_step_homekit(
self, discovery_info: ZeroconfServiceInfo
) -> config_entries.ConfigFlowResult:
"""Handle a flow initialized by Homekit discovery."""
return await self.async_step_oauth_discovery()
async def async_step_ssdp(
self, discovery_info: SsdpServiceInfo
) -> config_entries.ConfigFlowResult:
"""Handle a flow initialized by SSDP discovery."""
return await self.async_step_oauth_discovery()
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> config_entries.ConfigFlowResult:
"""Handle a flow initialized by Zeroconf discovery."""
return await self.async_step_oauth_discovery()
async def async_step_oauth_discovery(
self, user_input: dict[str, Any] | None = None
) -> config_entries.ConfigFlowResult:
"""Handle a flow initialized by a discovery method."""
if user_input is not None:
return await self.async_step_user()
await self._async_handle_discovery_without_unique_id()
return self.async_show_form(step_id="oauth_discovery")
@classmethod @classmethod
def async_register_implementation( def async_register_implementation(
cls, hass: HomeAssistant, local_impl: LocalOAuth2Implementation cls, hass: HomeAssistant, local_impl: LocalOAuth2Implementation

View File

@ -279,6 +279,15 @@ async def test_zeroconf_flow(
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF} DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}
) )
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "oauth_discovery"
assert not result["errors"]
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{},
)
state = config_entry_oauth2_flow._encode_jwt( state = config_entry_oauth2_flow._encode_jwt(
hass, hass,
{ {
@ -351,6 +360,15 @@ async def test_dhcp_flow(
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_discovery DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_discovery
) )
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "oauth_discovery"
assert not result["errors"]
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{},
)
state = config_entry_oauth2_flow._encode_jwt( state = config_entry_oauth2_flow._encode_jwt(
hass, hass,
{ {

View File

@ -225,6 +225,16 @@ async def test_zeroconf_flow(
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF} DOMAIN, context={"source": SOURCE_ZEROCONF}
) )
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "oauth_discovery"
assert not result["errors"]
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{},
)
state = config_entry_oauth2_flow._encode_jwt( state = config_entry_oauth2_flow._encode_jwt(
hass, hass,
{ {

View File

@ -38,13 +38,6 @@ async def test_abort_if_no_configuration(hass: HomeAssistant) -> None:
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "missing_credentials" assert result["reason"] == "missing_credentials"
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=BLANK_ZEROCONF_INFO
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "missing_credentials"
async def test_zeroconf_abort_if_existing_entry(hass: HomeAssistant) -> None: async def test_zeroconf_abort_if_existing_entry(hass: HomeAssistant) -> None:
"""Check zeroconf flow aborts when an entry already exist.""" """Check zeroconf flow aborts when an entry already exist."""
@ -265,3 +258,18 @@ async def test_reauth_account_mismatch(
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_account_mismatch" assert result["reason"] == "reauth_account_mismatch"
async def test_zeroconf(hass: HomeAssistant) -> None:
"""Check zeroconf flow aborts when an entry already exist."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=BLANK_ZEROCONF_INFO
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "oauth_discovery"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "missing_credentials"

View File

@ -312,6 +312,15 @@ async def test_dhcp(
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_DHCP}, data=service_info DOMAIN, context={"source": SOURCE_DHCP}, data=service_info
) )
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "oauth_discovery"
assert not result["errors"]
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{},
)
state = config_entry_oauth2_flow._encode_jwt( state = config_entry_oauth2_flow._encode_jwt(
hass, hass,
{ {

View File

@ -397,6 +397,14 @@ async def test_step_discovery(
data=data_entry_flow.BaseServiceInfo(), data=data_entry_flow.BaseServiceInfo(),
) )
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "oauth_discovery"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={},
)
assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "pick_implementation" assert result["step_id"] == "pick_implementation"
@ -418,6 +426,11 @@ async def test_abort_discovered_multiple(
data=data_entry_flow.BaseServiceInfo(), data=data_entry_flow.BaseServiceInfo(),
) )
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={},
)
assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "pick_implementation" assert result["step_id"] == "pick_implementation"