Mark integrations as single_config_entry in manifest [a-i] (#128189)

* mark integrations as single_config_entry in manifest

* fix ecobee tests

* fix iaqualink test
This commit is contained in:
Michael 2024-10-12 08:59:57 +02:00 committed by GitHub
parent 1484a9c0ee
commit c50d0646ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 48 additions and 90 deletions

View File

@ -47,18 +47,12 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
return await self.async_step_config() return await self.async_step_config()
async def async_step_zeroconf( async def async_step_zeroconf(
self, discovery_info: zeroconf.ZeroconfServiceInfo self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle a flow initialized by zeroconf discovery.""" """Handle a flow initialized by zeroconf discovery."""
if self._async_in_progress() or self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
await self.async_set_unique_id(DOMAIN) await self.async_set_unique_id(DOMAIN)
return await self.async_step_confirm() return await self.async_step_confirm()

View File

@ -15,5 +15,6 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["casttube", "pychromecast"], "loggers": ["casttube", "pychromecast"],
"requirements": ["PyChromecast==14.0.4"], "requirements": ["PyChromecast==14.0.4"],
"single_config_entry": true,
"zeroconf": ["_googlecast._tcp.local."] "zeroconf": ["_googlecast._tcp.local."]
} }

View File

@ -12,9 +12,6 @@
} }
} }
}, },
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
},
"error": { "error": {
"invalid_known_hosts": "Known hosts must be a comma separated list of hosts." "invalid_known_hosts": "Known hosts must be a comma separated list of hosts."
} }

View File

@ -18,6 +18,4 @@ class CloudConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle the system step.""" """Handle the system step."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
return self.async_create_entry(title="Home Assistant Cloud", data={}) return self.async_create_entry(title="Home Assistant Cloud", data={})

View File

@ -8,5 +8,6 @@
"integration_type": "system", "integration_type": "system",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["hass_nabucasa"], "loggers": ["hass_nabucasa"],
"requirements": ["hass-nabucasa==0.81.1"] "requirements": ["hass-nabucasa==0.81.1"],
"single_config_entry": true
} }

View File

@ -1,10 +1,4 @@
{ {
"config": {
"step": {},
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
},
"system_health": { "system_health": {
"info": { "info": {
"can_reach_cert_server": "Reach certificate server", "can_reach_cert_server": "Reach certificate server",

View File

@ -118,9 +118,6 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle a flow initiated by the user.""" """Handle a flow initiated by the user."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
persistent_notification.async_dismiss(self.hass, "cloudflare_setup") persistent_notification.async_dismiss(self.hass, "cloudflare_setup")
errors: dict[str, str] = {} errors: dict[str, str] = {}

View File

@ -6,5 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/cloudflare", "documentation": "https://www.home-assistant.io/integrations/cloudflare",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pycfdns"], "loggers": ["pycfdns"],
"requirements": ["pycfdns==3.0.0"] "requirements": ["pycfdns==3.0.0"],
"single_config_entry": true
} }

View File

@ -34,8 +34,7 @@
"unknown": "[%key:common::config_flow::error::unknown%]" "unknown": "[%key:common::config_flow::error::unknown%]"
}, },
"abort": { "abort": {
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
} }
}, },
"services": { "services": {

View File

@ -39,9 +39,6 @@ class DemoConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
"""Set the config entry up from yaml.""" """Set the config entry up from yaml."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
return self.async_create_entry(title="Demo", data=import_data) return self.async_create_entry(title="Demo", data=import_data)

View File

@ -5,5 +5,6 @@
"dependencies": ["conversation", "group", "zone"], "dependencies": ["conversation", "group", "zone"],
"documentation": "https://www.home-assistant.io/integrations/demo", "documentation": "https://www.home-assistant.io/integrations/demo",
"iot_class": "calculated", "iot_class": "calculated",
"quality_scale": "internal" "quality_scale": "internal",
"single_config_entry": true
} }

View File

@ -34,9 +34,6 @@ class DuoTecnoConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle the initial step.""" """Handle the initial step."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
try: try:

View File

@ -7,5 +7,6 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pyduotecno", "pyduotecno-node", "pyduotecno-unit"], "loggers": ["pyduotecno", "pyduotecno-node", "pyduotecno-unit"],
"quality_scale": "silver", "quality_scale": "silver",
"requirements": ["pyDuotecno==2024.10.0"] "requirements": ["pyDuotecno==2024.10.0"],
"single_config_entry": true
} }

