diff --git a/src/data/config_entries.ts b/src/data/config_entries.ts index 9a13ecbbca..b8e17db1da 100644 --- a/src/data/config_entries.ts +++ b/src/data/config_entries.ts @@ -1,6 +1,6 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { HomeAssistant } from "../types"; -import { integrationType } from "./integration"; +import { IntegrationType } from "./integration"; export interface ConfigEntry { entry_id: string; @@ -56,7 +56,7 @@ export const subscribeConfigEntries = ( hass: HomeAssistant, callbackFunction: (message: ConfigEntryUpdate[]) => void, filters?: { - type?: Array; + type?: IntegrationType[]; domain?: string; } ): Promise => { @@ -75,7 +75,7 @@ export const subscribeConfigEntries = ( export const getConfigEntries = ( hass: HomeAssistant, filters?: { - type?: Array; + type?: IntegrationType[]; domain?: string; } ): Promise => { diff --git a/src/data/config_flow.ts b/src/data/config_flow.ts index a1b7bfab37..7a5c9207c9 100644 --- a/src/data/config_flow.ts +++ b/src/data/config_flow.ts @@ -3,7 +3,7 @@ import { LocalizeFunc } from "../common/translations/localize"; import { debounce } from "../common/util/debounce"; import { HomeAssistant } from "../types"; import { DataEntryFlowProgress, DataEntryFlowStep } from "./data_entry_flow"; -import { domainToName, integrationType } from "./integration"; +import { domainToName, IntegrationType } from "./integration"; export const DISCOVERY_SOURCES = [ "bluetooth", @@ -68,7 +68,7 @@ export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) => export const getConfigFlowHandlers = ( hass: HomeAssistant, - type?: Array + type?: IntegrationType[] ) => hass.callApi( "GET", diff --git a/src/data/integration.ts b/src/data/integration.ts index d878ef1e17..e644cd64be 100644 --- a/src/data/integration.ts +++ b/src/data/integration.ts @@ -1,7 +1,7 @@ import { LocalizeFunc } from "../common/translations/localize"; import { HomeAssistant } from "../types"; -export type integrationType = "device" | "helper" | "hub" | "service"; +export type IntegrationType = "device" | "helper" | "hub" | "service"; export interface IntegrationManifest { is_built_in: boolean; @@ -17,7 +17,7 @@ export interface IntegrationManifest { ssdp?: Array<{ manufacturer?: string; modelName?: string; st?: string }>; zeroconf?: string[]; homekit?: { models: string[] }; - integration_type?: integrationType; + integration_type?: IntegrationType; quality_scale?: "gold" | "internal" | "platinum" | "silver"; iot_class: | "assumed_state" diff --git a/src/data/integrations.ts b/src/data/integrations.ts index fb09c44fcf..908a7fc6aa 100644 --- a/src/data/integrations.ts +++ b/src/data/integrations.ts @@ -1,29 +1,42 @@ import { HomeAssistant } from "../types"; +import { IntegrationType } from "./integration"; export type IotStandards = "zwave" | "zigbee" | "homekit" | "matter"; export interface Integration { + integration_type: IntegrationType; name?: string; config_flow?: boolean; - integrations?: Integrations; iot_standards?: IotStandards[]; - is_built_in?: boolean; iot_class?: string; + supported_by?: string; + is_built_in?: boolean; } export interface Integrations { [domain: string]: Integration; } +export interface Brand { + name?: string; + integrations?: Integrations; + iot_standards?: IotStandards[]; + is_built_in?: boolean; +} + +export interface Brands { + [domain: string]: Integration | Brand; +} + export interface IntegrationDescriptions { core: { - integration: Integrations; + integration: Brands; hardware: Integrations; helper: Integrations; translated_name: string[]; }; custom: { - integration: Integrations; + integration: Brands; hardware: Integrations; helper: Integrations; }; @@ -35,3 +48,28 @@ export const getIntegrationDescriptions = ( hass.callWS({ type: "integration/descriptions", }); + +export const findIntegration = ( + integrations: Brands | undefined, + domain: string +): Integration | undefined => { + if (!integrations) { + return undefined; + } + if (domain in integrations) { + const integration = integrations[domain]; + if ("integration_type" in integration) { + return integration; + } + } + for (const integration of Object.values(integrations)) { + if ( + "integrations" in integration && + integration.integrations && + domain in integration.integrations + ) { + return integration.integrations[domain]; + } + } + return undefined; +}; diff --git a/src/data/supported_brands.ts b/src/data/supported_brands.ts deleted file mode 100644 index 62691ed275..0000000000 --- a/src/data/supported_brands.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { HomeAssistant } from "../types"; - -export interface SupportedBrandObj { - name: string; - slug: string; - is_add?: boolean; - is_helper?: boolean; - supported_flows: string[]; -} - -export type SupportedBrandHandler = Record; - -export const getSupportedBrands = (hass: HomeAssistant) => - hass.callWS>({ - type: "supported_brands", - }); - -export const getSupportedBrandsLookup = ( - supportedBrands: Record -): Record> => { - const supportedBrandsIntegrations: Record< - string, - Partial - > = {}; - for (const [d, domainBrands] of Object.entries(supportedBrands)) { - for (const [slug, name] of Object.entries(domainBrands)) { - supportedBrandsIntegrations[slug] = { - name, - supported_flows: [d], - }; - } - } - return supportedBrandsIntegrations; -}; diff --git a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts index 5a6239cb3b..a3de1caf34 100644 --- a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts @@ -11,14 +11,8 @@ import { DataEntryFlowStepProgress, } from "../../data/data_entry_flow"; import type { IntegrationManifest } from "../../data/integration"; -import type { SupportedBrandHandler } from "../../data/supported_brands"; import type { HomeAssistant } from "../../types"; -export interface FlowHandlers { - integrations: string[]; - helpers: string[]; - supportedBrands: Record; -} export interface FlowConfig { loadDevicesAndAreas: boolean; diff --git a/src/panels/config/integrations/dialog-add-integration.ts b/src/panels/config/integrations/dialog-add-integration.ts index b9bdd7107e..10a9802934 100644 --- a/src/panels/config/integrations/dialog-add-integration.ts +++ b/src/panels/config/integrations/dialog-add-integration.ts @@ -21,14 +21,13 @@ import { fetchIntegrationManifest, } from "../../../data/integration"; import { + Brand, + Brands, + findIntegration, getIntegrationDescriptions, Integration, Integrations, } from "../../../data/integrations"; -import { - getSupportedBrands, - SupportedBrandHandler, -} from "../../../data/supported_brands"; import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; import { showAlertDialog, @@ -50,7 +49,7 @@ export interface IntegrationListItem { is_helper?: boolean; integrations?: string[]; iot_standards?: string[]; - supported_flows?: string[]; + supported_by?: string; cloud?: boolean; is_built_in?: boolean; is_add?: boolean; @@ -60,12 +59,10 @@ export interface IntegrationListItem { class AddIntegrationDialog extends LitElement { public hass!: HomeAssistant; - @state() private _integrations?: Integrations; + @state() private _integrations?: Brands; @state() private _helpers?: Integrations; - @state() private _supportedBrands?: Record; - @state() private _initialFilter?: string; @state() private _filter?: string; @@ -83,6 +80,7 @@ class AddIntegrationDialog extends LitElement { private _height?: number; public showDialog(params?: AddIntegrationDialogParams): void { + this._load(); this._open = true; this._pickedBrand = params?.brand; this._initialFilter = params?.initialFilter; @@ -95,7 +93,6 @@ class AddIntegrationDialog extends LitElement { this._open = false; this._integrations = undefined; this._helpers = undefined; - this._supportedBrands = undefined; this._pickedBrand = undefined; this._flowsInProgress = undefined; this._filter = undefined; @@ -127,18 +124,10 @@ class AddIntegrationDialog extends LitElement { } } - public updated(changedProps: PropertyValues) { - super.updated(changedProps); - if (changedProps.has("_open") && this._open) { - this._load(); - } - } - private _filterIntegrations = memoizeOne( ( - i: Integrations, + i: Brands, h: Integrations, - sb: Record, components: HomeAssistant["config"]["components"], localize: LocalizeFunc, filter?: string @@ -161,14 +150,35 @@ class AddIntegrationDialog extends LitElement { Object.entries(i).forEach(([domain, integration]) => { if ( - integration.config_flow || - integration.iot_standards || - integration.integrations + "integration_type" in integration && + (integration.config_flow || + integration.iot_standards || + integration.supported_by) ) { + // Integration with a config flow, iot standard, or supported by + const supportedIntegration = integration.supported_by + ? findIntegration(this._integrations, integration.supported_by) + : integration; + if (!supportedIntegration) { + return; + } + integrations.push({ + domain, + name: integration.name || domainToName(localize, domain), + config_flow: supportedIntegration.config_flow, + iot_standards: supportedIntegration.iot_standards, + supported_by: integration.supported_by, + is_built_in: supportedIntegration.is_built_in !== false, + cloud: supportedIntegration.iot_class?.startsWith("cloud_"), + }); + } else if ( + !("integration_type" in integration) && + ("iot_standards" in integration || "integrations" in integration) + ) { + // Brand integrations.push({ domain, name: integration.name || domainToName(localize, domain), - config_flow: integration.config_flow, iot_standards: integration.iot_standards, integrations: integration.integrations ? Object.entries(integration.integrations).map( @@ -176,9 +186,9 @@ class AddIntegrationDialog extends LitElement { ) : undefined, is_built_in: integration.is_built_in !== false, - cloud: integration.iot_class?.startsWith("cloud_"), }); - } else if (filter) { + } else if (filter && "integration_type" in integration) { + // Integration without a config flow yamlIntegrations.push({ domain, name: integration.name || domainToName(localize, domain), @@ -189,29 +199,12 @@ class AddIntegrationDialog extends LitElement { } }); - for (const [domain, domainBrands] of Object.entries(sb)) { - const integration = this._findIntegration(domain); - if (!integration) { - continue; - } - for (const [slug, name] of Object.entries(domainBrands)) { - integrations.push({ - domain: slug, - name, - config_flow: integration.config_flow, - supported_flows: [domain], - is_built_in: true, - cloud: integration.iot_class?.startsWith("cloud_"), - }); - } - } - if (filter) { const options: Fuse.IFuseOptions = { keys: [ "name", "domain", - "supported_flows", + "supported_by", "integrations", "iot_standards", ], @@ -219,21 +212,14 @@ class AddIntegrationDialog extends LitElement { minMatchCharLength: 2, threshold: 0.2, }; - const helpers = Object.entries(h) - .filter( - ([_domain, integration]) => - integration.config_flow || - integration.iot_standards || - integration.integrations - ) - .map(([domain, integration]) => ({ - domain, - name: integration.name || domainToName(localize, domain), - config_flow: integration.config_flow, - is_helper: true, - is_built_in: integration.is_built_in !== false, - cloud: integration.iot_class?.startsWith("cloud_"), - })); + const helpers = Object.entries(h).map(([domain, integration]) => ({ + domain, + name: integration.name || domainToName(localize, domain), + config_flow: integration.config_flow, + is_helper: true, + is_built_in: integration.is_built_in !== false, + cloud: integration.iot_class?.startsWith("cloud_"), + })); return [ ...new Fuse(integrations, options) .search(filter) @@ -255,26 +241,10 @@ class AddIntegrationDialog extends LitElement { } ); - private _findIntegration(domain: string): Integration | undefined { - if (!this._integrations) { - return undefined; - } - if (domain in this._integrations) { - return this._integrations[domain]; - } - for (const integration of Object.values(this._integrations)) { - if (integration.integrations && domain in integration.integrations) { - return integration.integrations[domain]; - } - } - return undefined; - } - private _getIntegrations() { return this._filterIntegrations( this._integrations!, this._helpers!, - this._supportedBrands!, this.hass.config.components, this.hass.localize, this._filter @@ -289,6 +259,11 @@ class AddIntegrationDialog extends LitElement { ? this._getIntegrations() : undefined; + const pickedIntegration = this._pickedBrand + ? this._integrations?.[this._pickedBrand] || + findIntegration(this._integrations, this._pickedBrand) + : undefined; + return html` - ${this._pickedBrand && - (!this._integrations || this._pickedBrand in this._integrations) + ${this._pickedBrand && (!this._integrations || pickedIntegration) ? html`

- ${this._calculateBrandHeading()} + ${this._calculateBrandHeading(pickedIntegration)}

- ${this._renderIntegration()}` + ${this._renderIntegration(pickedIntegration)}` : this._renderAll(integrations)}
`; } - private _calculateBrandHeading() { - const brand = this._integrations?.[this._pickedBrand!]; + private _calculateBrandHeading(integration: Brand | Integration | undefined) { if ( - brand?.iot_standards && - !brand.integrations && + integration?.iot_standards && + !("integrations" in integration) && !this._flowsInProgress?.length ) { return "What type of device is it?"; } if ( - !brand?.iot_standards && - !brand?.integrations && + integration && + !integration?.iot_standards && + !("integrations" in integration) && this._flowsInProgress?.length ) { return "Want to add these discovered devices?"; @@ -334,20 +308,74 @@ class AddIntegrationDialog extends LitElement { return "What do you want to add?"; } - private _renderIntegration(): TemplateResult { + private _renderIntegration( + integration: Brand | Integration | undefined + ): TemplateResult { return html``; } + private _handleSelectBrandEvent(ev: CustomEvent) { + this._pickedBrand = ev.detail.brand; + } + + private _handleSupportedByEvent(ev: CustomEvent) { + this._supportedBy(ev.detail.integration); + } + + private _supportedBy(integration) { + const supportIntegration = findIntegration( + this._integrations, + integration.supported_by + ); + showConfirmationDialog(this, { + text: this.hass.localize( + "ui.panel.config.integrations.config_flow.supported_brand_flow", + { + supported_brand: + integration.name || + domainToName(this.hass.localize, integration.domain), + flow_domain_name: + supportIntegration?.name || + domainToName(this.hass.localize, integration.supported_by), + } + ), + confirm: () => { + this.closeDialog(); + if (["zha", "zwave_js"].includes(integration.supported_by)) { + protocolIntegrationPicked(this, this.hass, integration.supported_by); + return; + } + if (supportIntegration) { + this._handleIntegrationPicked({ + domain: integration.supported_by, + name: + supportIntegration.name || + domainToName(this.hass.localize, integration.supported_by), + config_flow: supportIntegration.config_flow, + iot_standards: supportIntegration.iot_standards, + }); + } else { + showAlertDialog(this, { + text: "Integration not found", + warning: true, + }); + } + }, + }); + } + private _renderAll(integrations?: IntegrationListItem[]): TemplateResult { return html` { - const supportIntegration = this._findIntegration(domain); - this.closeDialog(); - if (["zha", "zwave_js"].includes(domain)) { - protocolIntegrationPicked(this, this.hass, domain); - return; - } - if (supportIntegration) { - this._handleIntegrationPicked({ - domain, - name: - supportIntegration.name || - domainToName(this.hass.localize, domain), - config_flow: supportIntegration.config_flow, - iot_standards: supportIntegration.iot_standards, - integrations: supportIntegration.integrations - ? Object.entries(supportIntegration.integrations).map( - ([dom, val]) => - val.name || domainToName(this.hass.localize, dom) - ) - : undefined, - }); - } else { - showAlertDialog(this, { - text: "Integration not found", - warning: true, - }); - } - }, - }); - + if (integration.supported_by) { + this._supportedBy(integration); return; } @@ -506,9 +490,7 @@ class AddIntegrationDialog extends LitElement { } if (integration.integrations) { - const integrations = - this._integrations![integration.domain].integrations!; - let domains = Object.keys(integrations); + let domains = integration.integrations; if (integration.domain === "apple") { // we show discoverd homekit devices in their own brand section, dont show them at apple domains = domains.filter((domain) => domain !== "homekit_controller"); diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index 18cf0bd7ff..c2c4b5d139 100644 --- a/src/panels/config/integrations/ha-config-integrations.ts +++ b/src/panels/config/integrations/ha-config-integrations.ts @@ -32,7 +32,6 @@ import { subscribeConfigEntries, } from "../../../data/config_entries"; import { - getConfigFlowHandlers, getConfigFlowInProgressCollection, localizeConfigFlowTitle, subscribeConfigFlowInProgress, @@ -49,13 +48,14 @@ import { } from "../../../data/entity_registry"; import { domainToName, + fetchIntegrationManifest, fetchIntegrationManifests, IntegrationManifest, } from "../../../data/integration"; import { - getSupportedBrands, - getSupportedBrandsLookup, -} from "../../../data/supported_brands"; + getIntegrationDescriptions, + findIntegration, +} from "../../../data/integrations"; import { scanUSBDevices } from "../../../data/usb"; import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; import { @@ -693,19 +693,21 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { return; } - const handlers = await getConfigFlowHandlers(this.hass, [ - "device", - "hub", - "service", - ]); + const descriptions = await getIntegrationDescriptions(this.hass); + const integrations = { + ...descriptions.core.integration, + ...descriptions.custom.integration, + }; - // Integration exists, so we can just create a flow - if (handlers.includes(domain)) { + const integration = findIntegration(integrations, domain); + + if (integration?.config_flow) { + // Integration exists, so we can just create a flow const localize = await localizePromise; if ( await showConfirmationDialog(this, { title: localize("ui.panel.config.integrations.confirm_new", { - integration: domainToName(localize, domain), + integration: integration.name || domainToName(localize, domain), }), }) ) { @@ -714,46 +716,57 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { this._handleFlowUpdated(); }, startFlowHandler: domain, - manifest: this._manifests[domain], + manifest: await fetchIntegrationManifest(this.hass, domain), showAdvanced: this.hass.userData?.showAdvanced, }); } return; } - const supportedBrands = await getSupportedBrands(this.hass); - const supportedBrandsIntegrations = - getSupportedBrandsLookup(supportedBrands); + if (integration?.supported_by) { + // Integration is a alias, so we can just create a flow + const localize = await localizePromise; + const supportedIntegration = findIntegration( + integrations, + integration.supported_by + ); - // Supported brand exists, so we can just create a flow - if (Object.keys(supportedBrandsIntegrations).includes(domain)) { - const supBrand = supportedBrandsIntegrations[domain]; - const slug = supBrand.supported_flows![0]; + if (!supportedIntegration) { + return; + } showConfirmationDialog(this, { text: this.hass.localize( "ui.panel.config.integrations.config_flow.supported_brand_flow", { - supported_brand: supBrand.name, - flow_domain_name: domainToName(this.hass.localize, slug), + supported_brand: integration.name || domainToName(localize, domain), + flow_domain_name: + supportedIntegration.name || + domainToName(localize, integration.supported_by), } ), - confirm: () => { - if (["zha", "zwave_js"].includes(slug)) { - protocolIntegrationPicked(this, this.hass, slug); + confirm: async () => { + if (["zha", "zwave_js"].includes(integration.supported_by!)) { + protocolIntegrationPicked( + this, + this.hass, + integration.supported_by! + ); return; } showConfigFlowDialog(this, { dialogClosedCallback: () => { this._handleFlowUpdated(); }, - startFlowHandler: slug, - manifest: this._manifests[slug], + startFlowHandler: integration.supported_by, + manifest: await fetchIntegrationManifest( + this.hass, + integration.supported_by! + ), showAdvanced: this.hass.userData?.showAdvanced, }); }, }); - return; } @@ -764,8 +777,12 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { }); return; } - const helpers = await getConfigFlowHandlers(this.hass, ["helper"]); - if (helpers.includes(domain)) { + const helpers = { + ...descriptions.core.helper, + ...descriptions.custom.helper, + }; + const helper = findIntegration(helpers, domain); + if (helper) { navigate(`/config/helpers/add?domain=${domain}`, { replace: true, }); diff --git a/src/panels/config/integrations/ha-domain-integrations.ts b/src/panels/config/integrations/ha-domain-integrations.ts index 41d458b185..0de8b6c331 100644 --- a/src/panels/config/integrations/ha-domain-integrations.ts +++ b/src/panels/config/integrations/ha-domain-integrations.ts @@ -13,7 +13,7 @@ import { domainToName, fetchIntegrationManifest, } from "../../../data/integration"; -import { Integration } from "../../../data/integrations"; +import { Brand, Integration } from "../../../data/integrations"; import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; @@ -29,7 +29,7 @@ class HaDomainIntegrations extends LitElement { @property() public domain!: string; - @property({ attribute: false }) public integration?: Integration; + @property({ attribute: false }) public integration?: Brand | Integration; @property({ attribute: false }) public flowsInProgress?: DataEntryFlowProgress[]; @@ -65,7 +65,9 @@ class HaDomainIntegrations extends LitElement { ` )}
  • - ${this.integration?.integrations + ${this.integration && + "integrations" in this.integration && + this.integration.integrations ? html`

    ${this.hass.localize( "ui.panel.config.integrations.available_integrations" @@ -106,7 +108,9 @@ class HaDomainIntegrations extends LitElement { `; }) : ""} - ${this.integration?.integrations + ${this.integration && + "integrations" in this.integration && + this.integration.integrations ? Object.entries(this.integration.integrations) .sort((a, b) => { if (a[1].config_flow && !b[1].config_flow) { @@ -163,7 +167,9 @@ class HaDomainIntegrations extends LitElement { ` : ""} - ${this.integration?.config_flow + ${this.integration && + "config_flow" in this.integration && + this.integration.config_flow ? html`${this.flowsInProgress?.length ? html`