diff --git a/src/data/integration.ts b/src/data/integration.ts index 5c331c814a..ce75f49c92 100644 --- a/src/data/integration.ts +++ b/src/data/integration.ts @@ -43,6 +43,7 @@ export interface IntegrationManifest { | "cloud_push" | "local_polling" | "local_push"; + single_config_entry?: boolean; } export interface IntegrationSetup { domain: string; diff --git a/src/data/integrations.ts b/src/data/integrations.ts index 5b26f1a0e3..94235ae38c 100644 --- a/src/data/integrations.ts +++ b/src/data/integrations.ts @@ -11,6 +11,7 @@ export interface Integration { iot_class?: string; supported_by?: string; is_built_in?: boolean; + single_config_entry?: boolean; } export interface Integrations { diff --git a/src/panels/config/integrations/dialog-add-integration.ts b/src/panels/config/integrations/dialog-add-integration.ts index 7fb14f6e50..f03d3be6f3 100644 --- a/src/panels/config/integrations/dialog-add-integration.ts +++ b/src/panels/config/integrations/dialog-add-integration.ts @@ -54,6 +54,7 @@ import { AddIntegrationDialogParams, showYamlIntegrationDialog, } from "./show-add-integration-dialog"; +import { getConfigEntries } from "../../../data/config_entries"; export interface IntegrationListItem { name: string; @@ -67,6 +68,7 @@ export interface IntegrationListItem { cloud?: boolean; is_built_in?: boolean; is_add?: boolean; + single_config_entry?: boolean; } @customElement("dialog-add-integration") @@ -208,6 +210,7 @@ class AddIntegrationDialog extends LitElement { supported_by: integration.supported_by, is_built_in: supportedIntegration.is_built_in !== false, cloud: supportedIntegration.iot_class?.startsWith("cloud_"), + single_config_entry: integration.single_config_entry, }); } else if ( !("integration_type" in integration) && @@ -572,6 +575,27 @@ class AddIntegrationDialog extends LitElement { return; } + if (integration.single_config_entry) { + const configEntries = await getConfigEntries(this.hass, { + domain: integration.domain, + }); + if (configEntries.length > 0) { + this.closeDialog(); + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.integrations.config_flow.single_config_entry_title" + ), + text: this.hass.localize( + "ui.panel.config.integrations.config_flow.single_config_entry", + { + integration_name: integration.name, + } + ), + }); + return; + } + } + if (integration.config_flow) { this._createFlow(integration.domain); return; diff --git a/src/panels/config/integrations/ha-config-integration-page.ts b/src/panels/config/integrations/ha-config-integration-page.ts index b53f46e855..5f0b01f2a0 100644 --- a/src/panels/config/integrations/ha-config-integration-page.ts +++ b/src/panels/config/integrations/ha-config-integration-page.ts @@ -1318,6 +1318,26 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) { } private async _addIntegration() { + if (this._manifest?.single_config_entry) { + const entries = this._domainConfigEntries( + this.domain, + this._extraConfigEntries || this.configEntries + ); + if (entries.length > 0) { + await showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.integrations.config_flow.single_config_entry_title" + ), + text: this.hass.localize( + "ui.panel.config.integrations.config_flow.single_config_entry", + { + integration_name: this._manifest.name, + } + ), + }); + return; + } + } showAddIntegrationDialog(this, { domain: this.domain, }); diff --git a/src/panels/config/integrations/ha-config-integrations-dashboard.ts b/src/panels/config/integrations/ha-config-integrations-dashboard.ts index 7a960576e8..55c5d1214e 100644 --- a/src/panels/config/integrations/ha-config-integrations-dashboard.ts +++ b/src/panels/config/integrations/ha-config-integrations-dashboard.ts @@ -29,7 +29,7 @@ import "../../../components/ha-fab"; import "../../../components/ha-icon-button"; import "../../../components/ha-svg-icon"; import "../../../components/search-input"; -import { ConfigEntry } from "../../../data/config_entries"; +import { ConfigEntry, getConfigEntries } from "../../../data/config_entries"; import { getConfigFlowInProgressCollection } from "../../../data/config_flow"; import { fetchDiagnosticHandlers } from "../../../data/diagnostics"; import { @@ -658,6 +658,24 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) { const integration = findIntegration(integrations, domain); if (integration?.config_flow) { + if (integration.single_config_entry) { + const configEntries = await getConfigEntries(this.hass, { domain }); + if (configEntries.length > 0) { + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.integrations.config_flow.single_config_entry_title" + ), + text: this.hass.localize( + "ui.panel.config.integrations.config_flow.single_config_entry", + { + integration_name: integration.name, + } + ), + }); + return; + } + } + // Integration exists, so we can just create a flow const localize = await this.hass.loadBackendTranslation( "title", diff --git a/src/translations/en.json b/src/translations/en.json index ecd61cd655..49c060157b 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4043,7 +4043,9 @@ "missing_matter": "To add a {brand} device, you first need the {integration} integration and {supported_hardware_link}. Do you want to proceed with the setup of {integration}?", "matter_mobile_app": "You need to use the Home Assistant Companion app on your mobile phone to commission Matter devices.", "supported_hardware": "supported hardware", - "proceed": "Proceed" + "proceed": "Proceed", + "single_config_entry_title": "This integration allows only one configuration", + "single_config_entry": "{integration_name} supports only one configuration. Adding additional ones is not needed." } }, "users": {