View File

@ -13,9 +13,6 @@
} }
} }
}, },
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
},
"error": { "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",

View File

@ -29,10 +29,6 @@ class EcobeeFlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle a flow initiated by the user.""" """Handle a flow initiated by the user."""
if self._async_current_entries():
# Config entry already exists, only one allowed.
return self.async_abort(reason="single_instance_allowed")
errors = {} errors = {}
stored_api_key = ( stored_api_key = (
self.hass.data[DATA_ECOBEE_CONFIG].get(CONF_API_KEY) self.hass.data[DATA_ECOBEE_CONFIG].get(CONF_API_KEY)

View File

@ -10,6 +10,7 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pyecobee"], "loggers": ["pyecobee"],
"requirements": ["python-ecobee-api==0.2.20"], "requirements": ["python-ecobee-api==0.2.20"],
"single_config_entry": true,
"zeroconf": [ "zeroconf": [
{ {
"type": "_ecobee._tcp.local." "type": "_ecobee._tcp.local."

View File

@ -38,9 +38,6 @@ class EnOceanFlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle an EnOcean config flow start.""" """Handle an EnOcean config flow start."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
return await self.async_step_detect() return await self.async_step_detect()
async def async_step_detect( async def async_step_detect(

View File

@ -6,5 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/enocean", "documentation": "https://www.home-assistant.io/integrations/enocean",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["enocean"], "loggers": ["enocean"],
"requirements": ["enocean==0.50"] "requirements": ["enocean==0.50"],
"single_config_entry": true
} }

View File

@ -18,8 +18,7 @@
"invalid_dongle_path": "No valid dongle found for this path" "invalid_dongle_path": "No valid dongle found for this path"
}, },
"abort": { "abort": {
"invalid_dongle_path": "Invalid dongle path", "invalid_dongle_path": "Invalid dongle path"
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
} }
} }
} }

View File

@ -27,11 +27,6 @@ class AqualinkFlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle a flow start.""" """Handle a flow start."""
# Supporting a single account.
entries = self._async_current_entries()
if entries:
return self.async_abort(reason="single_instance_allowed")
errors = {} errors = {}
if user_input is not None: if user_input is not None:

View File

@ -6,5 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/iaqualink", "documentation": "https://www.home-assistant.io/integrations/iaqualink",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["iaqualink"], "loggers": ["iaqualink"],
"requirements": ["iaqualink==0.5.0", "h2==4.1.0"] "requirements": ["iaqualink==0.5.0", "h2==4.1.0"],
"single_config_entry": true
} }

View File

@ -13,9 +13,6 @@
"error": { "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
} }
} }
} }

View File

@ -30,9 +30,6 @@ class IBeaconConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle the initial step.""" """Handle the initial step."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
if not bluetooth.async_scanner_count(self.hass, connectable=False): if not bluetooth.async_scanner_count(self.hass, connectable=False):
return self.async_abort(reason="bluetooth_not_available") return self.async_abort(reason="bluetooth_not_available")

View File

@ -13,5 +13,6 @@
"documentation": "https://www.home-assistant.io/integrations/ibeacon", "documentation": "https://www.home-assistant.io/integrations/ibeacon",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["bleak"], "loggers": ["bleak"],
"requirements": ["ibeacon-ble==1.2.0"] "requirements": ["ibeacon-ble==1.2.0"],
"single_config_entry": true
} }

