mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-15 13:26:34 +00:00
Add support for virtual integrations (#14138)
This commit is contained in:
parent
1b4989a7dc
commit
112ec10b30
@ -1,6 +1,6 @@
|
|||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import { integrationType } from "./integration";
|
import { IntegrationType } from "./integration";
|
||||||
|
|
||||||
export interface ConfigEntry {
|
export interface ConfigEntry {
|
||||||
entry_id: string;
|
entry_id: string;
|
||||||
@ -56,7 +56,7 @@ export const subscribeConfigEntries = (
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
callbackFunction: (message: ConfigEntryUpdate[]) => void,
|
callbackFunction: (message: ConfigEntryUpdate[]) => void,
|
||||||
filters?: {
|
filters?: {
|
||||||
type?: Array<integrationType>;
|
type?: IntegrationType[];
|
||||||
domain?: string;
|
domain?: string;
|
||||||
}
|
}
|
||||||
): Promise<UnsubscribeFunc> => {
|
): Promise<UnsubscribeFunc> => {
|
||||||
@ -75,7 +75,7 @@ export const subscribeConfigEntries = (
|
|||||||
export const getConfigEntries = (
|
export const getConfigEntries = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
filters?: {
|
filters?: {
|
||||||
type?: Array<integrationType>;
|
type?: IntegrationType[];
|
||||||
domain?: string;
|
domain?: string;
|
||||||
}
|
}
|
||||||
): Promise<ConfigEntry[]> => {
|
): Promise<ConfigEntry[]> => {
|
||||||
|
@ -3,7 +3,7 @@ import { LocalizeFunc } from "../common/translations/localize";
|
|||||||
import { debounce } from "../common/util/debounce";
|
import { debounce } from "../common/util/debounce";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import { DataEntryFlowProgress, DataEntryFlowStep } from "./data_entry_flow";
|
import { DataEntryFlowProgress, DataEntryFlowStep } from "./data_entry_flow";
|
||||||
import { domainToName, integrationType } from "./integration";
|
import { domainToName, IntegrationType } from "./integration";
|
||||||
|
|
||||||
export const DISCOVERY_SOURCES = [
|
export const DISCOVERY_SOURCES = [
|
||||||
"bluetooth",
|
"bluetooth",
|
||||||
@ -68,7 +68,7 @@ export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) =>
|
|||||||
|
|
||||||
export const getConfigFlowHandlers = (
|
export const getConfigFlowHandlers = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
type?: Array<integrationType>
|
type?: IntegrationType[]
|
||||||
) =>
|
) =>
|
||||||
hass.callApi<string[]>(
|
hass.callApi<string[]>(
|
||||||
"GET",
|
"GET",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { LocalizeFunc } from "../common/translations/localize";
|
import { LocalizeFunc } from "../common/translations/localize";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
export type integrationType = "device" | "helper" | "hub" | "service";
|
export type IntegrationType = "device" | "helper" | "hub" | "service";
|
||||||
|
|
||||||
export interface IntegrationManifest {
|
export interface IntegrationManifest {
|
||||||
is_built_in: boolean;
|
is_built_in: boolean;
|
||||||
@ -17,7 +17,7 @@ export interface IntegrationManifest {
|
|||||||
ssdp?: Array<{ manufacturer?: string; modelName?: string; st?: string }>;
|
ssdp?: Array<{ manufacturer?: string; modelName?: string; st?: string }>;
|
||||||
zeroconf?: string[];
|
zeroconf?: string[];
|
||||||
homekit?: { models: string[] };
|
homekit?: { models: string[] };
|
||||||
integration_type?: integrationType;
|
integration_type?: IntegrationType;
|
||||||
quality_scale?: "gold" | "internal" | "platinum" | "silver";
|
quality_scale?: "gold" | "internal" | "platinum" | "silver";
|
||||||
iot_class:
|
iot_class:
|
||||||
| "assumed_state"
|
| "assumed_state"
|
||||||
|
@ -1,29 +1,42 @@
|
|||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
|
import { IntegrationType } from "./integration";
|
||||||
|
|
||||||
export type IotStandards = "zwave" | "zigbee" | "homekit" | "matter";
|
export type IotStandards = "zwave" | "zigbee" | "homekit" | "matter";
|
||||||
|
|
||||||
export interface Integration {
|
export interface Integration {
|
||||||
|
integration_type: IntegrationType;
|
||||||
name?: string;
|
name?: string;
|
||||||
config_flow?: boolean;
|
config_flow?: boolean;
|
||||||
integrations?: Integrations;
|
|
||||||
iot_standards?: IotStandards[];
|
iot_standards?: IotStandards[];
|
||||||
is_built_in?: boolean;
|
|
||||||
iot_class?: string;
|
iot_class?: string;
|
||||||
|
supported_by?: string;
|
||||||
|
is_built_in?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Integrations {
|
export interface Integrations {
|
||||||
[domain: string]: Integration;
|
[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 {
|
export interface IntegrationDescriptions {
|
||||||
core: {
|
core: {
|
||||||
integration: Integrations;
|
integration: Brands;
|
||||||
hardware: Integrations;
|
hardware: Integrations;
|
||||||
helper: Integrations;
|
helper: Integrations;
|
||||||
translated_name: string[];
|
translated_name: string[];
|
||||||
};
|
};
|
||||||
custom: {
|
custom: {
|
||||||
integration: Integrations;
|
integration: Brands;
|
||||||
hardware: Integrations;
|
hardware: Integrations;
|
||||||
helper: Integrations;
|
helper: Integrations;
|
||||||
};
|
};
|
||||||
@ -35,3 +48,28 @@ export const getIntegrationDescriptions = (
|
|||||||
hass.callWS<IntegrationDescriptions>({
|
hass.callWS<IntegrationDescriptions>({
|
||||||
type: "integration/descriptions",
|
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;
|
||||||
|
};
|
||||||
|
@ -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<string, string>;
|
|
||||||
|
|
||||||
export const getSupportedBrands = (hass: HomeAssistant) =>
|
|
||||||
hass.callWS<Record<string, SupportedBrandHandler>>({
|
|
||||||
type: "supported_brands",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getSupportedBrandsLookup = (
|
|
||||||
supportedBrands: Record<string, SupportedBrandHandler>
|
|
||||||
): Record<string, Partial<SupportedBrandObj>> => {
|
|
||||||
const supportedBrandsIntegrations: Record<
|
|
||||||
string,
|
|
||||||
Partial<SupportedBrandObj>
|
|
||||||
> = {};
|
|
||||||
for (const [d, domainBrands] of Object.entries(supportedBrands)) {
|
|
||||||
for (const [slug, name] of Object.entries(domainBrands)) {
|
|
||||||
supportedBrandsIntegrations[slug] = {
|
|
||||||
name,
|
|
||||||
supported_flows: [d],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return supportedBrandsIntegrations;
|
|
||||||
};
|
|
@ -11,14 +11,8 @@ import {
|
|||||||
DataEntryFlowStepProgress,
|
DataEntryFlowStepProgress,
|
||||||
} from "../../data/data_entry_flow";
|
} from "../../data/data_entry_flow";
|
||||||
import type { IntegrationManifest } from "../../data/integration";
|
import type { IntegrationManifest } from "../../data/integration";
|
||||||
import type { SupportedBrandHandler } from "../../data/supported_brands";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
|
|
||||||
export interface FlowHandlers {
|
|
||||||
integrations: string[];
|
|
||||||
helpers: string[];
|
|
||||||
supportedBrands: Record<string, SupportedBrandHandler>;
|
|
||||||
}
|
|
||||||
export interface FlowConfig {
|
export interface FlowConfig {
|
||||||
loadDevicesAndAreas: boolean;
|
loadDevicesAndAreas: boolean;
|
||||||
|
|
||||||
|
@ -21,14 +21,13 @@ import {
|
|||||||
fetchIntegrationManifest,
|
fetchIntegrationManifest,
|
||||||
} from "../../../data/integration";
|
} from "../../../data/integration";
|
||||||
import {
|
import {
|
||||||
|
Brand,
|
||||||
|
Brands,
|
||||||
|
findIntegration,
|
||||||
getIntegrationDescriptions,
|
getIntegrationDescriptions,
|
||||||
Integration,
|
Integration,
|
||||||
Integrations,
|
Integrations,
|
||||||
} from "../../../data/integrations";
|
} from "../../../data/integrations";
|
||||||
import {
|
|
||||||
getSupportedBrands,
|
|
||||||
SupportedBrandHandler,
|
|
||||||
} from "../../../data/supported_brands";
|
|
||||||
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||||
import {
|
import {
|
||||||
showAlertDialog,
|
showAlertDialog,
|
||||||
@ -50,7 +49,7 @@ export interface IntegrationListItem {
|
|||||||
is_helper?: boolean;
|
is_helper?: boolean;
|
||||||
integrations?: string[];
|
integrations?: string[];
|
||||||
iot_standards?: string[];
|
iot_standards?: string[];
|
||||||
supported_flows?: string[];
|
supported_by?: string;
|
||||||
cloud?: boolean;
|
cloud?: boolean;
|
||||||
is_built_in?: boolean;
|
is_built_in?: boolean;
|
||||||
is_add?: boolean;
|
is_add?: boolean;
|
||||||
@ -60,12 +59,10 @@ export interface IntegrationListItem {
|
|||||||
class AddIntegrationDialog extends LitElement {
|
class AddIntegrationDialog extends LitElement {
|
||||||
public hass!: HomeAssistant;
|
public hass!: HomeAssistant;
|
||||||
|
|
||||||
@state() private _integrations?: Integrations;
|
@state() private _integrations?: Brands;
|
||||||
|
|
||||||
@state() private _helpers?: Integrations;
|
@state() private _helpers?: Integrations;
|
||||||
|
|
||||||
@state() private _supportedBrands?: Record<string, SupportedBrandHandler>;
|
|
||||||
|
|
||||||
@state() private _initialFilter?: string;
|
@state() private _initialFilter?: string;
|
||||||
|
|
||||||
@state() private _filter?: string;
|
@state() private _filter?: string;
|
||||||
@ -83,6 +80,7 @@ class AddIntegrationDialog extends LitElement {
|
|||||||
private _height?: number;
|
private _height?: number;
|
||||||
|
|
||||||
public showDialog(params?: AddIntegrationDialogParams): void {
|
public showDialog(params?: AddIntegrationDialogParams): void {
|
||||||
|
this._load();
|
||||||
this._open = true;
|
this._open = true;
|
||||||
this._pickedBrand = params?.brand;
|
this._pickedBrand = params?.brand;
|
||||||
this._initialFilter = params?.initialFilter;
|
this._initialFilter = params?.initialFilter;
|
||||||
@ -95,7 +93,6 @@ class AddIntegrationDialog extends LitElement {
|
|||||||
this._open = false;
|
this._open = false;
|
||||||
this._integrations = undefined;
|
this._integrations = undefined;
|
||||||
this._helpers = undefined;
|
this._helpers = undefined;
|
||||||
this._supportedBrands = undefined;
|
|
||||||
this._pickedBrand = undefined;
|
this._pickedBrand = undefined;
|
||||||
this._flowsInProgress = undefined;
|
this._flowsInProgress = undefined;
|
||||||
this._filter = 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(
|
private _filterIntegrations = memoizeOne(
|
||||||
(
|
(
|
||||||
i: Integrations,
|
i: Brands,
|
||||||
h: Integrations,
|
h: Integrations,
|
||||||
sb: Record<string, SupportedBrandHandler>,
|
|
||||||
components: HomeAssistant["config"]["components"],
|
components: HomeAssistant["config"]["components"],
|
||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
filter?: string
|
filter?: string
|
||||||
@ -161,14 +150,35 @@ class AddIntegrationDialog extends LitElement {
|
|||||||
|
|
||||||
Object.entries(i).forEach(([domain, integration]) => {
|
Object.entries(i).forEach(([domain, integration]) => {
|
||||||
if (
|
if (
|
||||||
integration.config_flow ||
|
"integration_type" in integration &&
|
||||||
integration.iot_standards ||
|
(integration.config_flow ||
|
||||||
integration.integrations
|
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({
|
integrations.push({
|
||||||
domain,
|
domain,
|
||||||
name: integration.name || domainToName(localize, domain),
|
name: integration.name || domainToName(localize, domain),
|
||||||
config_flow: integration.config_flow,
|
|
||||||
iot_standards: integration.iot_standards,
|
iot_standards: integration.iot_standards,
|
||||||
integrations: integration.integrations
|
integrations: integration.integrations
|
||||||
? Object.entries(integration.integrations).map(
|
? Object.entries(integration.integrations).map(
|
||||||
@ -176,9 +186,9 @@ class AddIntegrationDialog extends LitElement {
|
|||||||
)
|
)
|
||||||
: undefined,
|
: undefined,
|
||||||
is_built_in: integration.is_built_in !== false,
|
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({
|
yamlIntegrations.push({
|
||||||
domain,
|
domain,
|
||||||
name: integration.name || domainToName(localize, 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) {
|
if (filter) {
|
||||||
const options: Fuse.IFuseOptions<IntegrationListItem> = {
|
const options: Fuse.IFuseOptions<IntegrationListItem> = {
|
||||||
keys: [
|
keys: [
|
||||||
"name",
|
"name",
|
||||||
"domain",
|
"domain",
|
||||||
"supported_flows",
|
"supported_by",
|
||||||
"integrations",
|
"integrations",
|
||||||
"iot_standards",
|
"iot_standards",
|
||||||
],
|
],
|
||||||
@ -219,21 +212,14 @@ class AddIntegrationDialog extends LitElement {
|
|||||||
minMatchCharLength: 2,
|
minMatchCharLength: 2,
|
||||||
threshold: 0.2,
|
threshold: 0.2,
|
||||||
};
|
};
|
||||||
const helpers = Object.entries(h)
|
const helpers = Object.entries(h).map(([domain, integration]) => ({
|
||||||
.filter(
|
domain,
|
||||||
([_domain, integration]) =>
|
name: integration.name || domainToName(localize, domain),
|
||||||
integration.config_flow ||
|
config_flow: integration.config_flow,
|
||||||
integration.iot_standards ||
|
is_helper: true,
|
||||||
integration.integrations
|
is_built_in: integration.is_built_in !== false,
|
||||||
)
|
cloud: integration.iot_class?.startsWith("cloud_"),
|
||||||
.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 [
|
return [
|
||||||
...new Fuse(integrations, options)
|
...new Fuse(integrations, options)
|
||||||
.search(filter)
|
.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() {
|
private _getIntegrations() {
|
||||||
return this._filterIntegrations(
|
return this._filterIntegrations(
|
||||||
this._integrations!,
|
this._integrations!,
|
||||||
this._helpers!,
|
this._helpers!,
|
||||||
this._supportedBrands!,
|
|
||||||
this.hass.config.components,
|
this.hass.config.components,
|
||||||
this.hass.localize,
|
this.hass.localize,
|
||||||
this._filter
|
this._filter
|
||||||
@ -289,6 +259,11 @@ class AddIntegrationDialog extends LitElement {
|
|||||||
? this._getIntegrations()
|
? this._getIntegrations()
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const pickedIntegration = this._pickedBrand
|
||||||
|
? this._integrations?.[this._pickedBrand] ||
|
||||||
|
findIntegration(this._integrations, this._pickedBrand)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return html`<ha-dialog
|
return html`<ha-dialog
|
||||||
open
|
open
|
||||||
@closed=${this.closeDialog}
|
@closed=${this.closeDialog}
|
||||||
@ -300,33 +275,32 @@ class AddIntegrationDialog extends LitElement {
|
|||||||
this.hass.localize("ui.panel.config.integrations.new")
|
this.hass.localize("ui.panel.config.integrations.new")
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
${this._pickedBrand &&
|
${this._pickedBrand && (!this._integrations || pickedIntegration)
|
||||||
(!this._integrations || this._pickedBrand in this._integrations)
|
|
||||||
? html`<div slot="heading">
|
? html`<div slot="heading">
|
||||||
<ha-icon-button-prev
|
<ha-icon-button-prev
|
||||||
@click=${this._prevClicked}
|
@click=${this._prevClicked}
|
||||||
></ha-icon-button-prev>
|
></ha-icon-button-prev>
|
||||||
<h2 class="mdc-dialog__title">
|
<h2 class="mdc-dialog__title">
|
||||||
${this._calculateBrandHeading()}
|
${this._calculateBrandHeading(pickedIntegration)}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
${this._renderIntegration()}`
|
${this._renderIntegration(pickedIntegration)}`
|
||||||
: this._renderAll(integrations)}
|
: this._renderAll(integrations)}
|
||||||
</ha-dialog>`;
|
</ha-dialog>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _calculateBrandHeading() {
|
private _calculateBrandHeading(integration: Brand | Integration | undefined) {
|
||||||
const brand = this._integrations?.[this._pickedBrand!];
|
|
||||||
if (
|
if (
|
||||||
brand?.iot_standards &&
|
integration?.iot_standards &&
|
||||||
!brand.integrations &&
|
!("integrations" in integration) &&
|
||||||
!this._flowsInProgress?.length
|
!this._flowsInProgress?.length
|
||||||
) {
|
) {
|
||||||
return "What type of device is it?";
|
return "What type of device is it?";
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!brand?.iot_standards &&
|
integration &&
|
||||||
!brand?.integrations &&
|
!integration?.iot_standards &&
|
||||||
|
!("integrations" in integration) &&
|
||||||
this._flowsInProgress?.length
|
this._flowsInProgress?.length
|
||||||
) {
|
) {
|
||||||
return "Want to add these discovered devices?";
|
return "Want to add these discovered devices?";
|
||||||
@ -334,20 +308,74 @@ class AddIntegrationDialog extends LitElement {
|
|||||||
return "What do you want to add?";
|
return "What do you want to add?";
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderIntegration(): TemplateResult {
|
private _renderIntegration(
|
||||||
|
integration: Brand | Integration | undefined
|
||||||
|
): TemplateResult {
|
||||||
return html`<ha-domain-integrations
|
return html`<ha-domain-integrations
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.domain=${this._pickedBrand}
|
.domain=${this._pickedBrand}
|
||||||
.integration=${this._integrations?.[this._pickedBrand!]}
|
.integration=${integration}
|
||||||
.flowsInProgress=${this._flowsInProgress}
|
.flowsInProgress=${this._flowsInProgress}
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
minWidth: `${this._width}px`,
|
minWidth: `${this._width}px`,
|
||||||
minHeight: `581px`,
|
minHeight: `581px`,
|
||||||
})}
|
})}
|
||||||
@close-dialog=${this.closeDialog}
|
@close-dialog=${this.closeDialog}
|
||||||
|
@supported-by=${this._handleSupportedByEvent}
|
||||||
|
@select-brand=${this._handleSelectBrandEvent}
|
||||||
></ha-domain-integrations>`;
|
></ha-domain-integrations>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
private _renderAll(integrations?: IntegrationListItem[]): TemplateResult {
|
||||||
return html`<search-input
|
return html`<search-input
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@ -393,10 +421,7 @@ class AddIntegrationDialog extends LitElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private async _load() {
|
private async _load() {
|
||||||
const [descriptions, supportedBrands] = await Promise.all([
|
const descriptions = await getIntegrationDescriptions(this.hass);
|
||||||
getIntegrationDescriptions(this.hass),
|
|
||||||
getSupportedBrands(this.hass),
|
|
||||||
]);
|
|
||||||
for (const integration in descriptions.custom.integration) {
|
for (const integration in descriptions.custom.integration) {
|
||||||
if (
|
if (
|
||||||
!Object.prototype.hasOwnProperty.call(
|
!Object.prototype.hasOwnProperty.call(
|
||||||
@ -427,7 +452,6 @@ class AddIntegrationDialog extends LitElement {
|
|||||||
...descriptions.core.helper,
|
...descriptions.core.helper,
|
||||||
...descriptions.custom.helper,
|
...descriptions.custom.helper,
|
||||||
};
|
};
|
||||||
this._supportedBrands = supportedBrands;
|
|
||||||
this.hass.loadBackendTranslation(
|
this.hass.loadBackendTranslation(
|
||||||
"title",
|
"title",
|
||||||
descriptions.core.translated_name,
|
descriptions.core.translated_name,
|
||||||
@ -448,48 +472,8 @@ class AddIntegrationDialog extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _handleIntegrationPicked(integration: IntegrationListItem) {
|
private async _handleIntegrationPicked(integration: IntegrationListItem) {
|
||||||
if ("supported_flows" in integration) {
|
if (integration.supported_by) {
|
||||||
const domain = integration.supported_flows![0];
|
this._supportedBy(integration);
|
||||||
|
|
||||||
showConfirmationDialog(this, {
|
|
||||||
text: this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.config_flow.supported_brand_flow",
|
|
||||||
{
|
|
||||||
supported_brand: integration.name,
|
|
||||||
flow_domain_name: domainToName(this.hass.localize, domain),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
confirm: () => {
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -506,9 +490,7 @@ class AddIntegrationDialog extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (integration.integrations) {
|
if (integration.integrations) {
|
||||||
const integrations =
|
let domains = integration.integrations;
|
||||||
this._integrations![integration.domain].integrations!;
|
|
||||||
let domains = Object.keys(integrations);
|
|
||||||
if (integration.domain === "apple") {
|
if (integration.domain === "apple") {
|
||||||
// we show discoverd homekit devices in their own brand section, dont show them at apple
|
// we show discoverd homekit devices in their own brand section, dont show them at apple
|
||||||
domains = domains.filter((domain) => domain !== "homekit_controller");
|
domains = domains.filter((domain) => domain !== "homekit_controller");
|
||||||
|
@ -32,7 +32,6 @@ import {
|
|||||||
subscribeConfigEntries,
|
subscribeConfigEntries,
|
||||||
} from "../../../data/config_entries";
|
} from "../../../data/config_entries";
|
||||||
import {
|
import {
|
||||||
getConfigFlowHandlers,
|
|
||||||
getConfigFlowInProgressCollection,
|
getConfigFlowInProgressCollection,
|
||||||
localizeConfigFlowTitle,
|
localizeConfigFlowTitle,
|
||||||
subscribeConfigFlowInProgress,
|
subscribeConfigFlowInProgress,
|
||||||
@ -49,13 +48,14 @@ import {
|
|||||||
} from "../../../data/entity_registry";
|
} from "../../../data/entity_registry";
|
||||||
import {
|
import {
|
||||||
domainToName,
|
domainToName,
|
||||||
|
fetchIntegrationManifest,
|
||||||
fetchIntegrationManifests,
|
fetchIntegrationManifests,
|
||||||
IntegrationManifest,
|
IntegrationManifest,
|
||||||
} from "../../../data/integration";
|
} from "../../../data/integration";
|
||||||
import {
|
import {
|
||||||
getSupportedBrands,
|
getIntegrationDescriptions,
|
||||||
getSupportedBrandsLookup,
|
findIntegration,
|
||||||
} from "../../../data/supported_brands";
|
} from "../../../data/integrations";
|
||||||
import { scanUSBDevices } from "../../../data/usb";
|
import { scanUSBDevices } from "../../../data/usb";
|
||||||
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||||
import {
|
import {
|
||||||
@ -693,19 +693,21 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlers = await getConfigFlowHandlers(this.hass, [
|
const descriptions = await getIntegrationDescriptions(this.hass);
|
||||||
"device",
|
const integrations = {
|
||||||
"hub",
|
...descriptions.core.integration,
|
||||||
"service",
|
...descriptions.custom.integration,
|
||||||
]);
|
};
|
||||||
|
|
||||||
// Integration exists, so we can just create a flow
|
const integration = findIntegration(integrations, domain);
|
||||||
if (handlers.includes(domain)) {
|
|
||||||
|
if (integration?.config_flow) {
|
||||||
|
// Integration exists, so we can just create a flow
|
||||||
const localize = await localizePromise;
|
const localize = await localizePromise;
|
||||||
if (
|
if (
|
||||||
await showConfirmationDialog(this, {
|
await showConfirmationDialog(this, {
|
||||||
title: localize("ui.panel.config.integrations.confirm_new", {
|
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();
|
this._handleFlowUpdated();
|
||||||
},
|
},
|
||||||
startFlowHandler: domain,
|
startFlowHandler: domain,
|
||||||
manifest: this._manifests[domain],
|
manifest: await fetchIntegrationManifest(this.hass, domain),
|
||||||
showAdvanced: this.hass.userData?.showAdvanced,
|
showAdvanced: this.hass.userData?.showAdvanced,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const supportedBrands = await getSupportedBrands(this.hass);
|
if (integration?.supported_by) {
|
||||||
const supportedBrandsIntegrations =
|
// Integration is a alias, so we can just create a flow
|
||||||
getSupportedBrandsLookup(supportedBrands);
|
const localize = await localizePromise;
|
||||||
|
const supportedIntegration = findIntegration(
|
||||||
|
integrations,
|
||||||
|
integration.supported_by
|
||||||
|
);
|
||||||
|
|
||||||
// Supported brand exists, so we can just create a flow
|
if (!supportedIntegration) {
|
||||||
if (Object.keys(supportedBrandsIntegrations).includes(domain)) {
|
return;
|
||||||
const supBrand = supportedBrandsIntegrations[domain];
|
}
|
||||||
const slug = supBrand.supported_flows![0];
|
|
||||||
|
|
||||||
showConfirmationDialog(this, {
|
showConfirmationDialog(this, {
|
||||||
text: this.hass.localize(
|
text: this.hass.localize(
|
||||||
"ui.panel.config.integrations.config_flow.supported_brand_flow",
|
"ui.panel.config.integrations.config_flow.supported_brand_flow",
|
||||||
{
|
{
|
||||||
supported_brand: supBrand.name,
|
supported_brand: integration.name || domainToName(localize, domain),
|
||||||
flow_domain_name: domainToName(this.hass.localize, slug),
|
flow_domain_name:
|
||||||
|
supportedIntegration.name ||
|
||||||
|
domainToName(localize, integration.supported_by),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
confirm: () => {
|
confirm: async () => {
|
||||||
if (["zha", "zwave_js"].includes(slug)) {
|
if (["zha", "zwave_js"].includes(integration.supported_by!)) {
|
||||||
protocolIntegrationPicked(this, this.hass, slug);
|
protocolIntegrationPicked(
|
||||||
|
this,
|
||||||
|
this.hass,
|
||||||
|
integration.supported_by!
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showConfigFlowDialog(this, {
|
showConfigFlowDialog(this, {
|
||||||
dialogClosedCallback: () => {
|
dialogClosedCallback: () => {
|
||||||
this._handleFlowUpdated();
|
this._handleFlowUpdated();
|
||||||
},
|
},
|
||||||
startFlowHandler: slug,
|
startFlowHandler: integration.supported_by,
|
||||||
manifest: this._manifests[slug],
|
manifest: await fetchIntegrationManifest(
|
||||||
|
this.hass,
|
||||||
|
integration.supported_by!
|
||||||
|
),
|
||||||
showAdvanced: this.hass.userData?.showAdvanced,
|
showAdvanced: this.hass.userData?.showAdvanced,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -764,8 +777,12 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const helpers = await getConfigFlowHandlers(this.hass, ["helper"]);
|
const helpers = {
|
||||||
if (helpers.includes(domain)) {
|
...descriptions.core.helper,
|
||||||
|
...descriptions.custom.helper,
|
||||||
|
};
|
||||||
|
const helper = findIntegration(helpers, domain);
|
||||||
|
if (helper) {
|
||||||
navigate(`/config/helpers/add?domain=${domain}`, {
|
navigate(`/config/helpers/add?domain=${domain}`, {
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
domainToName,
|
domainToName,
|
||||||
fetchIntegrationManifest,
|
fetchIntegrationManifest,
|
||||||
} from "../../../data/integration";
|
} 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 { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||||
import { haStyle } from "../../../resources/styles";
|
import { haStyle } from "../../../resources/styles";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
@ -29,7 +29,7 @@ class HaDomainIntegrations extends LitElement {
|
|||||||
|
|
||||||
@property() public domain!: string;
|
@property() public domain!: string;
|
||||||
|
|
||||||
@property({ attribute: false }) public integration?: Integration;
|
@property({ attribute: false }) public integration?: Brand | Integration;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public flowsInProgress?: DataEntryFlowProgress[];
|
public flowsInProgress?: DataEntryFlowProgress[];
|
||||||
@ -65,7 +65,9 @@ class HaDomainIntegrations extends LitElement {
|
|||||||
</mwc-list-item>`
|
</mwc-list-item>`
|
||||||
)}
|
)}
|
||||||
<li divider role="separator"></li>
|
<li divider role="separator"></li>
|
||||||
${this.integration?.integrations
|
${this.integration &&
|
||||||
|
"integrations" in this.integration &&
|
||||||
|
this.integration.integrations
|
||||||
? html`<h3>
|
? html`<h3>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.integrations.available_integrations"
|
"ui.panel.config.integrations.available_integrations"
|
||||||
@ -106,7 +108,9 @@ class HaDomainIntegrations extends LitElement {
|
|||||||
</mwc-list-item>`;
|
</mwc-list-item>`;
|
||||||
})
|
})
|
||||||
: ""}
|
: ""}
|
||||||
${this.integration?.integrations
|
${this.integration &&
|
||||||
|
"integrations" in this.integration &&
|
||||||
|
this.integration.integrations
|
||||||
? Object.entries(this.integration.integrations)
|
? Object.entries(this.integration.integrations)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (a[1].config_flow && !b[1].config_flow) {
|
if (a[1].config_flow && !b[1].config_flow) {
|
||||||
@ -163,7 +167,9 @@ class HaDomainIntegrations extends LitElement {
|
|||||||
<ha-icon-next slot="meta"></ha-icon-next>
|
<ha-icon-next slot="meta"></ha-icon-next>
|
||||||
</mwc-list-item>`
|
</mwc-list-item>`
|
||||||
: ""}
|
: ""}
|
||||||
${this.integration?.config_flow
|
${this.integration &&
|
||||||
|
"config_flow" in this.integration &&
|
||||||
|
this.integration.config_flow
|
||||||
? html`${this.flowsInProgress?.length
|
? html`${this.flowsInProgress?.length
|
||||||
? html`<mwc-list-item
|
? html`<mwc-list-item
|
||||||
.domain=${this.domain}
|
.domain=${this.domain}
|
||||||
@ -211,12 +217,30 @@ class HaDomainIntegrations extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const integration = (ev.currentTarget as any).integration;
|
||||||
|
|
||||||
|
if (integration.supported_by) {
|
||||||
|
// @ts-ignore
|
||||||
|
fireEvent(this, "supported-by", { integration });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (integration.iot_standards) {
|
||||||
|
// @ts-ignore
|
||||||
|
fireEvent(this, "select-brand", {
|
||||||
|
brand: integration.domain,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(domain === this.domain &&
|
(domain === this.domain &&
|
||||||
(!this.integration!.integrations ||
|
(("integration_type" in this.integration! &&
|
||||||
!(domain in this.integration!.integrations)) &&
|
!this.integration.config_flow) ||
|
||||||
!this.integration!.config_flow) ||
|
(!("integration_type" in this.integration!) &&
|
||||||
this.integration!.integrations?.[domain]?.config_flow === false
|
(!this.integration!.integrations ||
|
||||||
|
!(domain in this.integration!.integrations))))) ||
|
||||||
|
(this.integration as Brand)!.integrations?.[domain]?.config_flow === false
|
||||||
) {
|
) {
|
||||||
const manifest = await fetchIntegrationManifest(this.hass, domain);
|
const manifest = await fetchIntegrationManifest(this.hass, domain);
|
||||||
showYamlIntegrationDialog(this, { manifest });
|
showYamlIntegrationDialog(this, { manifest });
|
||||||
|
Loading…
x
Reference in New Issue
Block a user