mirror of
https://github.com/home-assistant/frontend.git
synced 2025-06-26 12:06:34 +00:00
Integrations v2 (#13887)
* WIP: Integrations v2 * update * manifests * update wording * show yaml only * Show spinner * Update * Use virtulizer * Update * change interval if 5 min stats * remove yaml * fix application credentials * Add zwave and zigbee device support * make back button bigger * margin
This commit is contained in:
parent
dddb922593
commit
8e4bebb694
@ -1,11 +1,11 @@
|
|||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
import { getConfigEntries } from "../../data/config_entries";
|
import { getConfigEntries } from "../../data/config_entries";
|
||||||
|
import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow";
|
||||||
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
||||||
import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node";
|
import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { documentationUrl } from "../../util/documentation-url";
|
import { documentationUrl } from "../../util/documentation-url";
|
||||||
import { isComponentLoaded } from "../config/is_component_loaded";
|
import { isComponentLoaded } from "../config/is_component_loaded";
|
||||||
import { fireEvent } from "../dom/fire_event";
|
|
||||||
import { navigate } from "../navigate";
|
import { navigate } from "../navigate";
|
||||||
|
|
||||||
export const protocolIntegrationPicked = async (
|
export const protocolIntegrationPicked = async (
|
||||||
@ -39,8 +39,8 @@ export const protocolIntegrationPicked = async (
|
|||||||
"ui.panel.config.integrations.config_flow.proceed"
|
"ui.panel.config.integrations.config_flow.proceed"
|
||||||
),
|
),
|
||||||
confirm: () => {
|
confirm: () => {
|
||||||
fireEvent(element, "handler-picked", {
|
showConfigFlowDialog(element, {
|
||||||
handler: "zwave_js",
|
startFlowHandler: "zwave_js",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -75,8 +75,8 @@ export const protocolIntegrationPicked = async (
|
|||||||
"ui.panel.config.integrations.config_flow.proceed"
|
"ui.panel.config.integrations.config_flow.proceed"
|
||||||
),
|
),
|
||||||
confirm: () => {
|
confirm: () => {
|
||||||
fireEvent(element, "handler-picked", {
|
showConfigFlowDialog(element, {
|
||||||
handler: "zha",
|
startFlowHandler: "zha",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -91,7 +91,7 @@ export class HaDialog extends DialogBase {
|
|||||||
.header_button {
|
.header_button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
top: 10px;
|
top: 14px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
37
src/data/integrations.ts
Normal file
37
src/data/integrations.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
|
export type IotStandards = "z-wave" | "zigbee" | "homekit" | "matter";
|
||||||
|
|
||||||
|
export interface Integration {
|
||||||
|
name?: string;
|
||||||
|
config_flow?: boolean;
|
||||||
|
integrations?: Integrations;
|
||||||
|
iot_standards?: IotStandards[];
|
||||||
|
is_built_in?: boolean;
|
||||||
|
iot_class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Integrations {
|
||||||
|
[domain: string]: Integration;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IntegrationDescriptions {
|
||||||
|
core: {
|
||||||
|
integration: Integrations;
|
||||||
|
hardware: Integrations;
|
||||||
|
helper: Integrations;
|
||||||
|
translated_name: string[];
|
||||||
|
};
|
||||||
|
custom: {
|
||||||
|
integration: Integrations;
|
||||||
|
hardware: Integrations;
|
||||||
|
helper: Integrations;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getIntegrationDescriptions = (
|
||||||
|
hass: HomeAssistant
|
||||||
|
): Promise<IntegrationDescriptions> =>
|
||||||
|
hass.callWS<IntegrationDescriptions>({
|
||||||
|
type: "integration/descriptions",
|
||||||
|
});
|
@ -1,6 +1,13 @@
|
|||||||
import { SupportedBrandObj } from "../dialogs/config-flow/step-flow-pick-handler";
|
|
||||||
import type { HomeAssistant } from "../types";
|
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 type SupportedBrandHandler = Record<string, string>;
|
||||||
|
|
||||||
export const getSupportedBrands = (hass: HomeAssistant) =>
|
export const getSupportedBrands = (hass: HomeAssistant) =>
|
||||||
|
@ -18,9 +18,7 @@ import {
|
|||||||
AreaRegistryEntry,
|
AreaRegistryEntry,
|
||||||
subscribeAreaRegistry,
|
subscribeAreaRegistry,
|
||||||
} from "../../data/area_registry";
|
} from "../../data/area_registry";
|
||||||
import { fetchConfigFlowInProgress } from "../../data/config_flow";
|
|
||||||
import {
|
import {
|
||||||
DataEntryFlowProgress,
|
|
||||||
DataEntryFlowStep,
|
DataEntryFlowStep,
|
||||||
subscribeDataEntryFlowProgressed,
|
subscribeDataEntryFlowProgressed,
|
||||||
} from "../../data/data_entry_flow";
|
} from "../../data/data_entry_flow";
|
||||||
@ -28,14 +26,12 @@ import {
|
|||||||
DeviceRegistryEntry,
|
DeviceRegistryEntry,
|
||||||
subscribeDeviceRegistry,
|
subscribeDeviceRegistry,
|
||||||
} from "../../data/device_registry";
|
} from "../../data/device_registry";
|
||||||
import { fetchIntegrationManifest } from "../../data/integration";
|
|
||||||
import { haStyleDialog } from "../../resources/styles";
|
import { haStyleDialog } from "../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { documentationUrl } from "../../util/documentation-url";
|
import { documentationUrl } from "../../util/documentation-url";
|
||||||
import { showAlertDialog } from "../generic/show-dialog-box";
|
import { showAlertDialog } from "../generic/show-dialog-box";
|
||||||
import {
|
import {
|
||||||
DataEntryFlowDialogParams,
|
DataEntryFlowDialogParams,
|
||||||
FlowHandlers,
|
|
||||||
LoadingReason,
|
LoadingReason,
|
||||||
} from "./show-dialog-data-entry-flow";
|
} from "./show-dialog-data-entry-flow";
|
||||||
import "./step-flow-abort";
|
import "./step-flow-abort";
|
||||||
@ -44,8 +40,6 @@ import "./step-flow-external";
|
|||||||
import "./step-flow-form";
|
import "./step-flow-form";
|
||||||
import "./step-flow-loading";
|
import "./step-flow-loading";
|
||||||
import "./step-flow-menu";
|
import "./step-flow-menu";
|
||||||
import "./step-flow-pick-flow";
|
|
||||||
import "./step-flow-pick-handler";
|
|
||||||
import "./step-flow-progress";
|
import "./step-flow-progress";
|
||||||
|
|
||||||
let instance = 0;
|
let instance = 0;
|
||||||
@ -86,12 +80,8 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
|
|
||||||
@state() private _areas?: AreaRegistryEntry[];
|
@state() private _areas?: AreaRegistryEntry[];
|
||||||
|
|
||||||
@state() private _handlers?: FlowHandlers;
|
|
||||||
|
|
||||||
@state() private _handler?: string;
|
@state() private _handler?: string;
|
||||||
|
|
||||||
@state() private _flowsInProgress?: DataEntryFlowProgress[];
|
|
||||||
|
|
||||||
private _unsubAreas?: UnsubscribeFunc;
|
private _unsubAreas?: UnsubscribeFunc;
|
||||||
|
|
||||||
private _unsubDevices?: UnsubscribeFunc;
|
private _unsubDevices?: UnsubscribeFunc;
|
||||||
@ -102,15 +92,39 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
this._params = params;
|
this._params = params;
|
||||||
this._instance = instance++;
|
this._instance = instance++;
|
||||||
|
|
||||||
if (params.startFlowHandler) {
|
const curInstance = this._instance;
|
||||||
this._checkFlowsInProgress(params.startFlowHandler);
|
let step: DataEntryFlowStep;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.continueFlowId) {
|
if (params.startFlowHandler) {
|
||||||
|
this._loading = "loading_flow";
|
||||||
|
this._handler = params.startFlowHandler;
|
||||||
|
try {
|
||||||
|
step = await this._params!.flowConfig.createFlow(
|
||||||
|
this.hass,
|
||||||
|
params.startFlowHandler
|
||||||
|
);
|
||||||
|
} catch (err: any) {
|
||||||
|
this.closeDialog();
|
||||||
|
let message = err.message || err.body || "Unknown error";
|
||||||
|
if (typeof message !== "string") {
|
||||||
|
message = JSON.stringify(message);
|
||||||
|
}
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.error"
|
||||||
|
),
|
||||||
|
text: `${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.could_not_load"
|
||||||
|
)}: ${message}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Happens if second showDialog called
|
||||||
|
if (curInstance !== this._instance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (params.continueFlowId) {
|
||||||
this._loading = "loading_flow";
|
this._loading = "loading_flow";
|
||||||
const curInstance = this._instance;
|
|
||||||
let step: DataEntryFlowStep;
|
|
||||||
try {
|
try {
|
||||||
step = await params.flowConfig.fetchFlow(
|
step = await params.flowConfig.fetchFlow(
|
||||||
this.hass,
|
this.hass,
|
||||||
@ -132,32 +146,17 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// Happens if second showDialog called
|
|
||||||
if (curInstance !== this._instance) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._processStep(step);
|
|
||||||
this._loading = undefined;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new config flow. Show picker
|
// Happens if second showDialog called
|
||||||
if (!params.flowConfig.getFlowHandlers) {
|
if (curInstance !== this._instance) {
|
||||||
throw new Error("No getFlowHandlers defined in flow config");
|
return;
|
||||||
}
|
}
|
||||||
this._step = null;
|
|
||||||
|
|
||||||
// We only load the handlers once
|
this._processStep(step);
|
||||||
if (this._handlers === undefined) {
|
this._loading = undefined;
|
||||||
this._loading = "loading_handlers";
|
|
||||||
try {
|
|
||||||
this._handlers = await params.flowConfig.getFlowHandlers(this.hass);
|
|
||||||
} finally {
|
|
||||||
this._loading = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog() {
|
public closeDialog() {
|
||||||
@ -185,7 +184,6 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
this._step = undefined;
|
this._step = undefined;
|
||||||
this._params = undefined;
|
this._params = undefined;
|
||||||
this._devices = undefined;
|
this._devices = undefined;
|
||||||
this._flowsInProgress = undefined;
|
|
||||||
this._handler = undefined;
|
this._handler = undefined;
|
||||||
if (this._unsubAreas) {
|
if (this._unsubAreas) {
|
||||||
this._unsubAreas();
|
this._unsubAreas();
|
||||||
@ -218,15 +216,12 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
hideActions
|
hideActions
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
${this._loading ||
|
${this._loading || this._step === null
|
||||||
(this._step === null &&
|
|
||||||
this._handlers === undefined &&
|
|
||||||
this._handler === undefined)
|
|
||||||
? html`
|
? html`
|
||||||
<step-flow-loading
|
<step-flow-loading
|
||||||
.flowConfig=${this._params.flowConfig}
|
.flowConfig=${this._params.flowConfig}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.loadingReason=${this._loading || "loading_handlers"}
|
.loadingReason=${this._loading}
|
||||||
.handler=${this._handler}
|
.handler=${this._handler}
|
||||||
.step=${this._step}
|
.step=${this._step}
|
||||||
></step-flow-loading>
|
></step-flow-loading>
|
||||||
@ -273,24 +268,7 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
dialogAction="close"
|
dialogAction="close"
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
</div>
|
</div>
|
||||||
${this._step === null
|
${this._step.type === "form"
|
||||||
? this._handler
|
|
||||||
? html`<step-flow-pick-flow
|
|
||||||
.flowConfig=${this._params.flowConfig}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.handler=${this._handler}
|
|
||||||
.flowsInProgress=${this._flowsInProgress}
|
|
||||||
></step-flow-pick-flow>`
|
|
||||||
: // Show handler picker
|
|
||||||
html`
|
|
||||||
<step-flow-pick-handler
|
|
||||||
.hass=${this.hass}
|
|
||||||
.handlers=${this._handlers}
|
|
||||||
.initialFilter=${this._params.searchQuery}
|
|
||||||
@handler-picked=${this._handlerPicked}
|
|
||||||
></step-flow-pick-handler>
|
|
||||||
`
|
|
||||||
: this._step.type === "form"
|
|
||||||
? html`
|
? html`
|
||||||
<step-flow-form
|
<step-flow-form
|
||||||
.flowConfig=${this._params.flowConfig}
|
.flowConfig=${this._params.flowConfig}
|
||||||
@ -400,64 +378,6 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _checkFlowsInProgress(handler: string) {
|
|
||||||
this._loading = "loading_handlers";
|
|
||||||
this._handler = handler;
|
|
||||||
|
|
||||||
const flowsInProgress = (
|
|
||||||
await fetchConfigFlowInProgress(this.hass.connection)
|
|
||||||
).filter((flow) => flow.handler === handler);
|
|
||||||
|
|
||||||
if (!flowsInProgress.length) {
|
|
||||||
// No flows in progress, create a new flow
|
|
||||||
this._loading = "loading_flow";
|
|
||||||
let step: DataEntryFlowStep;
|
|
||||||
try {
|
|
||||||
step = await this._params!.flowConfig.createFlow(this.hass, handler);
|
|
||||||
} catch (err: any) {
|
|
||||||
this.closeDialog();
|
|
||||||
const message =
|
|
||||||
err?.status_code === 404
|
|
||||||
? this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.config_flow.no_config_flow"
|
|
||||||
)
|
|
||||||
: `${this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.config_flow.could_not_load"
|
|
||||||
)}: ${err?.body?.message || err?.message}`;
|
|
||||||
|
|
||||||
showAlertDialog(this, {
|
|
||||||
title: this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.config_flow.error"
|
|
||||||
),
|
|
||||||
text: message,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
} finally {
|
|
||||||
this._handler = undefined;
|
|
||||||
}
|
|
||||||
this._processStep(step);
|
|
||||||
if (this._params!.manifest === undefined) {
|
|
||||||
try {
|
|
||||||
this._params!.manifest = await fetchIntegrationManifest(
|
|
||||||
this.hass,
|
|
||||||
this._params?.domain || step.handler
|
|
||||||
);
|
|
||||||
} catch (_) {
|
|
||||||
// No manifest
|
|
||||||
this._params!.manifest = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._step = null;
|
|
||||||
this._flowsInProgress = flowsInProgress;
|
|
||||||
}
|
|
||||||
this._loading = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handlerPicked(ev) {
|
|
||||||
this._checkFlowsInProgress(ev.detail.handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _processStep(
|
private async _processStep(
|
||||||
step: DataEntryFlowStep | undefined | Promise<DataEntryFlowStep>
|
step: DataEntryFlowStep | undefined | Promise<DataEntryFlowStep>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
@ -3,11 +3,9 @@ import {
|
|||||||
createConfigFlow,
|
createConfigFlow,
|
||||||
deleteConfigFlow,
|
deleteConfigFlow,
|
||||||
fetchConfigFlow,
|
fetchConfigFlow,
|
||||||
getConfigFlowHandlers,
|
|
||||||
handleConfigFlowStep,
|
handleConfigFlowStep,
|
||||||
} from "../../data/config_flow";
|
} from "../../data/config_flow";
|
||||||
import { domainToName } from "../../data/integration";
|
import { domainToName } from "../../data/integration";
|
||||||
import { getSupportedBrands } from "../../data/supported_brands";
|
|
||||||
import {
|
import {
|
||||||
DataEntryFlowDialogParams,
|
DataEntryFlowDialogParams,
|
||||||
loadDataEntryFlowDialog,
|
loadDataEntryFlowDialog,
|
||||||
@ -22,16 +20,6 @@ export const showConfigFlowDialog = (
|
|||||||
): void =>
|
): void =>
|
||||||
showFlowDialog(element, dialogParams, {
|
showFlowDialog(element, dialogParams, {
|
||||||
loadDevicesAndAreas: true,
|
loadDevicesAndAreas: true,
|
||||||
getFlowHandlers: async (hass) => {
|
|
||||||
const [integrations, helpers, supportedBrands] = await Promise.all([
|
|
||||||
getConfigFlowHandlers(hass, "integration"),
|
|
||||||
getConfigFlowHandlers(hass, "helper"),
|
|
||||||
getSupportedBrands(hass),
|
|
||||||
hass.loadBackendTranslation("title", undefined, true),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { integrations, helpers, supportedBrands };
|
|
||||||
},
|
|
||||||
createFlow: async (hass, handler) => {
|
createFlow: async (hass, handler) => {
|
||||||
const [step] = await Promise.all([
|
const [step] = await Promise.all([
|
||||||
createConfigFlow(hass, handler),
|
createConfigFlow(hass, handler),
|
||||||
|
@ -22,8 +22,6 @@ export interface FlowHandlers {
|
|||||||
export interface FlowConfig {
|
export interface FlowConfig {
|
||||||
loadDevicesAndAreas: boolean;
|
loadDevicesAndAreas: boolean;
|
||||||
|
|
||||||
getFlowHandlers?: (hass: HomeAssistant) => Promise<FlowHandlers>;
|
|
||||||
|
|
||||||
createFlow(hass: HomeAssistant, handler: string): Promise<DataEntryFlowStep>;
|
createFlow(hass: HomeAssistant, handler: string): Promise<DataEntryFlowStep>;
|
||||||
|
|
||||||
fetchFlow(hass: HomeAssistant, flowId: string): Promise<DataEntryFlowStep>;
|
fetchFlow(hass: HomeAssistant, flowId: string): Promise<DataEntryFlowStep>;
|
||||||
|
@ -12,10 +12,10 @@ import { DataEntryFlowStepAbort } from "../../data/data_entry_flow";
|
|||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import { showAddApplicationCredentialDialog } from "../../panels/config/application_credentials/show-dialog-add-application-credential";
|
import { showAddApplicationCredentialDialog } from "../../panels/config/application_credentials/show-dialog-add-application-credential";
|
||||||
import { configFlowContentStyles } from "./styles";
|
import { configFlowContentStyles } from "./styles";
|
||||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
|
||||||
import { domainToName } from "../../data/integration";
|
|
||||||
import { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow";
|
import { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow";
|
||||||
import { showConfigFlowDialog } from "./show-dialog-config-flow";
|
import { showConfigFlowDialog } from "./show-dialog-config-flow";
|
||||||
|
import { domainToName } from "../../data/integration";
|
||||||
|
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||||
|
|
||||||
@customElement("step-flow-abort")
|
@customElement("step-flow-abort")
|
||||||
class StepFlowAbort extends LitElement {
|
class StepFlowAbort extends LitElement {
|
||||||
@ -56,11 +56,16 @@ class StepFlowAbort extends LitElement {
|
|||||||
private async _handleMissingCreds() {
|
private async _handleMissingCreds() {
|
||||||
const confirm = await showConfirmationDialog(this, {
|
const confirm = await showConfirmationDialog(this, {
|
||||||
title: this.hass.localize(
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.missing_credentials_title"
|
||||||
|
),
|
||||||
|
text: this.hass.localize(
|
||||||
"ui.panel.config.integrations.config_flow.missing_credentials",
|
"ui.panel.config.integrations.config_flow.missing_credentials",
|
||||||
{
|
{
|
||||||
integration: domainToName(this.hass.localize, this.domain),
|
integration: domainToName(this.hass.localize, this.domain),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
confirmText: this.hass.localize("ui.common.yes"),
|
||||||
|
dismissText: this.hass.localize("ui.common.no"),
|
||||||
});
|
});
|
||||||
this._flowDone();
|
this._flowDone();
|
||||||
if (!confirm) {
|
if (!confirm) {
|
||||||
|
@ -1,130 +0,0 @@
|
|||||||
import "@polymer/paper-item";
|
|
||||||
import "@polymer/paper-item/paper-icon-item";
|
|
||||||
import "@polymer/paper-item/paper-item";
|
|
||||||
import "@polymer/paper-item/paper-item-body";
|
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
|
||||||
import { customElement, property } from "lit/decorators";
|
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
|
||||||
import "../../components/ha-icon-next";
|
|
||||||
import { localizeConfigFlowTitle } from "../../data/config_flow";
|
|
||||||
import { DataEntryFlowProgress } from "../../data/data_entry_flow";
|
|
||||||
import { domainToName } from "../../data/integration";
|
|
||||||
import { HomeAssistant } from "../../types";
|
|
||||||
import { brandsUrl } from "../../util/brands-url";
|
|
||||||
import { FlowConfig } from "./show-dialog-data-entry-flow";
|
|
||||||
import { configFlowContentStyles } from "./styles";
|
|
||||||
|
|
||||||
@customElement("step-flow-pick-flow")
|
|
||||||
class StepFlowPickFlow extends LitElement {
|
|
||||||
public flowConfig!: FlowConfig;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public flowsInProgress!: DataEntryFlowProgress[];
|
|
||||||
|
|
||||||
@property() public handler!: string;
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
return html`
|
|
||||||
<h2>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.config_flow.pick_flow_step.title"
|
|
||||||
)}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
${this.flowsInProgress.map(
|
|
||||||
(flow) => html` <paper-icon-item
|
|
||||||
@click=${this._flowInProgressPicked}
|
|
||||||
.flow=${flow}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
slot="item-icon"
|
|
||||||
loading="lazy"
|
|
||||||
src=${brandsUrl({
|
|
||||||
domain: flow.handler,
|
|
||||||
type: "icon",
|
|
||||||
useFallback: true,
|
|
||||||
darkOptimized: this.hass.themes?.darkMode,
|
|
||||||
})}
|
|
||||||
referrerpolicy="no-referrer"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<paper-item-body>
|
|
||||||
${localizeConfigFlowTitle(this.hass.localize, flow)}
|
|
||||||
</paper-item-body>
|
|
||||||
<ha-icon-next></ha-icon-next>
|
|
||||||
</paper-icon-item>`
|
|
||||||
)}
|
|
||||||
<paper-item @click=${this._startNewFlowPicked} .handler=${this.handler}>
|
|
||||||
<paper-item-body>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.config_flow.pick_flow_step.new_flow",
|
|
||||||
"integration",
|
|
||||||
domainToName(this.hass.localize, this.handler)
|
|
||||||
)}
|
|
||||||
</paper-item-body>
|
|
||||||
<ha-icon-next></ha-icon-next>
|
|
||||||
</paper-item>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _startNewFlowPicked(ev) {
|
|
||||||
this._startFlow(ev.currentTarget.handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _startFlow(handler: string) {
|
|
||||||
fireEvent(this, "flow-update", {
|
|
||||||
stepPromise: this.flowConfig.createFlow(this.hass, handler),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _flowInProgressPicked(ev) {
|
|
||||||
const flow: DataEntryFlowProgress = ev.currentTarget.flow;
|
|
||||||
fireEvent(this, "flow-update", {
|
|
||||||
stepPromise: this.flowConfig.fetchFlow(this.hass, flow.flow_id),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return [
|
|
||||||
configFlowContentStyles,
|
|
||||||
css`
|
|
||||||
img {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
ha-icon-next {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
overflow: auto;
|
|
||||||
max-height: 600px;
|
|
||||||
margin: 16px 0;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
padding-inline-end: 66px;
|
|
||||||
direction: var(--direction);
|
|
||||||
}
|
|
||||||
@media all and (max-height: 900px) {
|
|
||||||
div {
|
|
||||||
max-height: calc(100vh - 134px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
paper-icon-item,
|
|
||||||
paper-item {
|
|
||||||
cursor: pointer;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"step-flow-pick-flow": StepFlowPickFlow;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,372 +0,0 @@
|
|||||||
import "@material/mwc-list/mwc-list";
|
|
||||||
import "@material/mwc-list/mwc-list-item";
|
|
||||||
import Fuse from "fuse.js";
|
|
||||||
import {
|
|
||||||
css,
|
|
||||||
CSSResultGroup,
|
|
||||||
html,
|
|
||||||
LitElement,
|
|
||||||
PropertyValues,
|
|
||||||
TemplateResult,
|
|
||||||
} from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import { styleMap } from "lit/directives/style-map";
|
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
|
||||||
import { protocolIntegrationPicked } from "../../common/integrations/protocolIntegrationPicked";
|
|
||||||
import { navigate } from "../../common/navigate";
|
|
||||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
|
||||||
import { LocalizeFunc } from "../../common/translations/localize";
|
|
||||||
import "../../components/ha-icon-next";
|
|
||||||
import "../../components/search-input";
|
|
||||||
import { domainToName } from "../../data/integration";
|
|
||||||
import { haStyleScrollbar } from "../../resources/styles";
|
|
||||||
import { HomeAssistant } from "../../types";
|
|
||||||
import { brandsUrl } from "../../util/brands-url";
|
|
||||||
import { documentationUrl } from "../../util/documentation-url";
|
|
||||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
|
||||||
import { FlowHandlers } from "./show-dialog-data-entry-flow";
|
|
||||||
import { configFlowContentStyles } from "./styles";
|
|
||||||
|
|
||||||
interface HandlerObj {
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
is_add?: boolean;
|
|
||||||
is_helper?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SupportedBrandObj extends HandlerObj {
|
|
||||||
supported_flows: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
// for fire event
|
|
||||||
interface HASSDomEvents {
|
|
||||||
"handler-picked": {
|
|
||||||
handler: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("step-flow-pick-handler")
|
|
||||||
class StepFlowPickHandler extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public handlers!: FlowHandlers;
|
|
||||||
|
|
||||||
@property() public initialFilter?: string;
|
|
||||||
|
|
||||||
@state() private _filter?: string;
|
|
||||||
|
|
||||||
private _width?: number;
|
|
||||||
|
|
||||||
private _height?: number;
|
|
||||||
|
|
||||||
private _filterHandlers = memoizeOne(
|
|
||||||
(
|
|
||||||
h: FlowHandlers,
|
|
||||||
filter?: string,
|
|
||||||
_localize?: LocalizeFunc
|
|
||||||
): [(HandlerObj | SupportedBrandObj)[], HandlerObj[]] => {
|
|
||||||
const integrations: (HandlerObj | SupportedBrandObj)[] =
|
|
||||||
h.integrations.map((handler) => ({
|
|
||||||
name: domainToName(this.hass.localize, handler),
|
|
||||||
slug: handler,
|
|
||||||
}));
|
|
||||||
|
|
||||||
for (const [domain, domainBrands] of Object.entries(h.supportedBrands)) {
|
|
||||||
for (const [slug, name] of Object.entries(domainBrands)) {
|
|
||||||
integrations.push({
|
|
||||||
slug,
|
|
||||||
name,
|
|
||||||
supported_flows: [domain],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter) {
|
|
||||||
const options: Fuse.IFuseOptions<HandlerObj> = {
|
|
||||||
keys: ["name", "slug"],
|
|
||||||
isCaseSensitive: false,
|
|
||||||
minMatchCharLength: 2,
|
|
||||||
threshold: 0.2,
|
|
||||||
};
|
|
||||||
const helpers: HandlerObj[] = h.helpers.map((handler) => ({
|
|
||||||
name: domainToName(this.hass.localize, handler),
|
|
||||||
slug: handler,
|
|
||||||
is_helper: true,
|
|
||||||
}));
|
|
||||||
return [
|
|
||||||
new Fuse(integrations, options)
|
|
||||||
.search(filter)
|
|
||||||
.map((result) => result.item),
|
|
||||||
new Fuse(helpers, options)
|
|
||||||
.search(filter)
|
|
||||||
.map((result) => result.item),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
integrations.sort((a, b) =>
|
|
||||||
caseInsensitiveStringCompare(a.name, b.name)
|
|
||||||
),
|
|
||||||
[],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
const [integrations, helpers] = this._getHandlers();
|
|
||||||
|
|
||||||
const addDeviceRows: HandlerObj[] = ["zha", "zwave_js"]
|
|
||||||
.filter((domain) => isComponentLoaded(this.hass, domain))
|
|
||||||
.map((domain) => ({
|
|
||||||
name: this.hass.localize(
|
|
||||||
`ui.panel.config.integrations.add_${domain}_device`
|
|
||||||
),
|
|
||||||
slug: domain,
|
|
||||||
is_add: true,
|
|
||||||
}))
|
|
||||||
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name));
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<h2>${this.hass.localize("ui.panel.config.integrations.new")}</h2>
|
|
||||||
<search-input
|
|
||||||
.hass=${this.hass}
|
|
||||||
autofocus
|
|
||||||
.filter=${this._filter}
|
|
||||||
@value-changed=${this._filterChanged}
|
|
||||||
.label=${this.hass.localize("ui.panel.config.integrations.search")}
|
|
||||||
@keypress=${this._maybeSubmit}
|
|
||||||
></search-input>
|
|
||||||
<mwc-list
|
|
||||||
style=${styleMap({
|
|
||||||
width: `${this._width}px`,
|
|
||||||
height: `${this._height}px`,
|
|
||||||
})}
|
|
||||||
class="ha-scrollbar"
|
|
||||||
>
|
|
||||||
${addDeviceRows.length
|
|
||||||
? html`
|
|
||||||
${addDeviceRows.map((handler) => this._renderRow(handler))}
|
|
||||||
<li divider padded class="divider" role="separator"></li>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
${integrations.length
|
|
||||||
? integrations.map((handler) => this._renderRow(handler))
|
|
||||||
: html`
|
|
||||||
<p>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.note_about_integrations"
|
|
||||||
)}<br />
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.note_about_website_reference"
|
|
||||||
)}<a
|
|
||||||
href=${documentationUrl(
|
|
||||||
this.hass,
|
|
||||||
`/integrations/${
|
|
||||||
this._filter ? `#search/${this._filter}` : ""
|
|
||||||
}`
|
|
||||||
)}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>${this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.home_assistant_website"
|
|
||||||
)}</a
|
|
||||||
>.
|
|
||||||
</p>
|
|
||||||
`}
|
|
||||||
${helpers.length
|
|
||||||
? html`
|
|
||||||
<li divider padded class="divider" role="separator"></li>
|
|
||||||
${helpers.map((handler) => this._renderRow(handler))}
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</mwc-list>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderRow(handler: HandlerObj) {
|
|
||||||
return html`
|
|
||||||
<mwc-list-item
|
|
||||||
graphic="medium"
|
|
||||||
.hasMeta=${!handler.is_add}
|
|
||||||
.handler=${handler}
|
|
||||||
@click=${this._handlerPicked}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
slot="graphic"
|
|
||||||
loading="lazy"
|
|
||||||
src=${brandsUrl({
|
|
||||||
domain: handler.slug,
|
|
||||||
type: "icon",
|
|
||||||
useFallback: true,
|
|
||||||
darkOptimized: this.hass.themes?.darkMode,
|
|
||||||
})}
|
|
||||||
referrerpolicy="no-referrer"
|
|
||||||
/>
|
|
||||||
<span>${handler.name} ${handler.is_helper ? " (helper)" : ""}</span>
|
|
||||||
${handler.is_add ? "" : html`<ha-icon-next slot="meta"></ha-icon-next>`}
|
|
||||||
</mwc-list-item>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public willUpdate(changedProps: PropertyValues): void {
|
|
||||||
super.willUpdate(changedProps);
|
|
||||||
if (this._filter === undefined && this.initialFilter !== undefined) {
|
|
||||||
this._filter = this.initialFilter;
|
|
||||||
}
|
|
||||||
if (this.initialFilter !== undefined && this._filter === "") {
|
|
||||||
this.initialFilter = undefined;
|
|
||||||
this._filter = "";
|
|
||||||
this._width = undefined;
|
|
||||||
this._height = undefined;
|
|
||||||
} else if (
|
|
||||||
this.hasUpdated &&
|
|
||||||
changedProps.has("_filter") &&
|
|
||||||
(!this._width || !this._height)
|
|
||||||
) {
|
|
||||||
// Store the width and height so that when we search, box doesn't jump
|
|
||||||
const boundingRect =
|
|
||||||
this.shadowRoot!.querySelector("mwc-list")!.getBoundingClientRect();
|
|
||||||
this._width = boundingRect.width;
|
|
||||||
this._height = boundingRect.height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected firstUpdated(changedProps) {
|
|
||||||
super.firstUpdated(changedProps);
|
|
||||||
setTimeout(
|
|
||||||
() => this.shadowRoot!.querySelector("search-input")!.focus(),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getHandlers() {
|
|
||||||
return this._filterHandlers(
|
|
||||||
this.handlers,
|
|
||||||
this._filter,
|
|
||||||
this.hass.localize
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _filterChanged(e) {
|
|
||||||
this._filter = e.detail.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _handlerPicked(ev) {
|
|
||||||
const handler: HandlerObj | SupportedBrandObj = ev.currentTarget.handler;
|
|
||||||
|
|
||||||
if (handler.is_add) {
|
|
||||||
this._handleAddPicked(handler.slug);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (handler.is_helper) {
|
|
||||||
navigate(`/config/helpers/add?domain=${handler.slug}`);
|
|
||||||
// This closes dialog.
|
|
||||||
fireEvent(this, "flow-update");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("supported_flows" in handler) {
|
|
||||||
const slug = handler.supported_flows[0];
|
|
||||||
|
|
||||||
showConfirmationDialog(this, {
|
|
||||||
text: this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.config_flow.supported_brand_flow",
|
|
||||||
{
|
|
||||||
supported_brand: handler.name,
|
|
||||||
flow_domain_name: domainToName(this.hass.localize, slug),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
confirm: () => {
|
|
||||||
if (["zha", "zwave_js"].includes(slug)) {
|
|
||||||
this._handleAddPicked(slug);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fireEvent(this, "handler-picked", {
|
|
||||||
handler: slug,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fireEvent(this, "handler-picked", {
|
|
||||||
handler: handler.slug,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _handleAddPicked(slug: string): Promise<void> {
|
|
||||||
await protocolIntegrationPicked(this, this.hass, slug);
|
|
||||||
// This closes dialog.
|
|
||||||
fireEvent(this, "flow-update");
|
|
||||||
}
|
|
||||||
|
|
||||||
private _maybeSubmit(ev: KeyboardEvent) {
|
|
||||||
if (ev.key !== "Enter") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlers = this._getHandlers();
|
|
||||||
|
|
||||||
if (handlers.length > 0) {
|
|
||||||
fireEvent(this, "handler-picked", {
|
|
||||||
handler: handlers[0][0].slug,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return [
|
|
||||||
configFlowContentStyles,
|
|
||||||
haStyleScrollbar,
|
|
||||||
css`
|
|
||||||
img {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
search-input {
|
|
||||||
display: block;
|
|
||||||
margin: 16px 16px 0;
|
|
||||||
}
|
|
||||||
ha-icon-next {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
mwc-list {
|
|
||||||
overflow: auto;
|
|
||||||
max-height: 600px;
|
|
||||||
}
|
|
||||||
.divider {
|
|
||||||
border-bottom-color: var(--divider-color);
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
padding-inline-end: 66px;
|
|
||||||
direction: var(--direction);
|
|
||||||
}
|
|
||||||
@media all and (max-height: 900px) {
|
|
||||||
mwc-list {
|
|
||||||
max-height: calc(100vh - 134px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
text-align: center;
|
|
||||||
padding: 16px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
p > a {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"step-flow-pick-handler": StepFlowPickHandler;
|
|
||||||
}
|
|
||||||
}
|
|
656
src/panels/config/integrations/dialog-add-integration.ts
Normal file
656
src/panels/config/integrations/dialog-add-integration.ts
Normal file
@ -0,0 +1,656 @@
|
|||||||
|
import "@material/mwc-button";
|
||||||
|
import "@material/mwc-list/mwc-list";
|
||||||
|
import Fuse from "fuse.js";
|
||||||
|
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||||
|
import { customElement, state } from "lit/decorators";
|
||||||
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked";
|
||||||
|
import { navigate } from "../../../common/navigate";
|
||||||
|
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||||
|
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||||
|
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||||
|
import "../../../components/ha-icon-button-prev";
|
||||||
|
import "../../../components/search-input";
|
||||||
|
import { fetchConfigFlowInProgress } from "../../../data/config_flow";
|
||||||
|
import { DataEntryFlowProgress } from "../../../data/data_entry_flow";
|
||||||
|
import {
|
||||||
|
domainToName,
|
||||||
|
fetchIntegrationManifest,
|
||||||
|
} from "../../../data/integration";
|
||||||
|
import {
|
||||||
|
getIntegrationDescriptions,
|
||||||
|
Integrations,
|
||||||
|
} from "../../../data/integrations";
|
||||||
|
import {
|
||||||
|
getSupportedBrands,
|
||||||
|
SupportedBrandHandler,
|
||||||
|
} from "../../../data/supported_brands";
|
||||||
|
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||||
|
import {
|
||||||
|
showAlertDialog,
|
||||||
|
showConfirmationDialog,
|
||||||
|
} from "../../../dialogs/generic/show-dialog-box";
|
||||||
|
import { haStyleDialog, haStyleScrollbar } from "../../../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import { documentationUrl } from "../../../util/documentation-url";
|
||||||
|
import "./ha-domain-integrations";
|
||||||
|
import "./ha-integration-list-item";
|
||||||
|
|
||||||
|
export interface IntegrationListItem {
|
||||||
|
name: string;
|
||||||
|
domain: string;
|
||||||
|
config_flow?: boolean;
|
||||||
|
is_helper?: boolean;
|
||||||
|
integrations?: string[];
|
||||||
|
iot_standards?: string[];
|
||||||
|
supported_flows?: string[];
|
||||||
|
cloud?: boolean;
|
||||||
|
is_built_in?: boolean;
|
||||||
|
is_add?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("dialog-add-integration")
|
||||||
|
class AddIntegrationDialog extends LitElement {
|
||||||
|
public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _integrations?: Integrations;
|
||||||
|
|
||||||
|
@state() private _helpers?: Integrations;
|
||||||
|
|
||||||
|
@state() private _supportedBrands?: Record<string, SupportedBrandHandler>;
|
||||||
|
|
||||||
|
@state() private _initialFilter?: string;
|
||||||
|
|
||||||
|
@state() private _filter?: string;
|
||||||
|
|
||||||
|
@state() private _pickedBrand?: string;
|
||||||
|
|
||||||
|
@state() private _flowsInProgress?: DataEntryFlowProgress[];
|
||||||
|
|
||||||
|
@state() private _open = false;
|
||||||
|
|
||||||
|
@state() private _narrow = false;
|
||||||
|
|
||||||
|
private _width?: number;
|
||||||
|
|
||||||
|
private _height?: number;
|
||||||
|
|
||||||
|
public showDialog(params): void {
|
||||||
|
this._open = true;
|
||||||
|
this._initialFilter = params.initialFilter;
|
||||||
|
this._narrow = matchMedia(
|
||||||
|
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||||
|
).matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog() {
|
||||||
|
this._open = false;
|
||||||
|
this._integrations = undefined;
|
||||||
|
this._helpers = undefined;
|
||||||
|
this._supportedBrands = undefined;
|
||||||
|
this._pickedBrand = undefined;
|
||||||
|
this._flowsInProgress = undefined;
|
||||||
|
this._filter = undefined;
|
||||||
|
this._width = undefined;
|
||||||
|
this._height = undefined;
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
|
||||||
|
public willUpdate(changedProps: PropertyValues): void {
|
||||||
|
super.willUpdate(changedProps);
|
||||||
|
if (this._filter === undefined && this._initialFilter !== undefined) {
|
||||||
|
this._filter = this._initialFilter;
|
||||||
|
}
|
||||||
|
if (this._initialFilter !== undefined && this._filter === "") {
|
||||||
|
this._initialFilter = undefined;
|
||||||
|
this._filter = "";
|
||||||
|
this._width = undefined;
|
||||||
|
this._height = undefined;
|
||||||
|
} else if (
|
||||||
|
this.hasUpdated &&
|
||||||
|
changedProps.has("_filter") &&
|
||||||
|
(!this._width || !this._height)
|
||||||
|
) {
|
||||||
|
// Store the width and height so that when we search, box doesn't jump
|
||||||
|
const boundingRect =
|
||||||
|
this.shadowRoot!.querySelector("mwc-list")?.getBoundingClientRect();
|
||||||
|
this._width = boundingRect?.width;
|
||||||
|
this._height = boundingRect?.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public updated(changedProps: PropertyValues) {
|
||||||
|
super.updated(changedProps);
|
||||||
|
if (changedProps.has("_open") && this._open) {
|
||||||
|
this._load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _filterIntegrations = memoizeOne(
|
||||||
|
(
|
||||||
|
i: Integrations,
|
||||||
|
h: Integrations,
|
||||||
|
sb: Record<string, SupportedBrandHandler>,
|
||||||
|
components: HomeAssistant["config"]["components"],
|
||||||
|
localize: LocalizeFunc,
|
||||||
|
filter?: string
|
||||||
|
): IntegrationListItem[] => {
|
||||||
|
const addDeviceRows: IntegrationListItem[] = ["zha", "zwave_js"]
|
||||||
|
.filter((domain) => components.includes(domain))
|
||||||
|
.map((domain) => ({
|
||||||
|
name: localize(`ui.panel.config.integrations.add_${domain}_device`),
|
||||||
|
domain,
|
||||||
|
config_flow: true,
|
||||||
|
is_built_in: true,
|
||||||
|
is_add: true,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name));
|
||||||
|
|
||||||
|
const integrations: IntegrationListItem[] = Object.entries(i)
|
||||||
|
.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,
|
||||||
|
iot_standards: integration.iot_standards,
|
||||||
|
integrations: integration.integrations
|
||||||
|
? Object.entries(integration.integrations).map(
|
||||||
|
([dom, val]) => val.name || domainToName(localize, dom)
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
is_built_in: integration.is_built_in !== false,
|
||||||
|
cloud: integration.iot_class?.startsWith("cloud_"),
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (const [domain, domainBrands] of Object.entries(sb)) {
|
||||||
|
const integration = i[domain];
|
||||||
|
if (
|
||||||
|
!integration.config_flow &&
|
||||||
|
!integration.iot_standards &&
|
||||||
|
!integration.integrations
|
||||||
|
) {
|
||||||
|
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<IntegrationListItem> = {
|
||||||
|
keys: [
|
||||||
|
"name",
|
||||||
|
"domain",
|
||||||
|
"supported_flows",
|
||||||
|
"integrations",
|
||||||
|
"iot_standards",
|
||||||
|
],
|
||||||
|
isCaseSensitive: false,
|
||||||
|
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_"),
|
||||||
|
}));
|
||||||
|
return [
|
||||||
|
...new Fuse(integrations, options)
|
||||||
|
.search(filter)
|
||||||
|
.map((result) => result.item),
|
||||||
|
...new Fuse(helpers, options)
|
||||||
|
.search(filter)
|
||||||
|
.map((result) => result.item),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
...addDeviceRows,
|
||||||
|
...integrations.sort((a, b) =>
|
||||||
|
caseInsensitiveStringCompare(a.name || "", b.name || "")
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private _getIntegrations() {
|
||||||
|
return this._filterIntegrations(
|
||||||
|
this._integrations!,
|
||||||
|
this._helpers!,
|
||||||
|
this._supportedBrands!,
|
||||||
|
this.hass.config.components,
|
||||||
|
this.hass.localize,
|
||||||
|
this._filter
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this._open) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
const integrations = this._integrations
|
||||||
|
? this._getIntegrations()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return html`<ha-dialog
|
||||||
|
open
|
||||||
|
@closed=${this.closeDialog}
|
||||||
|
scrimClickAction
|
||||||
|
escapeKeyAction
|
||||||
|
hideActions
|
||||||
|
.heading=${this._pickedBrand
|
||||||
|
? true
|
||||||
|
: createCloseHeading(
|
||||||
|
this.hass,
|
||||||
|
this.hass.localize("ui.panel.config.integrations.new")
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
${this._pickedBrand
|
||||||
|
? html`<div slot="heading">
|
||||||
|
<ha-icon-button-prev
|
||||||
|
@click=${this._prevClicked}
|
||||||
|
></ha-icon-button-prev>
|
||||||
|
<h2 class="mdc-dialog__title">
|
||||||
|
${this._calculateBrandHeading()}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
${this._renderIntegration()}`
|
||||||
|
: this._renderAll(integrations)}
|
||||||
|
</ha-dialog>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _calculateBrandHeading() {
|
||||||
|
const brand = this._integrations?.[this._pickedBrand!];
|
||||||
|
if (
|
||||||
|
brand?.iot_standards &&
|
||||||
|
!brand.integrations &&
|
||||||
|
!this._flowsInProgress?.length
|
||||||
|
) {
|
||||||
|
return "What type of device is it?";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!brand?.iot_standards &&
|
||||||
|
!brand?.integrations &&
|
||||||
|
this._flowsInProgress?.length
|
||||||
|
) {
|
||||||
|
return "Want to add these discovered devices?";
|
||||||
|
}
|
||||||
|
return "What do you want to add?";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderIntegration(): TemplateResult {
|
||||||
|
return html`<ha-domain-integrations
|
||||||
|
.hass=${this.hass}
|
||||||
|
.domain=${this._pickedBrand}
|
||||||
|
.integration=${this._integrations?.[this._pickedBrand!]}
|
||||||
|
.flowsInProgress=${this._flowsInProgress}
|
||||||
|
style=${styleMap({
|
||||||
|
minWidth: `${this._width}px`,
|
||||||
|
minHeight: `581px`,
|
||||||
|
})}
|
||||||
|
@close-dialog=${this.closeDialog}
|
||||||
|
></ha-domain-integrations>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderAll(integrations?: IntegrationListItem[]): TemplateResult {
|
||||||
|
return html`<search-input
|
||||||
|
.hass=${this.hass}
|
||||||
|
autofocus
|
||||||
|
dialogInitialFocus
|
||||||
|
.filter=${this._filter}
|
||||||
|
@value-changed=${this._filterChanged}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.search_brand"
|
||||||
|
)}
|
||||||
|
@keypress=${this._maybeSubmit}
|
||||||
|
></search-input>
|
||||||
|
${integrations
|
||||||
|
? html`<mwc-list>
|
||||||
|
<lit-virtualizer
|
||||||
|
scroller
|
||||||
|
class="ha-scrollbar"
|
||||||
|
style=${styleMap({
|
||||||
|
width: `${this._width}px`,
|
||||||
|
height: this._narrow ? "calc(100vh - 184px)" : "500px",
|
||||||
|
})}
|
||||||
|
@click=${this._integrationPicked}
|
||||||
|
.items=${integrations}
|
||||||
|
.renderItem=${this._renderRow}
|
||||||
|
>
|
||||||
|
</lit-virtualizer>
|
||||||
|
</mwc-list>`
|
||||||
|
: html`<ha-circular-progress active></ha-circular-progress>`} `;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderRow = (integration: IntegrationListItem) => {
|
||||||
|
if (!integration) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<ha-integration-list-item .hass=${this.hass} .integration=${integration}>
|
||||||
|
</ha-integration-list-item>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
private async _load() {
|
||||||
|
const [descriptions, supportedBrands] = await Promise.all([
|
||||||
|
getIntegrationDescriptions(this.hass),
|
||||||
|
getSupportedBrands(this.hass),
|
||||||
|
]);
|
||||||
|
for (const integration in descriptions.custom.integration) {
|
||||||
|
if (
|
||||||
|
!Object.prototype.hasOwnProperty.call(
|
||||||
|
descriptions.custom.integration,
|
||||||
|
integration
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
descriptions.custom.integration[integration].is_built_in = false;
|
||||||
|
}
|
||||||
|
this._integrations = {
|
||||||
|
...descriptions.core.integration,
|
||||||
|
...descriptions.custom.integration,
|
||||||
|
};
|
||||||
|
for (const integration in descriptions.custom.helper) {
|
||||||
|
if (
|
||||||
|
!Object.prototype.hasOwnProperty.call(
|
||||||
|
descriptions.custom.helper,
|
||||||
|
integration
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
descriptions.custom.helper[integration].is_built_in = false;
|
||||||
|
}
|
||||||
|
this._helpers = {
|
||||||
|
...descriptions.core.helper,
|
||||||
|
...descriptions.custom.helper,
|
||||||
|
};
|
||||||
|
this._supportedBrands = supportedBrands;
|
||||||
|
this.hass.loadBackendTranslation(
|
||||||
|
"title",
|
||||||
|
descriptions.core.translated_name,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _filterChanged(e) {
|
||||||
|
this._filter = e.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _integrationPicked(ev) {
|
||||||
|
const listItem = ev.target.closest("ha-integration-list-item");
|
||||||
|
const integration: IntegrationListItem = listItem.integration;
|
||||||
|
this._handleIntegrationPicked(integration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _handleIntegrationPicked(integration: IntegrationListItem) {
|
||||||
|
if ("supported_flows" in integration) {
|
||||||
|
const domain = integration.supported_flows![0];
|
||||||
|
|
||||||
|
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._integrations?.[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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (integration.is_add) {
|
||||||
|
protocolIntegrationPicked(this, this.hass, integration.domain);
|
||||||
|
this.closeDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (integration.is_helper) {
|
||||||
|
this.closeDialog();
|
||||||
|
navigate(`/config/helpers/add?domain=${integration.domain}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (integration.integrations) {
|
||||||
|
this._fetchFlowsInProgress(Object.keys(integration.integrations));
|
||||||
|
this._pickedBrand = integration.domain;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
["zha", "zwave_js"].includes(integration.domain) &&
|
||||||
|
isComponentLoaded(this.hass, integration.domain)
|
||||||
|
) {
|
||||||
|
this._pickedBrand = integration.domain;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (integration.iot_standards) {
|
||||||
|
this._pickedBrand = integration.domain;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (integration.config_flow) {
|
||||||
|
this._createFlow(integration);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const manifest = await fetchIntegrationManifest(
|
||||||
|
this.hass,
|
||||||
|
integration.domain
|
||||||
|
);
|
||||||
|
this.closeDialog();
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.yaml_only_title"
|
||||||
|
),
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.yaml_only_text",
|
||||||
|
{
|
||||||
|
link:
|
||||||
|
manifest?.is_built_in || manifest?.documentation
|
||||||
|
? html`<a
|
||||||
|
href=${manifest.is_built_in
|
||||||
|
? documentationUrl(
|
||||||
|
this.hass,
|
||||||
|
`/integrations/${manifest.domain}`
|
||||||
|
)
|
||||||
|
: manifest.documentation}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.documentation"
|
||||||
|
)}
|
||||||
|
</a>`
|
||||||
|
: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.documentation"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _createFlow(integration: IntegrationListItem) {
|
||||||
|
const flowsInProgress = await this._fetchFlowsInProgress([
|
||||||
|
integration.domain,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (flowsInProgress?.length) {
|
||||||
|
this._pickedBrand = integration.domain;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const manifest = await fetchIntegrationManifest(
|
||||||
|
this.hass,
|
||||||
|
integration.domain
|
||||||
|
);
|
||||||
|
|
||||||
|
this.closeDialog();
|
||||||
|
|
||||||
|
showConfigFlowDialog(this, {
|
||||||
|
startFlowHandler: integration.domain,
|
||||||
|
showAdvanced: this.hass.userData?.showAdvanced,
|
||||||
|
manifest,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fetchFlowsInProgress(domains: string[]) {
|
||||||
|
const flowsInProgress = (
|
||||||
|
await fetchConfigFlowInProgress(this.hass.connection)
|
||||||
|
).filter((flow) => domains.includes(flow.handler));
|
||||||
|
|
||||||
|
if (flowsInProgress.length) {
|
||||||
|
this._flowsInProgress = flowsInProgress;
|
||||||
|
}
|
||||||
|
return flowsInProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _maybeSubmit(ev: KeyboardEvent) {
|
||||||
|
if (ev.key !== "Enter") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const integrations = this._getIntegrations();
|
||||||
|
|
||||||
|
if (integrations.length > 0) {
|
||||||
|
this._handleIntegrationPicked(integrations[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _prevClicked() {
|
||||||
|
this._pickedBrand = undefined;
|
||||||
|
this._flowsInProgress = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = [
|
||||||
|
haStyleScrollbar,
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
|
ha-dialog {
|
||||||
|
--dialog-content-padding: 0;
|
||||||
|
}
|
||||||
|
search-input {
|
||||||
|
display: block;
|
||||||
|
margin: 16px 16px 0;
|
||||||
|
}
|
||||||
|
.divider {
|
||||||
|
border-bottom-color: var(--divider-color);
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
padding-inline-end: 66px;
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
text-align: center;
|
||||||
|
padding: 16px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
p > a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
ha-circular-progress {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 24px 0;
|
||||||
|
}
|
||||||
|
lit-virtualizer {
|
||||||
|
contain: size layout !important;
|
||||||
|
}
|
||||||
|
ha-integration-list-item {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
ha-icon-button-prev {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
top: 14px;
|
||||||
|
inset-inline-end: initial;
|
||||||
|
inset-inline-start: 16px;
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
.mdc-dialog__title {
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
margin-left: 48px;
|
||||||
|
padding: 24px 24px 0 24px;
|
||||||
|
color: var(--mdc-dialog-heading-ink-color, rgba(0, 0, 0, 0.87));
|
||||||
|
font-size: var(--mdc-typography-headline6-font-size, 1.25rem);
|
||||||
|
line-height: var(--mdc-typography-headline6-line-height, 2rem);
|
||||||
|
font-weight: var(--mdc-typography-headline6-font-weight, 500);
|
||||||
|
letter-spacing: var(
|
||||||
|
--mdc-typography-headline6-letter-spacing,
|
||||||
|
0.0125em
|
||||||
|
);
|
||||||
|
text-decoration: var(
|
||||||
|
--mdc-typography-headline6-text-decoration,
|
||||||
|
inherit
|
||||||
|
);
|
||||||
|
text-transform: var(--mdc-typography-headline6-text-transform, inherit);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"dialog-add-integration": AddIntegrationDialog;
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,6 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
|
||||||
import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked";
|
import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked";
|
||||||
import { navigate } from "../../../common/navigate";
|
import { navigate } from "../../../common/navigate";
|
||||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||||
@ -75,6 +74,7 @@ import "./ha-ignored-config-entry-card";
|
|||||||
import "./ha-integration-card";
|
import "./ha-integration-card";
|
||||||
import type { HaIntegrationCard } from "./ha-integration-card";
|
import type { HaIntegrationCard } from "./ha-integration-card";
|
||||||
import "./ha-integration-overflow-menu";
|
import "./ha-integration-overflow-menu";
|
||||||
|
import { showAddIntegrationDialog } from "./show-add-integration-dialog";
|
||||||
|
|
||||||
export interface ConfigEntryUpdatedEvent {
|
export interface ConfigEntryUpdatedEvent {
|
||||||
entry: ConfigEntry;
|
entry: ConfigEntry;
|
||||||
@ -312,7 +312,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
|||||||
undefined,
|
undefined,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
this._fetchManifests();
|
|
||||||
if (this.route.path === "/add") {
|
if (this.route.path === "/add") {
|
||||||
this._handleAdd(localizePromise);
|
this._handleAdd(localizePromise);
|
||||||
}
|
}
|
||||||
@ -599,7 +598,9 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
|||||||
// Make a copy so we can keep track of previously loaded manifests
|
// Make a copy so we can keep track of previously loaded manifests
|
||||||
// for discovered flows (which are not part of these results)
|
// for discovered flows (which are not part of these results)
|
||||||
const manifests = { ...this._manifests };
|
const manifests = { ...this._manifests };
|
||||||
for (const manifest of fetched) manifests[manifest.domain] = manifest;
|
for (const manifest of fetched) {
|
||||||
|
manifests[manifest.domain] = manifest;
|
||||||
|
}
|
||||||
this._manifests = manifests;
|
this._manifests = manifests;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -630,15 +631,9 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _createFlow() {
|
private _createFlow() {
|
||||||
showConfigFlowDialog(this, {
|
showAddIntegrationDialog(this, {
|
||||||
searchQuery: this._filter,
|
initialFilter: this._filter,
|
||||||
dialogClosedCallback: () => {
|
|
||||||
this._handleFlowUpdated();
|
|
||||||
},
|
|
||||||
showAdvanced: this.showAdvanced,
|
|
||||||
});
|
});
|
||||||
// For config entries. Also loading config flow ones for added integration
|
|
||||||
this.hass.loadBackendTranslation("title", undefined, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
private _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||||
@ -735,9 +730,13 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
|||||||
protocolIntegrationPicked(this, this.hass, slug);
|
protocolIntegrationPicked(this, this.hass, slug);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
showConfigFlowDialog(this, {
|
||||||
fireEvent(this, "handler-picked", {
|
dialogClosedCallback: () => {
|
||||||
handler: slug,
|
this._handleFlowUpdated();
|
||||||
|
},
|
||||||
|
startFlowHandler: slug,
|
||||||
|
manifest: this._manifests[slug],
|
||||||
|
showAdvanced: this.hass.userData?.showAdvanced,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
225
src/panels/config/integrations/ha-domain-integrations.ts
Normal file
225
src/panels/config/integrations/ha-domain-integrations.ts
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
import { css, html, LitElement } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked";
|
||||||
|
import { localizeConfigFlowTitle } from "../../../data/config_flow";
|
||||||
|
import { DataEntryFlowProgress } from "../../../data/data_entry_flow";
|
||||||
|
import {
|
||||||
|
domainToName,
|
||||||
|
fetchIntegrationManifest,
|
||||||
|
} from "../../../data/integration";
|
||||||
|
import { Integration } from "../../../data/integrations";
|
||||||
|
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||||
|
import { haStyle } from "../../../resources/styles";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { brandsUrl } from "../../../util/brands-url";
|
||||||
|
import "./ha-integration-list-item";
|
||||||
|
|
||||||
|
const standardToDomain = { zigbee: "zha", "z-wave": "zwave_js" } as const;
|
||||||
|
|
||||||
|
@customElement("ha-domain-integrations")
|
||||||
|
class HaDomainIntegrations extends LitElement {
|
||||||
|
public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public domain!: string;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public integration!: Integration;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public flowsInProgress?: DataEntryFlowProgress[];
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
${this.flowsInProgress?.length
|
||||||
|
? html`<h3>We discovered the following:</h3>
|
||||||
|
${this.flowsInProgress.map(
|
||||||
|
(flow) => html`<mwc-list-item
|
||||||
|
graphic="medium"
|
||||||
|
.flow=${flow}
|
||||||
|
@click=${this._flowInProgressPicked}
|
||||||
|
hasMeta
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
slot="graphic"
|
||||||
|
loading="lazy"
|
||||||
|
src=${brandsUrl({
|
||||||
|
domain: flow.handler,
|
||||||
|
type: "icon",
|
||||||
|
useFallback: true,
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
})}
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
>${localizeConfigFlowTitle(this.hass.localize, flow)}</span
|
||||||
|
>
|
||||||
|
<ha-icon-next slot="meta"></ha-icon-next>
|
||||||
|
</mwc-list-item>`
|
||||||
|
)}`
|
||||||
|
: ""}
|
||||||
|
${this.integration?.iot_standards
|
||||||
|
? this.integration.iot_standards.map((standard) => {
|
||||||
|
const domain: string = standardToDomain[standard] || standard;
|
||||||
|
return html`<mwc-list-item
|
||||||
|
graphic="medium"
|
||||||
|
.domain=${domain}
|
||||||
|
@click=${this._standardPicked}
|
||||||
|
hasMeta
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
slot="graphic"
|
||||||
|
loading="lazy"
|
||||||
|
src=${brandsUrl({
|
||||||
|
domain,
|
||||||
|
type: "icon",
|
||||||
|
useFallback: true,
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
})}
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
>${this.hass.localize(
|
||||||
|
`ui.panel.config.integrations.add_${domain}_device`
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
<ha-icon-next slot="meta"></ha-icon-next>
|
||||||
|
</mwc-list-item>`;
|
||||||
|
})
|
||||||
|
: ""}
|
||||||
|
${this.integration?.integrations
|
||||||
|
? Object.entries(this.integration.integrations).map(
|
||||||
|
([dom, val]) => html`<ha-integration-list-item
|
||||||
|
.hass=${this.hass}
|
||||||
|
.domain=${dom}
|
||||||
|
.integration=${{
|
||||||
|
...val,
|
||||||
|
domain: dom,
|
||||||
|
name: val.name || domainToName(this.hass.localize, dom),
|
||||||
|
is_built_in: val.is_built_in !== false,
|
||||||
|
cloud: val.iot_class?.startsWith("cloud_"),
|
||||||
|
}}
|
||||||
|
@click=${this._integrationPicked}
|
||||||
|
>
|
||||||
|
</ha-integration-list-item>`
|
||||||
|
)
|
||||||
|
: ""}
|
||||||
|
${["zha", "zwave_js"].includes(this.domain)
|
||||||
|
? html`<mwc-list-item
|
||||||
|
graphic="medium"
|
||||||
|
.domain=${this.domain}
|
||||||
|
@click=${this._standardPicked}
|
||||||
|
hasMeta
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
slot="graphic"
|
||||||
|
loading="lazy"
|
||||||
|
src=${brandsUrl({
|
||||||
|
domain: this.domain,
|
||||||
|
type: "icon",
|
||||||
|
useFallback: true,
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
})}
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
>${this.hass.localize(
|
||||||
|
`ui.panel.config.integrations.add_${this.domain}_device`
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
<ha-icon-next slot="meta"></ha-icon-next>
|
||||||
|
</mwc-list-item>`
|
||||||
|
: ""}
|
||||||
|
${this.integration?.config_flow
|
||||||
|
? html`${this.flowsInProgress?.length
|
||||||
|
? html`<mwc-list-item
|
||||||
|
.domain=${this.domain}
|
||||||
|
@click=${this._integrationPicked}
|
||||||
|
hasMeta
|
||||||
|
>
|
||||||
|
Setup another instance of
|
||||||
|
${this.integration.name ||
|
||||||
|
domainToName(this.hass.localize, this.domain)}
|
||||||
|
<ha-icon-next slot="meta"></ha-icon-next>
|
||||||
|
</mwc-list-item>`
|
||||||
|
: html`<ha-integration-list-item
|
||||||
|
.hass=${this.hass}
|
||||||
|
.domain=${this.domain}
|
||||||
|
.integration=${{
|
||||||
|
...this.integration,
|
||||||
|
domain: this.domain,
|
||||||
|
name:
|
||||||
|
this.integration.name ||
|
||||||
|
domainToName(this.hass.localize, this.domain),
|
||||||
|
is_built_in: this.integration.is_built_in !== false,
|
||||||
|
cloud: this.integration.iot_class?.startsWith("cloud_"),
|
||||||
|
}}
|
||||||
|
@click=${this._integrationPicked}
|
||||||
|
>
|
||||||
|
</ha-integration-list-item>`}`
|
||||||
|
: ""}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _integrationPicked(ev) {
|
||||||
|
const domain = ev.currentTarget.domain;
|
||||||
|
const root = this.getRootNode();
|
||||||
|
showConfigFlowDialog(
|
||||||
|
root instanceof ShadowRoot ? (root.host as HTMLElement) : this,
|
||||||
|
{
|
||||||
|
startFlowHandler: domain,
|
||||||
|
showAdvanced: this.hass.userData?.showAdvanced,
|
||||||
|
manifest: await fetchIntegrationManifest(this.hass, domain),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
fireEvent(this, "close-dialog");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _flowInProgressPicked(ev) {
|
||||||
|
const flow: DataEntryFlowProgress = ev.currentTarget.flow;
|
||||||
|
const root = this.getRootNode();
|
||||||
|
showConfigFlowDialog(
|
||||||
|
root instanceof ShadowRoot ? (root.host as HTMLElement) : this,
|
||||||
|
{
|
||||||
|
continueFlowId: flow.flow_id,
|
||||||
|
showAdvanced: this.hass.userData?.showAdvanced,
|
||||||
|
manifest: await fetchIntegrationManifest(this.hass, flow.handler),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
fireEvent(this, "close-dialog");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _standardPicked(ev) {
|
||||||
|
const domain = ev.currentTarget.domain;
|
||||||
|
const root = this.getRootNode();
|
||||||
|
fireEvent(this, "close-dialog");
|
||||||
|
protocolIntegrationPicked(
|
||||||
|
root instanceof ShadowRoot ? (root.host as HTMLElement) : this,
|
||||||
|
this.hass,
|
||||||
|
domain
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = [
|
||||||
|
haStyle,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
margin: 0 24px;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-domain-integrations": HaDomainIntegrations;
|
||||||
|
}
|
||||||
|
}
|
151
src/panels/config/integrations/ha-integration-list-item.ts
Normal file
151
src/panels/config/integrations/ha-integration-list-item.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import {
|
||||||
|
GraphicType,
|
||||||
|
ListItemBase,
|
||||||
|
} from "@material/mwc-list/mwc-list-item-base";
|
||||||
|
import { styles } from "@material/mwc-list/mwc-list-item.css";
|
||||||
|
import { mdiCloudOutline, mdiCodeBraces, mdiPackageVariant } from "@mdi/js";
|
||||||
|
import { css, CSSResultGroup, html } from "lit";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { domainToName } from "../../../data/integration";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { brandsUrl } from "../../../util/brands-url";
|
||||||
|
import { IntegrationListItem } from "./dialog-add-integration";
|
||||||
|
|
||||||
|
@customElement("ha-integration-list-item")
|
||||||
|
export class HaIntegrationListItem extends ListItemBase {
|
||||||
|
public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public integration?: IntegrationListItem;
|
||||||
|
|
||||||
|
@property({ type: String, reflect: true }) graphic: GraphicType = "medium";
|
||||||
|
|
||||||
|
@property({ type: Boolean }) hasMeta = true;
|
||||||
|
|
||||||
|
renderSingleLine() {
|
||||||
|
if (!this.integration) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
return html`${this.integration.name ||
|
||||||
|
domainToName(this.hass.localize, this.integration.domain)}
|
||||||
|
${this.integration.is_helper ? " (helper)" : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderGraphic() {
|
||||||
|
if (!this.integration) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
const graphicClasses = {
|
||||||
|
multi: this.multipleGraphics,
|
||||||
|
};
|
||||||
|
|
||||||
|
return html` <span
|
||||||
|
class="mdc-deprecated-list-item__graphic material-icons ${classMap(
|
||||||
|
graphicClasses
|
||||||
|
)}"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
loading="lazy"
|
||||||
|
src=${brandsUrl({
|
||||||
|
domain: this.integration.domain,
|
||||||
|
type: "icon",
|
||||||
|
useFallback: true,
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
})}
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderMeta() {
|
||||||
|
if (!this.integration) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
return html`<span class="mdc-deprecated-list-item__meta material-icons">
|
||||||
|
${!this.integration.config_flow &&
|
||||||
|
!this.integration.integrations &&
|
||||||
|
!this.integration.iot_standards
|
||||||
|
? html`<span
|
||||||
|
><ha-svg-icon .path=${mdiCodeBraces}></ha-svg-icon
|
||||||
|
><paper-tooltip animation-delay="0" position="left"
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.yaml_only"
|
||||||
|
)}</paper-tooltip
|
||||||
|
></span
|
||||||
|
>`
|
||||||
|
: ""}
|
||||||
|
${this.integration.cloud
|
||||||
|
? html`<span
|
||||||
|
><ha-svg-icon .path=${mdiCloudOutline}></ha-svg-icon
|
||||||
|
><paper-tooltip animation-delay="0" position="left"
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.depends_on_cloud"
|
||||||
|
)}</paper-tooltip
|
||||||
|
></span
|
||||||
|
>`
|
||||||
|
: ""}
|
||||||
|
${!this.integration.is_built_in
|
||||||
|
? html`<span
|
||||||
|
><ha-svg-icon .path=${mdiPackageVariant}></ha-svg-icon
|
||||||
|
><paper-tooltip animation-delay="0" position="left"
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.provided_by_custom_integration"
|
||||||
|
)}</paper-tooltip
|
||||||
|
></span
|
||||||
|
>`
|
||||||
|
: ""}
|
||||||
|
<ha-icon-next></ha-icon-next>
|
||||||
|
</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
styles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
padding-left: var(--mdc-list-side-padding, 20px);
|
||||||
|
padding-right: var(--mdc-list-side-padding, 20px);
|
||||||
|
}
|
||||||
|
:host([graphic="avatar"]:not([twoLine])),
|
||||||
|
:host([graphic="icon"]:not([twoLine])) {
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
span.material-icons:first-of-type {
|
||||||
|
margin-inline-start: 0px !important;
|
||||||
|
margin-inline-end: var(
|
||||||
|
--mdc-list-item-graphic-margin,
|
||||||
|
16px
|
||||||
|
) !important;
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
span.material-icons:last-of-type {
|
||||||
|
margin-inline-start: auto !important;
|
||||||
|
margin-inline-end: 0px !important;
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.mdc-deprecated-list-item__meta {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.mdc-deprecated-list-item__meta > * {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.mdc-deprecated-list-item__meta > *:last-child {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
ha-icon-next {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-integration-list-item": HaIntegrationListItem;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
|
||||||
|
export const showAddIntegrationDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
dialogParams?: any
|
||||||
|
): void => {
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "dialog-add-integration",
|
||||||
|
dialogImport: () => import("./dialog-add-integration"),
|
||||||
|
dialogParams: dialogParams,
|
||||||
|
});
|
||||||
|
};
|
@ -2835,7 +2835,7 @@
|
|||||||
"discovered": "Discovered",
|
"discovered": "Discovered",
|
||||||
"attention": "Attention required",
|
"attention": "Attention required",
|
||||||
"configured": "Configured",
|
"configured": "Configured",
|
||||||
"new": "Set up a new integration",
|
"new": "Select brand",
|
||||||
"confirm_new": "Do you want to set up {integration}?",
|
"confirm_new": "Do you want to set up {integration}?",
|
||||||
"add_integration": "Add integration",
|
"add_integration": "Add integration",
|
||||||
"no_integrations": "Seems like you don't have any integrations configured yet. Click on the button below to add your first integration!",
|
"no_integrations": "Seems like you don't have any integrations configured yet. Click on the button below to add your first integration!",
|
||||||
@ -2852,6 +2852,7 @@
|
|||||||
"rename_dialog": "Edit the name of this config entry",
|
"rename_dialog": "Edit the name of this config entry",
|
||||||
"rename_input_label": "Entry name",
|
"rename_input_label": "Entry name",
|
||||||
"search": "Search integrations",
|
"search": "Search integrations",
|
||||||
|
"search_brand": "Search for a brand name",
|
||||||
"add_zwave_js_device": "Add Z-Wave device",
|
"add_zwave_js_device": "Add Z-Wave device",
|
||||||
"add_zha_device": "Add Zigbee device",
|
"add_zha_device": "Add Zigbee device",
|
||||||
"disable": {
|
"disable": {
|
||||||
@ -2922,6 +2923,7 @@
|
|||||||
},
|
},
|
||||||
"provided_by_custom_integration": "Provided by a custom integration",
|
"provided_by_custom_integration": "Provided by a custom integration",
|
||||||
"depends_on_cloud": "Depends on the cloud",
|
"depends_on_cloud": "Depends on the cloud",
|
||||||
|
"yaml_only": "Can not be setup from the UI",
|
||||||
"disabled_polling": "Automatic polling for updated data disabled",
|
"disabled_polling": "Automatic polling for updated data disabled",
|
||||||
"state": {
|
"state": {
|
||||||
"loaded": "Loaded",
|
"loaded": "Loaded",
|
||||||
@ -2943,6 +2945,9 @@
|
|||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"found_following_devices": "We found the following devices",
|
"found_following_devices": "We found the following devices",
|
||||||
|
"yaml_only_title": "This device can not be added from the UI",
|
||||||
|
"yaml_only_text": "You can add this device by adding it to your `configuration.yaml`. See the {link} for more information.",
|
||||||
|
"documentation": "documentation",
|
||||||
"no_config_flow": "This integration does not support configuration via the UI. If you followed this link from the Home Assistant website, make sure you run the latest version of Home Assistant.",
|
"no_config_flow": "This integration does not support configuration via the UI. If you followed this link from the Home Assistant website, make sure you run the latest version of Home Assistant.",
|
||||||
"not_all_required_fields": "Not all required fields are filled in.",
|
"not_all_required_fields": "Not all required fields are filled in.",
|
||||||
"error_saving_area": "Error saving area: {error}",
|
"error_saving_area": "Error saving area: {error}",
|
||||||
@ -2963,6 +2968,7 @@
|
|||||||
"error": "Error",
|
"error": "Error",
|
||||||
"could_not_load": "Config flow could not be loaded",
|
"could_not_load": "Config flow could not be loaded",
|
||||||
"not_loaded": "The integration could not be loaded, try to restart Home Assistant.",
|
"not_loaded": "The integration could not be loaded, try to restart Home Assistant.",
|
||||||
|
"missing_credentials_title": "Add application credentials?",
|
||||||
"missing_credentials": "Setting up {integration} requires configuring application credentials. Do you want to do that now?",
|
"missing_credentials": "Setting up {integration} requires configuring application credentials. Do you want to do that now?",
|
||||||
"supported_brand_flow": "Support for {supported_brand} devices is provided by {flow_domain_name}. Do you want to continue?",
|
"supported_brand_flow": "Support for {supported_brand} devices is provided by {flow_domain_name}. Do you want to continue?",
|
||||||
"missing_zwave_zigbee": "To add a {integration} device, you first need {supported_hardware_link} and the {integration} integration set up. If you already have the hardware then you can proceed with the setup of {integration}.",
|
"missing_zwave_zigbee": "To add a {integration} device, you first need {supported_hardware_link} and the {integration} integration set up. If you already have the hardware then you can proceed with the setup of {integration}.",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user