View File

@ -6,8 +6,7 @@
} }
}, },
"abort": { "abort": {
"bluetooth_not_available": "At least one Bluetooth adapter or remote must be configured to use iBeacon Tracker.", "bluetooth_not_available": "At least one Bluetooth adapter or remote must be configured to use iBeacon Tracker."
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
} }
}, },
"options": { "options": {

View File

@ -59,8 +59,6 @@ class InsteonFlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Init the config flow.""" """Init the config flow."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
modem_types = [STEP_PLM, STEP_HUB_V1, STEP_HUB_V2] modem_types = [STEP_PLM, STEP_HUB_V1, STEP_HUB_V2]
return self.async_show_menu(step_id="user", menu_options=modem_types) return self.async_show_menu(step_id="user", menu_options=modem_types)
@ -135,9 +133,6 @@ class InsteonFlowHandler(ConfigFlow, domain=DOMAIN):
self, discovery_info: usb.UsbServiceInfo self, discovery_info: usb.UsbServiceInfo
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle USB discovery.""" """Handle USB discovery."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
self._device_path = discovery_info.device self._device_path = discovery_info.device
self._device_name = usb.human_readable_device_name( self._device_name = usb.human_readable_device_name(
discovery_info.device, discovery_info.device,

View File

@ -20,6 +20,7 @@
"pyinsteon==1.6.3", "pyinsteon==1.6.3",
"insteon-frontend-home-assistant==0.5.0" "insteon-frontend-home-assistant==0.5.0"
], ],
"single_config_entry": true,
"usb": [ "usb": [
{ {
"vid": "10BF" "vid": "10BF"

View File

@ -44,7 +44,6 @@
}, },
"abort": { "abort": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"not_insteon_device": "Discovered device not an Insteon device" "not_insteon_device": "Discovered device not an Insteon device"
} }
}, },

View File

@ -29,10 +29,6 @@ class ISSConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_user(self, user_input=None) -> ConfigFlowResult: async def async_step_user(self, user_input=None) -> ConfigFlowResult:
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
# Check if already configured
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
if user_input is not None: if user_input is not None:
return self.async_create_entry( return self.async_create_entry(
title=DEFAULT_NAME, title=DEFAULT_NAME,

View File

@ -7,5 +7,6 @@
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pyiss"], "loggers": ["pyiss"],
"requirements": ["pyiss==1.0.1"] "requirements": ["pyiss==1.0.1"],
"single_config_entry": true
} }

View File

@ -6,7 +6,6 @@
} }
}, },
"abort": { "abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"latitude_longitude_not_defined": "Latitude and longitude are not defined in Home Assistant." "latitude_longitude_not_defined": "Latitude and longitude are not defined in Home Assistant."
} }
}, },

View File

@ -958,7 +958,8 @@
"name": "Cloudflare", "name": "Cloudflare",
"integration_type": "hub", "integration_type": "hub",
"config_flow": true, "config_flow": true,
"iot_class": "cloud_push" "iot_class": "cloud_push",
"single_config_entry": true
}, },
"cmus": { "cmus": {
"name": "cmus", "name": "cmus",
@ -1160,7 +1161,8 @@
"demo": { "demo": {
"integration_type": "hub", "integration_type": "hub",
"config_flow": false, "config_flow": false,
"iot_class": "calculated" "iot_class": "calculated",
"single_config_entry": true
}, },
"denon": { "denon": {
"name": "Denon", "name": "Denon",
@ -1403,7 +1405,8 @@
"name": "Duotecno", "name": "Duotecno",
"integration_type": "hub", "integration_type": "hub",
"config_flow": true, "config_flow": true,
"iot_class": "local_push" "iot_class": "local_push",
"single_config_entry": true
}, },
"duquesne_light": { "duquesne_light": {
"name": "Duquesne Light", "name": "Duquesne Light",
@ -1461,7 +1464,8 @@
"name": "ecobee", "name": "ecobee",
"integration_type": "hub", "integration_type": "hub",
"config_flow": true, "config_flow": true,
"iot_class": "cloud_polling" "iot_class": "cloud_polling",
"single_config_entry": true
}, },
"ecoforest": { "ecoforest": {
"name": "Ecoforest", "name": "Ecoforest",
@ -1659,7 +1663,8 @@
"name": "EnOcean", "name": "EnOcean",
"integration_type": "hub", "integration_type": "hub",
"config_flow": true, "config_flow": true,
"iot_class": "local_push" "iot_class": "local_push",
"single_config_entry": true
}, },
"enphase_envoy": { "enphase_envoy": {
"name": "Enphase Envoy", "name": "Enphase Envoy",
@ -2732,7 +2737,8 @@
"name": "Jandy iAqualink", "name": "Jandy iAqualink",
"integration_type": "hub", "integration_type": "hub",
"config_flow": true, "config_flow": true,
"iot_class": "cloud_polling" "iot_class": "cloud_polling",
"single_config_entry": true
}, },
"ibm": { "ibm": {
"name": "IBM", "name": "IBM",
@ -2861,7 +2867,8 @@
"name": "Insteon", "name": "Insteon",
"integration_type": "hub", "integration_type": "hub",
"config_flow": true, "config_flow": true,
"iot_class": "local_push" "iot_class": "local_push",
"single_config_entry": true
}, },
"intellifire": { "intellifire": {
"name": "IntelliFire", "name": "IntelliFire",
@ -2960,7 +2967,8 @@
"name": "International Space Station (ISS)", "name": "International Space Station (ISS)",
"integration_type": "service", "integration_type": "service",
"config_flow": true, "config_flow": true,
"iot_class": "cloud_polling" "iot_class": "cloud_polling",
"single_config_entry": true
}, },
"ista_ecotrend": { "ista_ecotrend": {
"name": "ista EcoTrend", "name": "ista EcoTrend",

View File

@ -11,6 +11,7 @@ from homeassistant.components.ecobee.const import (
DATA_ECOBEE_CONFIG, DATA_ECOBEE_CONFIG,
DOMAIN, DOMAIN,
) )
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_API_KEY from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
@ -20,12 +21,11 @@ from tests.common import MockConfigEntry
async def test_abort_if_already_setup(hass: HomeAssistant) -> None: async def test_abort_if_already_setup(hass: HomeAssistant) -> None:
"""Test we abort if ecobee is already setup.""" """Test we abort if ecobee is already setup."""
flow = config_flow.EcobeeFlowHandler()
flow.hass = hass
MockConfigEntry(domain=DOMAIN).add_to_hass(hass) MockConfigEntry(domain=DOMAIN).add_to_hass(hass)
result = await flow.async_step_user() result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "single_instance_allowed" assert result["reason"] == "single_instance_allowed"

View File

@ -7,7 +7,8 @@ from iaqualink.exception import (
AqualinkServiceUnauthorizedException, AqualinkServiceUnauthorizedException,
) )
from homeassistant.components.iaqualink import config_flow from homeassistant.components.iaqualink import DOMAIN, config_flow
from homeassistant.config_entries import SOURCE_USER
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
@ -18,13 +19,12 @@ async def test_already_configured(
"""Test config flow when iaqualink component is already setup.""" """Test config flow when iaqualink component is already setup."""
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
flow = config_flow.AqualinkFlowHandler() result = await hass.config_entries.flow.async_init(
flow.hass = hass DOMAIN, context={"source": SOURCE_USER}
flow.context = {} )
result = await flow.async_step_user(config_data)
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "single_instance_allowed"
async def test_without_config(hass: HomeAssistant) -> None: async def test_without_config(hass: HomeAssistant) -> None: