diff --git a/public/static/images/z-wave-add-node/long-range.svg b/public/static/images/z-wave-add-node/long-range.svg
new file mode 100644
index 0000000000..48deddc513
--- /dev/null
+++ b/public/static/images/z-wave-add-node/long-range.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/static/images/z-wave-add-node/long-range_dark.svg b/public/static/images/z-wave-add-node/long-range_dark.svg
new file mode 100644
index 0000000000..ccc1ee8152
--- /dev/null
+++ b/public/static/images/z-wave-add-node/long-range_dark.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/static/images/z-wave-add-node/mesh.svg b/public/static/images/z-wave-add-node/mesh.svg
new file mode 100644
index 0000000000..92a03c444a
--- /dev/null
+++ b/public/static/images/z-wave-add-node/mesh.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/static/images/z-wave-add-node/mesh_dark.svg b/public/static/images/z-wave-add-node/mesh_dark.svg
new file mode 100644
index 0000000000..1824489e47
--- /dev/null
+++ b/public/static/images/z-wave-add-node/mesh_dark.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/common/integrations/protocolIntegrationPicked.ts b/src/common/integrations/protocolIntegrationPicked.ts
index 1f7ea0b3b8..6b4604ac8f 100644
--- a/src/common/integrations/protocolIntegrationPicked.ts
+++ b/src/common/integrations/protocolIntegrationPicked.ts
@@ -5,7 +5,7 @@ import { getIntegrationDescriptions } from "../../data/integrations";
import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow";
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
import { showMatterAddDeviceDialog } from "../../panels/config/integrations/integration-panels/matter/show-dialog-add-matter-device";
-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/add-node/show-dialog-zwave_js-add-node";
import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import { isComponentLoaded } from "../config/is_component_loaded";
diff --git a/src/components/ha-qr-scanner.ts b/src/components/ha-qr-scanner.ts
index 1aca4f3982..94a905596e 100644
--- a/src/components/ha-qr-scanner.ts
+++ b/src/components/ha-qr-scanner.ts
@@ -1,7 +1,5 @@
-import "@material/mwc-button/mwc-button";
import { mdiCamera } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
-import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
// The BarcodeDetector Web API is not yet supported in all browsers,
@@ -12,12 +10,13 @@ import { prepareZXingModule } from "barcode-detector";
import type QrScanner from "qr-scanner";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
-import type { LocalizeFunc } from "../common/translations/localize";
import { addExternalBarCodeListener } from "../external_app/external_app_entrypoint";
import type { HomeAssistant } from "../types";
import "./ha-alert";
+import "./ha-button";
import "./ha-button-menu";
import "./ha-list-item";
+import "./ha-spinner";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@@ -36,18 +35,22 @@ prepareZXingModule({
class HaQrScanner extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
- @property({ attribute: false }) public localize!: LocalizeFunc;
-
@property() public description?: string;
@property({ attribute: "alternative_option_label" })
public alternativeOptionLabel?: string;
- @property() public error?: string;
+ @property({ attribute: false }) public validate?: (
+ value: string
+ ) => string | undefined;
@state() private _cameras?: QrScanner.Camera[];
- @state() private _manual = false;
+ @state() private _loading = true;
+
+ @state() private _error?: string;
+
+ @state() private _warning?: string;
private _qrScanner?: QrScanner;
@@ -88,29 +91,40 @@ class HaQrScanner extends LitElement {
this._loadQrScanner();
}
- protected updated(changedProps: PropertyValues) {
- if (changedProps.has("error") && this.error) {
- alert(`error: ${this.error}`);
- this._notifyExternalScanner(this.error);
- }
- }
-
protected render() {
- if (this._nativeBarcodeScanner && !this._manual) {
+ if (this._nativeBarcodeScanner) {
return nothing;
}
- return html`${this.error
- ? html`${this.error} `
- : ""}
- ${navigator.mediaDevices && !this._manual
+ return html`${this._error || this._warning
+ ? html`
+ ${this._error || this._warning}
+ ${this._error
+ ? html`
+ ${this.hass.localize("ui.components.qr-scanner.retry")}
+ `
+ : nothing}
+ `
+ : nothing}
+ ${navigator.mediaDevices
? html`
- ${this._cameras && this._cameras.length > 1
+ ${this._loading
+ ? html`
+
+
`
+ : nothing}
+ ${!this._loading &&
+ !this._error &&
+ this._cameras &&
+ this._cameras.length > 1
? html`
`
: nothing}
`
- : html`${this._manual
- ? nothing
- : html`
- ${!window.isSecureContext
- ? this.localize(
- "ui.components.qr-scanner.only_https_supported"
- )
- : this.localize("ui.components.qr-scanner.not_supported")}
- `}
- ${this.localize("ui.components.qr-scanner.manual_input")}
+ : html`
+ ${!window.isSecureContext
+ ? this.hass.localize(
+ "ui.components.qr-scanner.only_https_supported"
+ )
+ : this.hass.localize("ui.components.qr-scanner.not_supported")}
+
+ ${this.hass.localize("ui.components.qr-scanner.manual_input")}
-
- ${this.localize("ui.common.submit")}
-
+
+ ${this.hass.localize("ui.common.submit")}
+
`}`;
}
@@ -165,7 +179,9 @@ class HaQrScanner extends LitElement {
// eslint-disable-next-line @typescript-eslint/naming-convention
const QrScanner = (await import("qr-scanner")).default;
if (!(await QrScanner.hasCamera())) {
- this._reportError("No camera found");
+ this._reportError(
+ this.hass.localize("ui.components.qr-scanner.no_camera_found")
+ );
return;
}
QrScanner.WORKER_PATH = "/static/js/qr-scanner-worker.min.js";
@@ -181,6 +197,7 @@ class HaQrScanner extends LitElement {
canvas.style.display = "block";
try {
await this._qrScanner.start();
+ this._loading = false;
} catch (err: any) {
this._reportError(err);
}
@@ -193,8 +210,8 @@ class HaQrScanner extends LitElement {
private _qrCodeError = (err: any) => {
if (err.endsWith("No QR code found")) {
this._qrNotFoundCount++;
- if (this._qrNotFoundCount === 250) {
- this._reportError(err);
+ if (this._qrNotFoundCount >= 250) {
+ this._reportWarning(err);
}
return;
}
@@ -204,7 +221,17 @@ class HaQrScanner extends LitElement {
};
private _qrCodeScanned = (qrCodeString: string): void => {
+ this._warning = undefined;
this._qrNotFoundCount = 0;
+ if (this.validate) {
+ const validationMessage = this.validate(qrCodeString);
+
+ if (validationMessage) {
+ this._reportWarning(validationMessage);
+ return;
+ }
+ }
+
fireEvent(this, "qr-code-scanned", { value: qrCodeString });
};
@@ -234,7 +261,10 @@ class HaQrScanner extends LitElement {
if (msg.command === "bar_code/scan_result") {
if (msg.payload.format !== "qr_code") {
this._notifyExternalScanner(
- `Wrong barcode scanned! ${msg.payload.format}: ${msg.payload.rawValue}, we need a QR code.`
+ this.hass.localize("ui.components.qr-scanner.wrong_code", {
+ format: msg.payload.format,
+ rawValue: msg.payload.rawValue,
+ })
);
} else {
this._qrCodeScanned(msg.payload.rawValue);
@@ -244,7 +274,7 @@ class HaQrScanner extends LitElement {
if (msg.payload.reason === "canceled") {
fireEvent(this, "qr-code-closed");
} else {
- this._manual = true;
+ fireEvent(this, "qr-code-more-options");
}
}
return true;
@@ -252,10 +282,17 @@ class HaQrScanner extends LitElement {
this.hass.auth.external!.fireMessage({
type: "bar_code/scan",
payload: {
- title: this.title || "Scan QR code",
- description: this.description || "Scan a barcode.",
+ title:
+ this.title ||
+ this.hass.localize("ui.components.qr-scanner.app.title"),
+ description:
+ this.description ||
+ this.hass.localize("ui.components.qr-scanner.app.description"),
alternative_option_label:
- this.alternativeOptionLabel || "Click to manually enter the barcode",
+ this.alternativeOptionLabel ||
+ this.hass.localize(
+ "ui.components.qr-scanner.app.alternativeOptionLabel"
+ ),
},
});
}
@@ -269,25 +306,55 @@ class HaQrScanner extends LitElement {
}
private _notifyExternalScanner(message: string) {
- if (!this.hass.auth.external) {
+ if (!this._nativeBarcodeScanner) {
return;
}
- this.hass.auth.external.fireMessage({
+ this.hass.auth.external!.fireMessage({
type: "bar_code/notify",
payload: {
message,
},
});
- this.error = undefined;
+ this._warning = undefined;
+ this._error = undefined;
}
private _reportError(message: string) {
- fireEvent(this, "qr-code-error", { message });
+ const canvas = this._qrScanner?.$canvas;
+ if (canvas) {
+ canvas.style.display = "none";
+ }
+ this._error = message;
+ }
+
+ private _reportWarning(message: string) {
+ if (this._nativeBarcodeScanner) {
+ this._notifyExternalScanner(message);
+ } else {
+ this._warning = message;
+ }
+ }
+
+ private async _retry() {
+ if (this._qrScanner) {
+ this._loading = true;
+ this._error = undefined;
+ this._warning = undefined;
+ const canvas = this._qrScanner.$canvas;
+ canvas.style.display = "block";
+ this._qrNotFoundCount = 0;
+ await this._qrScanner.start();
+ this._loading = false;
+ }
}
static styles = css`
+ :root {
+ position: relative;
+ }
canvas {
width: 100%;
+ border-radius: 16px;
}
#canvas-container {
position: relative;
@@ -312,6 +379,24 @@ class HaQrScanner extends LitElement {
margin-inline-end: 8px;
margin-inline-start: initial;
}
+ .loading {
+ display: flex;
+ position: absolute;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ width: 100%;
+ }
+ ha-alert {
+ display: block;
+ }
+ ha-alert.warning {
+ position: absolute;
+ z-index: 1;
+ background-color: var(--primary-background-color);
+ top: 0;
+ width: calc(100% - 48px);
+ }
`;
}
@@ -319,8 +404,8 @@ declare global {
// for fire event
interface HASSDomEvents {
"qr-code-scanned": { value: string };
- "qr-code-error": { message: string };
"qr-code-closed": undefined;
+ "qr-code-more-options": undefined;
}
interface HTMLElementTagNameMap {
diff --git a/src/data/zwave_js.ts b/src/data/zwave_js.ts
index 99f0866bb9..b0e5ae8cbe 100644
--- a/src/data/zwave_js.ts
+++ b/src/data/zwave_js.ts
@@ -80,7 +80,7 @@ enum QRCodeVersion {
SmartStart = 1,
}
-enum Protocols {
+export enum Protocols {
ZWave = 0,
ZWaveLongRange = 1,
}
@@ -151,12 +151,35 @@ export interface QRProvisioningInformation {
maxInclusionRequestInterval?: number | undefined;
uuid?: string | undefined;
supportedProtocols?: Protocols[] | undefined;
+ status?: ProvisioningEntryStatus;
}
export interface PlannedProvisioningEntry {
/** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */
dsk: string;
securityClasses: SecurityClass[];
+ status?: ProvisioningEntryStatus;
+}
+
+export enum ProvisioningEntryStatus {
+ Active = 0,
+ Inactive = 1,
+}
+
+export interface DeviceConfig {
+ filename: string;
+ manufacturer: string;
+ manufacturerId: number;
+ label: string;
+ description: string;
+ devices: {
+ productType: number;
+ productId: number;
+ }[];
+ firmwareVersion: {
+ min: string;
+ max: string;
+ };
}
export const MINIMUM_QR_STRING_LENGTH = 52;
@@ -195,6 +218,7 @@ export interface ZWaveJSController {
is_rebuilding_routes: boolean;
inclusion_state: InclusionState;
nodes: ZWaveJSNodeStatus[];
+ supports_long_range: boolean;
}
export interface ZWaveJSNodeStatus {
@@ -555,7 +579,7 @@ export const zwaveTryParseDskFromQrCode = (
export const zwaveValidateDskAndEnterPin = (
hass: HomeAssistant,
entry_id: string,
- pin: string
+ pin: string | false
) =>
hass.callWS({
type: "zwave_js/validate_dsk_and_enter_pin",
@@ -585,19 +609,38 @@ export const zwaveParseQrCode = (
qr_code_string,
});
+export const lookupZwaveDevice = (
+ hass: HomeAssistant,
+ entry_id: string,
+ manufacturerId: number,
+ productType: number,
+ productId: number,
+ applicationVersion?: string
+): Promise =>
+ hass.callWS({
+ type: "zwave_js/lookup_device",
+ entry_id,
+ manufacturerId,
+ productType,
+ productId,
+ applicationVersion,
+ });
+
export const provisionZwaveSmartStartNode = (
hass: HomeAssistant,
entry_id: string,
qr_provisioning_information?: QRProvisioningInformation,
- qr_code_string?: string,
- planned_provisioning_entry?: PlannedProvisioningEntry
-): Promise =>
+ protocol?: Protocols,
+ device_name?: string,
+ area_id?: string
+): Promise =>
hass.callWS({
type: "zwave_js/provision_smart_start_node",
entry_id,
- qr_code_string,
qr_provisioning_information,
- planned_provisioning_entry,
+ protocol,
+ device_name,
+ area_id,
});
export const unprovisionZwaveSmartStartNode = (
@@ -613,6 +656,16 @@ export const unprovisionZwaveSmartStartNode = (
node_id,
});
+export const subscribeNewDevices = (
+ hass: HomeAssistant,
+ entry_id: string,
+ callbackFunction: (message: any) => void
+): Promise =>
+ hass.connection.subscribeMessage((message) => callbackFunction(message), {
+ type: "zwave_js/subscribe_new_devices",
+ entry_id: entry_id,
+ });
+
export const fetchZwaveNodeStatus = (
hass: HomeAssistant,
device_id: string
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/add-node/data.ts b/src/panels/config/integrations/integration-panels/zwave_js/add-node/data.ts
new file mode 100644
index 0000000000..7a2d6a145c
--- /dev/null
+++ b/src/panels/config/integrations/integration-panels/zwave_js/add-node/data.ts
@@ -0,0 +1,54 @@
+import type { QRProvisioningInformation } from "../../../../../../data/zwave_js";
+
+export const backButtonStages: Partial[] = [
+ "qr_scan",
+ "select_other_method",
+ "qr_code_input",
+ "choose_security_strategy",
+ "configure_device",
+];
+
+export const closeButtonStages: Partial[] = [
+ "select_method",
+ "search_devices",
+ "search_smart_start_device",
+ "search_s2_device",
+ "failed",
+ "interviewing",
+ "validate_dsk_enter_pin",
+ "added_insecure",
+ "grant_security_classes",
+ "rename_device",
+];
+
+export type ZWaveJSAddNodeStage =
+ | "loading"
+ | "qr_scan"
+ | "select_method"
+ | "select_other_method"
+ | "qr_code_input"
+ | "search_devices"
+ | "search_smart_start_device"
+ | "search_s2_device"
+ | "choose_security_strategy"
+ | "configure_device"
+ | "interviewing"
+ | "failed"
+ | "timed_out"
+ | "added_insecure"
+ | "validate_dsk_enter_pin"
+ | "grant_security_classes"
+ | "waiting_for_device"
+ | "rename_device";
+
+export interface ZWaveJSAddNodeSmartStartOptions {
+ name: string;
+ area?: string;
+ network_type?: string;
+}
+
+export interface ZWaveJSAddNodeDevice {
+ id?: string;
+ name: string;
+ provisioningInfo?: QRProvisioningInformation;
+}
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/add-node/dialog-zwave_js-add-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/add-node/dialog-zwave_js-add-node.ts
new file mode 100644
index 0000000000..203f5b9cc6
--- /dev/null
+++ b/src/panels/config/integrations/integration-panels/zwave_js/add-node/dialog-zwave_js-add-node.ts
@@ -0,0 +1,1136 @@
+import { mdiChevronLeft, mdiClose } from "@mdi/js";
+import "@shoelace-style/shoelace/dist/components/animation/animation";
+import type { UnsubscribeFunc } from "home-assistant-js-websocket";
+import type { CSSResultGroup, TemplateResult } from "lit";
+import { css, html, LitElement, nothing } from "lit";
+import { customElement, property, query, state } from "lit/decorators";
+import memoizeOne from "memoize-one";
+import { fireEvent } from "../../../../../../common/dom/fire_event";
+import type { HaDialog } from "../../../../../../components/ha-dialog";
+import { updateDeviceRegistryEntry } from "../../../../../../data/device_registry";
+import type {
+ QRProvisioningInformation,
+ RequestedGrant,
+ SecurityClass,
+} from "../../../../../../data/zwave_js";
+import {
+ cancelSecureBootstrapS2,
+ fetchZwaveNetworkStatus,
+ InclusionStrategy,
+ lookupZwaveDevice,
+ MINIMUM_QR_STRING_LENGTH,
+ Protocols,
+ ProvisioningEntryStatus,
+ provisionZwaveSmartStartNode,
+ stopZwaveInclusion,
+ subscribeAddZwaveNode,
+ subscribeNewDevices,
+ ZWaveFeature,
+ zwaveGrantSecurityClasses,
+ zwaveParseQrCode,
+ zwaveSupportsFeature,
+ zwaveTryParseDskFromQrCode,
+ zwaveValidateDskAndEnterPin,
+} from "../../../../../../data/zwave_js";
+import type { HomeAssistant } from "../../../../../../types";
+import {
+ backButtonStages,
+ closeButtonStages,
+ type ZWaveJSAddNodeDevice,
+ type ZWaveJSAddNodeSmartStartOptions,
+ type ZWaveJSAddNodeStage,
+} from "./data";
+import type { ZWaveJSAddNodeDialogParams } from "./show-dialog-zwave_js-add-node";
+
+import "../../../../../../components/ha-button";
+import "../../../../../../components/ha-dialog";
+import "../../../../../../components/ha-dialog-header";
+import "../../../../../../components/ha-fade-in";
+import "../../../../../../components/ha-icon-button";
+import "../../../../../../components/ha-qr-scanner";
+import "../../../../../../components/ha-spinner";
+
+import { computeStateName } from "../../../../../../common/entity/compute_state_name";
+import { navigate } from "../../../../../../common/navigate";
+import { slugify } from "../../../../../../common/string/slugify";
+import type { EntityRegistryEntry } from "../../../../../../data/entity_registry";
+import {
+ subscribeEntityRegistry,
+ updateEntityRegistryEntry,
+} from "../../../../../../data/entity_registry";
+import { SubscribeMixin } from "../../../../../../mixins/subscribe-mixin";
+import "./zwave-js-add-node-added-insecure";
+import "./zwave-js-add-node-code-input";
+import "./zwave-js-add-node-configure-device";
+import "./zwave-js-add-node-failed";
+import "./zwave-js-add-node-grant-security-classes";
+import "./zwave-js-add-node-loading";
+import "./zwave-js-add-node-searching-devices";
+import "./zwave-js-add-node-select-method";
+import "./zwave-js-add-node-select-security-strategy";
+
+const INCLUSION_TIMEOUT_MINUTES = 5;
+
+@customElement("dialog-zwave_js-add-node")
+class DialogZWaveJSAddNode extends SubscribeMixin(LitElement) {
+ // #region variables
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @state() private _open = false;
+
+ @state() private _step?: ZWaveJSAddNodeStage;
+
+ @state() private _entryId?: string;
+
+ @state() private _controllerSupportsLongRange? = false;
+
+ @state() private _supportsSmartStart?: boolean;
+
+ @state() private _dsk?: string;
+
+ @state() private _dskPin = "";
+
+ @state() private _error?: string;
+
+ @state() private _inclusionStrategy?: InclusionStrategy;
+
+ @state() private _lowSecurity = false;
+
+ @state() private _lowSecurityReason?: number;
+
+ @state() private _device?: ZWaveJSAddNodeDevice;
+
+ @state() private _deviceOptions?: ZWaveJSAddNodeSmartStartOptions;
+
+ @state() private _requestedGrant?: RequestedGrant;
+
+ @state() private _securityClasses: SecurityClass[] = [];
+
+ @state() private _codeInput = "";
+
+ @query("ha-dialog") private _dialog?: HaDialog;
+
+ private _qrProcessing = false;
+
+ private _addNodeTimeoutHandle?: number;
+
+ private _onStop?: () => void;
+
+ private _subscribed?: Promise;
+
+ private _newDeviceSubscription?: Promise;
+
+ @state() private _entities: EntityRegistryEntry[] = [];
+
+ // #endregion
+
+ public hassSubscribe(): UnsubscribeFunc[] {
+ return [
+ subscribeEntityRegistry(this.hass.connection, (entities) => {
+ this._entities = entities;
+ }),
+ ];
+ }
+
+ protected render() {
+ if (!this._entryId) {
+ return nothing;
+ }
+
+ // Prevent accidentally closing the dialog in certain stages
+ const preventClose = !!this._step && this._shouldPreventClose(this._step);
+
+ const { headerText, headerHtml } = this._renderHeader();
+
+ return html`
+
+ ${headerHtml}
+ ${this._renderStep()}
+
+ `;
+ }
+
+ private _renderHeader(): { headerText: string; headerHtml: TemplateResult } {
+ let headerText = this.hass.localize(
+ `ui.panel.config.zwave_js.add_node.title`
+ );
+
+ if (this._step === "loading") {
+ return {
+ headerText,
+ headerHtml: html`
+
+ ${headerText}
+
+ `,
+ };
+ }
+
+ let icon: string | undefined;
+ if (
+ (this._step && closeButtonStages.includes(this._step)) ||
+ (this._step === "search_devices" && !this._supportsSmartStart) ||
+ (this._step === "configure_device" && this._device?.id)
+ ) {
+ icon = mdiClose;
+ } else if (
+ (this._step && backButtonStages.includes(this._step)) ||
+ (this._step === "search_devices" && this._supportsSmartStart)
+ ) {
+ icon = mdiChevronLeft;
+ }
+
+ let titleTranslationKey = "title";
+
+ switch (this._step) {
+ case "qr_scan":
+ titleTranslationKey = "qr.scan_code";
+ break;
+ case "qr_code_input":
+ titleTranslationKey = "qr.manual.title";
+ break;
+ case "select_other_method":
+ titleTranslationKey = "qr.other_add_options";
+ break;
+ case "search_devices":
+ titleTranslationKey = "searching_devices";
+ break;
+ case "search_smart_start_device":
+ titleTranslationKey = "specific_device.title";
+ break;
+ case "choose_security_strategy":
+ titleTranslationKey = "security_options";
+ break;
+ case "validate_dsk_enter_pin":
+ titleTranslationKey = "validate_dsk_pin.title";
+ break;
+ case "configure_device":
+ case "rename_device":
+ titleTranslationKey = "configure_device.title";
+ break;
+ case "added_insecure":
+ titleTranslationKey = "added_insecure.title";
+ break;
+ case "grant_security_classes":
+ titleTranslationKey = "grant_security_classes.title";
+ break;
+ case "failed":
+ titleTranslationKey = "add_device_failed";
+ break;
+ }
+
+ headerText = this.hass.localize(
+ `ui.panel.config.zwave_js.add_node.${titleTranslationKey}`
+ );
+
+ return {
+ headerText,
+ headerHtml: html`
+ ${icon
+ ? html` `
+ : nothing}
+ ${headerText}
+ `,
+ };
+ }
+
+ private _renderStep() {
+ if (["select_method", "select_other_method"].includes(this._step!)) {
+ return html` `;
+ }
+
+ if (this._step === "qr_scan") {
+ return html`
+
+
+
+ `;
+ }
+
+ if (this._step === "qr_code_input") {
+ return html`
+
+
+ ${this.hass.localize("ui.common.next")}
+
+ `;
+ }
+
+ if (
+ this._step === "search_devices" ||
+ this._step === "search_smart_start_device" ||
+ this._step === "search_s2_device"
+ ) {
+ return html`
+
+ ${this._step === "search_smart_start_device"
+ ? html`
+
+ ${this.hass.localize("ui.common.close")}
+
+ `
+ : nothing}
+ `;
+ }
+
+ if (this._step === "choose_security_strategy") {
+ return html`
+
+ ${this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.select_method.search_device"
+ )}
+ `;
+ }
+
+ if (this._step === "configure_device") {
+ return html`
+
+ ${this.hass.localize(
+ this._device?.id
+ ? "ui.common.save"
+ : "ui.panel.config.zwave_js.add_node.configure_device.add_device"
+ )}
+ `;
+ }
+
+ if (this._step === "validate_dsk_enter_pin") {
+ return html`
+
+
+ ${this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.configure_device.add_device"
+ )}
+
+ `;
+ }
+
+ if (
+ ["interviewing", "waiting_for_device", "rename_device"].includes(
+ this._step ?? ""
+ )
+ ) {
+ return html`
+
+ `;
+ }
+
+ if (this._step === "added_insecure") {
+ return html`
+
+
+ ${this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.added_insecure.view_device"
+ )}
+
+ `;
+ }
+
+ if (this._step === "grant_security_classes") {
+ return html`
+
+
+ ${this.hass.localize("ui.common.submit")}
+
+ `;
+ }
+
+ if (this._step === "failed") {
+ return html`
+
+ `;
+ }
+
+ return html` `;
+ }
+
+ public connectedCallback(): void {
+ super.connectedCallback();
+ window.addEventListener("beforeunload", this._onBeforeUnload);
+ }
+
+ private _onBeforeUnload = (event: BeforeUnloadEvent) => {
+ if (this._step && this._shouldPreventClose(this._step)) {
+ event.preventDefault();
+ // support for legacy browsers
+ event.returnValue = true;
+ }
+ };
+
+ private _showFirstStep() {
+ if (this._supportsSmartStart) {
+ if (this.hass.auth.external?.config.hasBarCodeScanner) {
+ this._step = "qr_scan";
+ } else {
+ this._step = "select_method";
+ this._open = true;
+ }
+ } else {
+ this._open = true;
+ this._step = "search_devices";
+ this._startInclusion();
+ }
+ }
+
+ public async showDialog(params: ZWaveJSAddNodeDialogParams): Promise {
+ if (this._step) {
+ // already started
+ return;
+ }
+ this._onStop = params?.onStop;
+ this._entryId = params.entry_id;
+ this._controllerSupportsLongRange = params.longRangeSupported;
+
+ this._step = "loading";
+
+ if (this._controllerSupportsLongRange === undefined) {
+ try {
+ const zwaveNetwork = await fetchZwaveNetworkStatus(this.hass, {
+ entry_id: this._entryId,
+ });
+ this._controllerSupportsLongRange =
+ zwaveNetwork?.controller?.supports_long_range;
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.error(err);
+ }
+ }
+
+ if (params.dsk) {
+ this._open = true;
+ this._dskPin = "";
+ this._step = "validate_dsk_enter_pin";
+ this._dsk = params.dsk;
+
+ this._startInclusion();
+ return;
+ }
+
+ this._supportsSmartStart = (
+ await zwaveSupportsFeature(
+ this.hass,
+ this._entryId!,
+ ZWaveFeature.SmartStart
+ )
+ ).supported;
+
+ if (params?.inclusionOngoing) {
+ this._open = true;
+ this._step = "search_devices";
+ this._startInclusion();
+ return;
+ }
+
+ this._showFirstStep();
+ }
+
+ /**
+ * prevent esc key, click out of dialog and close tab/browser
+ */
+ private _shouldPreventClose = memoizeOne((step: ZWaveJSAddNodeStage) =>
+ [
+ "loading",
+ "qr_scan",
+ "qr_code_input",
+ "search_smart_start_device",
+ "search_s2_device",
+ "choose_security_strategy",
+ "configure_device",
+ "interviewing",
+ "validate_dsk_enter_pin",
+ "grant_security_classes",
+ "waiting_for_device",
+ ].includes(step)
+ );
+
+ private _handleCloseOrBack() {
+ if (
+ (this._step && closeButtonStages.includes(this._step!)) ||
+ (this._step === "search_devices" && !this._supportsSmartStart)
+ ) {
+ this.closeDialog();
+ return;
+ }
+
+ switch (this._step) {
+ case "select_other_method":
+ this._step = "qr_scan";
+ break;
+ case "qr_scan":
+ this._step = "select_method";
+ break;
+ case "qr_code_input":
+ if (this.hass.auth.external?.config.hasBarCodeScanner) {
+ this._step = "select_other_method";
+ break;
+ }
+ this._step = "select_method";
+ break;
+ case "search_devices":
+ this._unsubscribe();
+ if (
+ this._supportsSmartStart &&
+ this.hass.auth.external?.config.hasBarCodeScanner
+ ) {
+ this._step = "select_other_method";
+ break;
+ } else if (this._supportsSmartStart) {
+ this._step = "select_method";
+ break;
+ }
+ break;
+ case "choose_security_strategy":
+ this._inclusionStrategy = undefined;
+ this._step = "loading";
+ this._startInclusion();
+ break;
+ case "configure_device":
+ this._showFirstStep();
+ break;
+ }
+ }
+
+ private _methodSelected(ev: CustomEvent): void {
+ const method = ev.detail.method;
+ if (method === "qr_code_webcam") {
+ this._step = "qr_scan";
+ } else if (method === "qr_code_manual") {
+ this._codeInput = "";
+ this._step = "qr_code_input";
+ } else if (method === "search_device") {
+ this._step = "loading";
+ this._startInclusion();
+ }
+ }
+
+ private _qrScanShowMoreOptions() {
+ this._open = true;
+ this._step = "select_other_method";
+ }
+
+ private _searchDevicesShowSecurityOptions() {
+ this._unsubscribe();
+ this._step = "choose_security_strategy";
+ }
+
+ private _searchDevicesWithStrategy() {
+ if (this._inclusionStrategy !== undefined) {
+ this._step = "loading";
+ this._startInclusion();
+ }
+ }
+
+ private _setSecurityStrategy(ev: CustomEvent): void {
+ this._inclusionStrategy = ev.detail.strategy;
+ }
+
+ private _startInclusion(
+ qrProvisioningInformation?: QRProvisioningInformation,
+ dsk?: string
+ ): void {
+ this._lowSecurity = false;
+
+ const s2Device = qrProvisioningInformation || dsk;
+ this._subscribed = subscribeAddZwaveNode(
+ this.hass,
+ this._entryId!,
+ (message) => {
+ switch (message.event) {
+ case "inclusion started":
+ this._step = s2Device ? "search_s2_device" : "search_devices";
+ break;
+ case "inclusion failed":
+ this._unsubscribe();
+ this._step = "failed";
+ break;
+ case "inclusion stopped":
+ // We either found a device, or it failed, either way, cancel the timeout as we are no longer searching
+ if (this._addNodeTimeoutHandle) {
+ clearTimeout(this._addNodeTimeoutHandle);
+ }
+ this._addNodeTimeoutHandle = undefined;
+ break;
+ case "node found":
+ // The user may have to enter a PIN. Until then prevent accidentally
+ // closing the dialog
+ this._step = "waiting_for_device";
+ break;
+ case "validate dsk and enter pin":
+ this._step = "validate_dsk_enter_pin";
+ this._dsk = message.dsk;
+ break;
+ case "grant security classes":
+ if (this._inclusionStrategy === undefined) {
+ zwaveGrantSecurityClasses(
+ this.hass,
+ this._entryId!,
+ message.requested_grant.securityClasses,
+ message.requested_grant.clientSideAuth
+ );
+ break;
+ }
+ this._requestedGrant = message.requested_grant;
+ this._securityClasses = message.requested_grant.securityClasses;
+ this._step = "grant_security_classes";
+ break;
+ case "device registered":
+ this._device = message.device;
+ break;
+ case "node added":
+ this._step = "interviewing";
+ this._lowSecurity = message.node.low_security;
+ this._lowSecurityReason = message.node.low_security_reason;
+ break;
+ case "interview completed":
+ this._unsubscribe();
+ this._step = "configure_device";
+ break;
+ }
+ },
+ qrProvisioningInformation,
+ undefined,
+ undefined,
+ dsk,
+ this._inclusionStrategy
+ ).catch((err) => {
+ this._error = err.message;
+ this._step = "failed";
+ return undefined;
+ });
+ this._addNodeTimeoutHandle = window.setTimeout(
+ () => {
+ this._unsubscribe();
+ this._error = this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.timeout_error",
+ { minutes: INCLUSION_TIMEOUT_MINUTES }
+ );
+ this._step = "failed";
+ },
+ INCLUSION_TIMEOUT_MINUTES * 1000 * 60
+ );
+ }
+
+ private _validateQrCode = (qrCode: string): boolean =>
+ qrCode.length >= MINIMUM_QR_STRING_LENGTH && qrCode.startsWith("90");
+
+ private _getQrCodeValidationError = (qrCode: string): string | undefined =>
+ this._validateQrCode(qrCode)
+ ? undefined
+ : this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.qr.invalid_code",
+ { code: qrCode }
+ );
+
+ private async _qrCodeScanned(ev?: CustomEvent): Promise {
+ let qrCodeString: string;
+ this._error = undefined;
+ this._open = true;
+
+ if (
+ (this._step !== "qr_scan" && this._step !== "qr_code_input") ||
+ this._qrProcessing ||
+ (this._step === "qr_scan" && !ev?.detail.value)
+ ) {
+ return;
+ }
+
+ if (this._step === "qr_code_input") {
+ if (!this._codeInput) {
+ return;
+ }
+
+ if (!this._validateQrCode(this._codeInput)) {
+ this._step = "failed";
+ this._error = this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.qr.invalid_code",
+ { code: this._codeInput }
+ );
+ return;
+ }
+
+ qrCodeString = this._codeInput;
+ } else {
+ qrCodeString = ev!.detail.value;
+ }
+
+ this._qrProcessing = true;
+ const dsk = await zwaveTryParseDskFromQrCode(
+ this.hass,
+ this._entryId!,
+ qrCodeString
+ );
+
+ if (dsk) {
+ // own screen add device inclusion
+ this._step = "loading";
+ // wait for QR scanner to be removed before resetting qr processing
+ this.updateComplete.then(() => {
+ this._qrProcessing = false;
+ });
+ this._inclusionStrategy = InclusionStrategy.Security_S2;
+ this._startInclusion(undefined, dsk);
+ return;
+ }
+
+ let provisioningInfo: QRProvisioningInformation;
+
+ try {
+ provisioningInfo = await zwaveParseQrCode(
+ this.hass,
+ this._entryId!,
+ qrCodeString
+ );
+ } catch (err: any) {
+ this._qrProcessing = false;
+ this._error = err.message;
+ this._step = "failed";
+ return;
+ }
+
+ let deviceName = "";
+ try {
+ const device = await lookupZwaveDevice(
+ this.hass,
+ this._entryId!,
+ provisioningInfo.manufacturerId,
+ provisioningInfo.productType,
+ provisioningInfo.productId,
+ provisioningInfo.applicationVersion
+ );
+
+ deviceName = device?.description ?? "";
+ } catch (_err: any) {
+ // ignore
+ // if device is not found in z-wave db set empty as default name
+ }
+
+ this._device = {
+ name: deviceName,
+ provisioningInfo,
+ };
+
+ // wait for QR scanner to be removed before resetting qr processing
+ this.updateComplete.then(() => {
+ this._qrProcessing = false;
+ });
+
+ if (provisioningInfo.version === 1) {
+ this._step = "configure_device";
+ } else if (provisioningInfo.version === 0) {
+ this._step = "loading";
+ this._inclusionStrategy = InclusionStrategy.Security_S2;
+ this._startInclusion(provisioningInfo);
+ } else {
+ this._error = this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.qr.unsupported_code",
+ { code: qrCodeString }
+ );
+ this._step = "failed";
+ }
+ }
+
+ private _setDeviceOptions(ev: CustomEvent) {
+ this._deviceOptions = ev.detail.value;
+ }
+
+ private async _saveDevice() {
+ // smart start device
+ if (!this._device?.id) {
+ if (!this._deviceOptions?.name || !this._device?.provisioningInfo) {
+ return;
+ }
+
+ this._step = "loading";
+
+ try {
+ const id = await provisionZwaveSmartStartNode(
+ this.hass,
+ this._entryId!,
+ {
+ ...this._device.provisioningInfo,
+ status: ProvisioningEntryStatus.Active,
+ },
+ this._deviceOptions.network_type
+ ? Number(this._deviceOptions.network_type)
+ : undefined,
+ this._deviceOptions.name,
+ this._deviceOptions.area
+ );
+ this._device.id = id;
+ this._subscribeNewDeviceSearch();
+ this._step = "search_smart_start_device";
+ } catch (err: any) {
+ this._error = err.message;
+ this._step = "failed";
+ }
+ } else {
+ this._step = "rename_device";
+ const nameChanged = this._device.name !== this._deviceOptions?.name;
+ if (nameChanged || this._deviceOptions?.area) {
+ const oldDeviceName = this._device.name;
+ const newDeviceName = this._deviceOptions!.name;
+ try {
+ await updateDeviceRegistryEntry(this.hass, this._device.id, {
+ name_by_user: this._deviceOptions!.name,
+ area_id: this._deviceOptions!.area,
+ });
+
+ if (nameChanged) {
+ // rename entities
+ const oldDeviceEntityId = slugify(oldDeviceName);
+ const newDeviceEntityId = slugify(newDeviceName);
+
+ await Promise.all(
+ this._entities
+ .filter((entity) => entity.device_id === this._device!.id)
+ .map((entity) => {
+ const entityState = this.hass.states[entity.entity_id];
+ const name =
+ entity.name ||
+ (entityState && computeStateName(entityState));
+ let newEntityId: string | null = null;
+ let newName: string | null = null;
+
+ if (name && name.includes(oldDeviceName)) {
+ newName = name.replace(oldDeviceName, newDeviceName);
+ }
+
+ newEntityId = entity.entity_id.replace(
+ oldDeviceEntityId,
+ newDeviceEntityId
+ );
+
+ if (!newName && newEntityId === entity.entity_id) {
+ return undefined;
+ }
+
+ return updateEntityRegistryEntry(
+ this.hass!,
+ entity.entity_id,
+ {
+ name: newName || name,
+ new_entity_id: newEntityId || entity.entity_id,
+ }
+ );
+ })
+ );
+ }
+ } catch (_err: any) {
+ this._error = this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.configure_device.save_device_failed"
+ );
+ this._step = "failed";
+ return;
+ }
+ }
+
+ // if device wasn't added securely show added added-insecure screen
+ if (this._lowSecurity) {
+ this._step = "added_insecure";
+ return;
+ }
+
+ this._navigateToDevice();
+ }
+ }
+
+ private _navigateToDevice() {
+ const deviceId = this._device?.id;
+
+ if (deviceId) {
+ setTimeout(() => {
+ // delay to ensure the node is added after smart start
+ // in this case we don't have a subscription to the node's "node added" event
+ // and it is near simultaneous with "device registered" event
+ navigate(`/config/devices/device/${deviceId}`);
+ }, 1000);
+ } else {
+ this.closeDialog();
+ }
+ }
+
+ private _subscribeNewDeviceSearch() {
+ if (!this._device?.id) {
+ return;
+ }
+ this._newDeviceSubscription = subscribeNewDevices(
+ this.hass,
+ this._entryId!,
+ ({ event, device }) => {
+ if (
+ event === "device registered" &&
+ this._device?.id &&
+ device.id === this._device.id
+ ) {
+ this._unsubscribeNewDeviceSearch();
+ this._navigateToDevice();
+ }
+ }
+ );
+ }
+
+ private _addAnotherDevice() {
+ this._unsubscribeNewDeviceSearch();
+ this._showFirstStep();
+ }
+
+ private _manualQrCodeInputChange(ev: CustomEvent): void {
+ this._codeInput = ev.detail.value;
+ }
+
+ private _dskPinChanged(ev: CustomEvent): void {
+ this._dskPin = ev.detail.value;
+ }
+
+ private async _validateDskAndEnterPin(): Promise {
+ this._step = "waiting_for_device";
+ this._error = undefined;
+ try {
+ await zwaveValidateDskAndEnterPin(
+ this.hass,
+ this._entryId!,
+ this._dskPin
+ );
+ } catch (err: any) {
+ this._error = err.message;
+ this._step = "validate_dsk_enter_pin";
+ }
+ }
+
+ private async _grantSecurityClasses() {
+ this._step = "waiting_for_device";
+ this._error = undefined;
+ try {
+ await zwaveGrantSecurityClasses(
+ this.hass,
+ this._entryId!,
+ this._securityClasses
+ );
+ } catch (err: any) {
+ this._error = err.message;
+ this._step = "grant_security_classes";
+ }
+ }
+
+ private _securityClassChange(ev: CustomEvent) {
+ this._securityClasses = ev.detail.value;
+ }
+
+ private _unsubscribeNewDeviceSearch() {
+ if (this._newDeviceSubscription) {
+ this._newDeviceSubscription.then((unsub) => unsub && unsub());
+ this._newDeviceSubscription = undefined;
+ }
+ }
+
+ private _unsubscribe(): void {
+ if (this._subscribed) {
+ this._subscribed.then((unsub) => unsub && unsub());
+ this._subscribed = undefined;
+
+ if (this._entryId) {
+ stopZwaveInclusion(this.hass, this._entryId);
+ if (
+ this._step &&
+ [
+ "waiting_for_device",
+ "validate_dsk_enter_pin",
+ "grant_security_classes",
+ ].includes(this._step)
+ ) {
+ cancelSecureBootstrapS2(this.hass, this._entryId);
+ }
+ }
+ }
+
+ this._unsubscribeNewDeviceSearch();
+
+ this._requestedGrant = undefined;
+ this._securityClasses = [];
+ this._dsk = undefined;
+ this._dskPin = "";
+ this._lowSecurity = false;
+ this._lowSecurityReason = undefined;
+ this._inclusionStrategy = undefined;
+
+ if (this._addNodeTimeoutHandle) {
+ clearTimeout(this._addNodeTimeoutHandle);
+ }
+ this._addNodeTimeoutHandle = undefined;
+ window.removeEventListener("beforeunload", this._onBeforeUnload);
+ }
+
+ private _dialogClosed() {
+ this._unsubscribe();
+ this._open = false;
+ this._entryId = undefined;
+ this._step = undefined;
+ this._device = undefined;
+ this._error = undefined;
+ this._codeInput = "";
+ this._deviceOptions = undefined;
+
+ this._onStop?.();
+ fireEvent(this, "dialog-closed", { dialog: this.localName });
+ }
+
+ public async closeDialog() {
+ // special case for validate dsk and enter pin to stop 4 minute waiting process
+ if (this._step === "validate_dsk_enter_pin") {
+ this._step = "loading";
+ try {
+ await zwaveValidateDskAndEnterPin(this.hass, this._entryId!, false);
+ } catch (err: any) {
+ // ignore
+ // eslint-disable-next-line no-console
+ console.error("Failed to cancel DSK validation");
+ // eslint-disable-next-line no-console
+ console.error(err);
+ }
+ }
+
+ if (this._open) {
+ this._dialog?.close();
+ } else {
+ this._dialogClosed();
+ }
+ }
+
+ public disconnectedCallback(): void {
+ super.disconnectedCallback();
+ window.removeEventListener("beforeunload", this._onBeforeUnload);
+
+ this._unsubscribe();
+ }
+
+ static get styles(): CSSResultGroup {
+ return [
+ css`
+ ha-dialog {
+ --mdc-dialog-min-width: 512px;
+ }
+ @media all and (max-width: 500px), all and (max-height: 500px) {
+ ha-dialog {
+ --mdc-dialog-min-width: calc(
+ 100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
+ );
+ --mdc-dialog-max-width: calc(
+ 100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
+ );
+ --mdc-dialog-min-height: 100%;
+ --mdc-dialog-max-height: 100%;
+ --vertical-align-dialog: flex-end;
+ --ha-dialog-border-radius: 0;
+ }
+ }
+ ha-fade-in {
+ display: block;
+ }
+ `,
+ ];
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "dialog-zwave_js-add-node": DialogZWaveJSAddNode;
+ }
+}
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/add-node/show-dialog-zwave_js-add-node.ts
similarity index 78%
rename from src/panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node.ts
rename to src/panels/config/integrations/integration-panels/zwave_js/add-node/show-dialog-zwave_js-add-node.ts
index 27ef0ccd87..39e12e7bab 100644
--- a/src/panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node.ts
+++ b/src/panels/config/integrations/integration-panels/zwave_js/add-node/show-dialog-zwave_js-add-node.ts
@@ -1,7 +1,9 @@
-import { fireEvent } from "../../../../../common/dom/fire_event";
+import { fireEvent } from "../../../../../../common/dom/fire_event";
export interface ZWaveJSAddNodeDialogParams {
entry_id: string;
+ longRangeSupported?: boolean;
+ inclusionOngoing?: boolean;
dsk?: string;
onStop?: () => void;
}
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-added-insecure.ts b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-added-insecure.ts
new file mode 100644
index 0000000000..b19a8262ab
--- /dev/null
+++ b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-added-insecure.ts
@@ -0,0 +1,65 @@
+import { mdiCheckCircleOutline } from "@mdi/js";
+import { customElement, property } from "lit/decorators";
+import "@shoelace-style/shoelace/dist/components/animation/animation";
+import { css, html, LitElement } from "lit";
+import type { HomeAssistant } from "../../../../../../types";
+
+import "../../../../../../components/ha-svg-icon";
+import "../../../../../../components/ha-alert";
+
+@customElement("zwave-js-add-node-added-insecure")
+export class ZWaveJsAddNodeFinished extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property({ attribute: "device-name" }) public deviceName?: string;
+
+ @property() public reason?;
+
+ render() {
+ return html`
+
+
+
+
+ ${this.reason
+ ? this.hass.localize(
+ `ui.panel.config.zwave_js.add_node.added_insecure.low_security_reason.${this.reason}`
+ )
+ : ""}
+ ${this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.added_insecure.added_insecurely_text",
+ {
+ deviceName: html`${this.deviceName} `,
+ }
+ )}
+
+ ${this.hass.localize(
+ `ui.panel.config.zwave_js.add_node.added_insecure.try_again_text`
+ )}
+
+
+ `;
+ }
+
+ static styles = css`
+ :host {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ }
+ ha-svg-icon {
+ --mdc-icon-size: 96px;
+ color: var(--warning-color);
+ }
+ ha-alert {
+ margin-top: 16px;
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "zwave-js-add-node-added-insecure": ZWaveJsAddNodeFinished;
+ }
+}
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-code-input.ts b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-code-input.ts
new file mode 100644
index 0000000000..ed91ba56bf
--- /dev/null
+++ b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-code-input.ts
@@ -0,0 +1,99 @@
+import { customElement, property } from "lit/decorators";
+import { css, html, LitElement, nothing } from "lit";
+
+import { fireEvent } from "../../../../../../common/dom/fire_event";
+import type { HaTextField } from "../../../../../../components/ha-textfield";
+
+import "../../../../../../components/ha-textfield";
+import "../../../../../../components/ha-alert";
+
+@customElement("zwave-js-add-node-code-input")
+export class ZWaveJsAddNodeCodeInput extends LitElement {
+ @property() public value = "";
+
+ @property() public description = "";
+
+ @property() public placeholder = "";
+
+ @property({ attribute: "reference-key" }) public referenceKey = "";
+
+ @property() public error?: string;
+
+ @property({ type: Boolean }) public numeric = false;
+
+ render() {
+ return html`
+ ${this.description}
+ ${this.error
+ ? html`${this.error} `
+ : nothing}
+
+ ${this.referenceKey
+ ? html`
+ ${this.value.padEnd(5, "·")} ${this.referenceKey}
+
`
+ : nothing}
+ `;
+ }
+
+ private _handleKeyup(ev: KeyboardEvent): void {
+ if (ev.key === "Enter" && this.value) {
+ fireEvent(this, "z-wave-submit");
+ }
+ }
+
+ private _handleChange(ev: InputEvent): void {
+ const inputElement = ev.target as HaTextField;
+ if (
+ this.numeric &&
+ (isNaN(Number(inputElement.value)) || inputElement.value.length > 5)
+ ) {
+ inputElement.value = this.value;
+ return;
+ }
+
+ this.value = (ev.target as HaTextField).value;
+
+ fireEvent(this, "value-changed", {
+ value: (ev.target as HaTextField).value,
+ });
+ }
+
+ static styles = css`
+ ha-textfield {
+ width: 100%;
+ }
+ ha-alert {
+ display: block;
+ margin-bottom: 16px;
+ }
+ p {
+ color: var(--secondary-text-color);
+ margin-top: 0;
+ margin-bottom: 16px;
+ }
+ div {
+ font-family: "Roboto Mono", "Consolas", "Menlo", monospace;
+ margin-top: 16px;
+ }
+ div span {
+ color: var(--primary-color);
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "zwave-js-add-node-code-input": ZWaveJsAddNodeCodeInput;
+ }
+ interface HASSDomEvents {
+ "z-wave-submit";
+ }
+}
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-configure-device.ts b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-configure-device.ts
new file mode 100644
index 0000000000..721c1a9417
--- /dev/null
+++ b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-configure-device.ts
@@ -0,0 +1,152 @@
+import { customElement, property, state } from "lit/decorators";
+import { html, LitElement, type PropertyValues } from "lit";
+import memoizeOne from "memoize-one";
+
+import type { HomeAssistant } from "../../../../../../types";
+import type { LocalizeFunc } from "../../../../../../common/translations/localize";
+import type { HaFormSchema } from "../../../../../../components/ha-form/types";
+import { fireEvent } from "../../../../../../common/dom/fire_event";
+import { Protocols } from "../../../../../../data/zwave_js";
+import type { ZWaveJSAddNodeSmartStartOptions } from "./data";
+
+import "../../../../../../components/ha-form/ha-form";
+
+@customElement("zwave-js-add-node-configure-device")
+export class ZWaveJsAddNodeConfigureDevice extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property({ attribute: "device-name" }) public deviceName = "";
+
+ @property({ type: Boolean, attribute: "lr-supported" })
+ public longRangeSupported = false;
+
+ @state() private _options?: ZWaveJSAddNodeSmartStartOptions;
+
+ protected willUpdate(changedProps: PropertyValues): void {
+ super.willUpdate(changedProps);
+
+ if (!this.hasUpdated) {
+ this._options = {
+ name: this.deviceName,
+ };
+
+ if (this.longRangeSupported) {
+ this._options.network_type = Protocols.ZWaveLongRange.toString();
+ }
+
+ fireEvent(this, "value-changed", { value: this._options });
+ }
+ }
+
+ render() {
+ return html`
+
+
+ `;
+ }
+
+ private _getSchema = memoizeOne(
+ (localize: LocalizeFunc, longRangeSupported: boolean): HaFormSchema[] => {
+ const schema: HaFormSchema[] = [
+ {
+ name: "name",
+ required: true,
+ default: this.deviceName,
+ type: "string",
+ autofocus: true,
+ },
+ {
+ name: "area",
+ selector: {
+ area: {},
+ },
+ },
+ ];
+
+ if (longRangeSupported) {
+ schema.push({
+ name: "network_type",
+ required: true,
+ selector: {
+ select: {
+ box_max_columns: 1,
+ mode: "box",
+ options: [
+ {
+ value: Protocols.ZWaveLongRange.toString(),
+ label: localize(
+ "ui.panel.config.zwave_js.add_node.configure_device.long_range_label"
+ ),
+ description: localize(
+ "ui.panel.config.zwave_js.add_node.configure_device.long_range_description"
+ ),
+ image: {
+ src: "/static/images/z-wave-add-node/long-range.svg",
+ src_dark:
+ "/static/images/z-wave-add-node/long-range_dark.svg",
+ flip_rtl: true,
+ },
+ },
+ {
+ value: Protocols.ZWave.toString(),
+ label: localize(
+ "ui.panel.config.zwave_js.add_node.configure_device.mesh_label"
+ ),
+ description: localize(
+ "ui.panel.config.zwave_js.add_node.configure_device.mesh_description"
+ ),
+ image: {
+ src: "/static/images/z-wave-add-node/mesh.svg",
+ src_dark: "/static/images/z-wave-add-node/mesh_dark.svg",
+ flip_rtl: true,
+ },
+ },
+ ],
+ },
+ },
+ });
+ }
+ return schema;
+ }
+ );
+
+ private _computeLabel = (schema: HaFormSchema): string | undefined => {
+ if (schema.name === "network_type") {
+ return this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.configure_device.choose_network_type"
+ );
+ }
+ if (schema.name === "name") {
+ return this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.configure_device.device_name"
+ );
+ }
+ if (schema.name === "area") {
+ return this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.configure_device.device_area"
+ );
+ }
+ return undefined;
+ };
+
+ private _setOptions(event: any) {
+ this._options = {
+ ...this._options!,
+ ...event.detail.value,
+ };
+
+ fireEvent(this, "value-changed", { value: this._options });
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "zwave-js-add-node-configure-device": ZWaveJsAddNodeConfigureDevice;
+ }
+}
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-failed.ts b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-failed.ts
new file mode 100644
index 0000000000..7f9ef1412d
--- /dev/null
+++ b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-failed.ts
@@ -0,0 +1,68 @@
+import { customElement, property } from "lit/decorators";
+import { css, html, LitElement, nothing } from "lit";
+import type { HomeAssistant } from "../../../../../../types";
+import type { ZWaveJSAddNodeDevice } from "./data";
+
+import "../../../../../../components/ha-alert";
+import "../../../../../../components/ha-button";
+
+@customElement("zwave-js-add-node-failed")
+export class ZWaveJsAddNodeFailed extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property() public error?: string;
+
+ @property({ attribute: false }) public device?: ZWaveJSAddNodeDevice;
+
+ render() {
+ return html`
+
+ ${this.error ||
+ this.hass.localize("ui.panel.config.zwave_js.add_node.check_logs")}
+
+ ${this.error
+ ? html`
+ ${this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.check_logs"
+ )}
+
`
+ : nothing}
+ ${this.device?.id
+ ? html`
+
+ ${this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.view_device"
+ )}
+
+ `
+ : nothing}
+ `;
+ }
+
+ static styles = css`
+ :host {
+ display: block;
+ padding: 16px;
+ }
+ div.note {
+ text-align: center;
+ margin-top: 16px;
+ font-size: 12px;
+ color: var(--secondary-text-color);
+ }
+ ha-button {
+ margin-top: 32px;
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "zwave-js-add-node-failed": ZWaveJsAddNodeFailed;
+ }
+}
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-grant-security-classes.ts b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-grant-security-classes.ts
new file mode 100644
index 0000000000..3155d74354
--- /dev/null
+++ b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-grant-security-classes.ts
@@ -0,0 +1,108 @@
+import { customElement, property } from "lit/decorators";
+import "@shoelace-style/shoelace/dist/components/animation/animation";
+import { css, html, LitElement, nothing } from "lit";
+import type { HomeAssistant } from "../../../../../../types";
+import { SecurityClass } from "../../../../../../data/zwave_js";
+import type { HaCheckbox } from "../../../../../../components/ha-checkbox";
+import { fireEvent } from "../../../../../../common/dom/fire_event";
+
+import "../../../../../../components/ha-alert";
+import "../../../../../../components/ha-formfield";
+import "../../../../../../components/ha-checkbox";
+
+@customElement("zwave-js-add-node-grant-security-classes")
+export class ZWaveJsAddNodeGrantSecurityClasses extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property() public error?: string;
+
+ @property({ attribute: false }) public securityClassOptions!: SecurityClass[];
+
+ @property({ attribute: false })
+ public selectedSecurityClasses: SecurityClass[] = [];
+
+ render() {
+ return html`
+ ${this.error
+ ? html` ${this.error} `
+ : nothing}
+
+ ${this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.grant_security_classes.description"
+ )}
+
+
+ ${this.securityClassOptions
+ .sort((a, b) => {
+ // Put highest security classes at the top, S0 at the bottom
+ if (a === SecurityClass.S0_Legacy) return 1;
+ if (b === SecurityClass.S0_Legacy) return -1;
+ return b - a;
+ })
+ .map(
+ (securityClass) =>
+ html`
${this.hass.localize(
+ `ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.title`
+ )}
+
+ ${this.hass.localize(
+ `ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.description`
+ )}
+
`}
+ >
+
+
+ `
+ )}
+
+ `;
+ }
+
+ private _handleSecurityClassChange(ev: CustomEvent) {
+ const checkbox = ev.currentTarget as HaCheckbox;
+ const securityClass = Number(checkbox.value);
+ if (
+ checkbox.checked &&
+ !this.selectedSecurityClasses.includes(securityClass)
+ ) {
+ fireEvent(this, "value-changed", {
+ value: [...this.selectedSecurityClasses, securityClass],
+ });
+ } else if (!checkbox.checked) {
+ fireEvent(this, "value-changed", {
+ value: this.selectedSecurityClasses.filter(
+ (val) => val !== securityClass
+ ),
+ });
+ }
+ }
+
+ static styles = css`
+ ha-alert {
+ display: block;
+ margin-bottom: 16px;
+ }
+ .flex-column {
+ display: flex;
+ flex-direction: column;
+ }
+ .secondary {
+ color: var(--secondary-text-color);
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "zwave-js-add-node-grant-security-classes": ZWaveJsAddNodeGrantSecurityClasses;
+ }
+}
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-loading.ts b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-loading.ts
new file mode 100644
index 0000000000..bae3acb94b
--- /dev/null
+++ b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-loading.ts
@@ -0,0 +1,46 @@
+import { customElement, property } from "lit/decorators";
+import { css, html, LitElement, nothing } from "lit";
+
+import "../../../../../../components/ha-fade-in";
+import "../../../../../../components/ha-spinner";
+
+@customElement("zwave-js-add-node-loading")
+export class ZWaveJsAddNodeLoading extends LitElement {
+ @property() public description?: string;
+
+ @property({ type: Number }) public delay = 0;
+
+ render() {
+ return html`
+
+
+
+
+ ${this.description ? html`${this.description}
` : nothing}
+
+ `;
+ }
+
+ static styles = css`
+ ha-fade-in {
+ display: block;
+ }
+ .loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 200px;
+ }
+ p {
+ margin-top: 16px;
+ color: var(--secondary-text-color);
+ text-align: center;
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "zwave-js-add-node-loading": ZWaveJsAddNodeLoading;
+ }
+}
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-searching-devices.ts b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-searching-devices.ts
new file mode 100644
index 0000000000..c92c856e9b
--- /dev/null
+++ b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-searching-devices.ts
@@ -0,0 +1,164 @@
+import "@shoelace-style/shoelace/dist/components/animation/animation";
+import { mdiRestart } from "@mdi/js";
+
+import { customElement, property } from "lit/decorators";
+import { css, html, LitElement, nothing } from "lit";
+import type { HomeAssistant } from "../../../../../../types";
+import { fireEvent } from "../../../../../../common/dom/fire_event";
+import { InclusionStrategy } from "../../../../../../data/zwave_js";
+
+import "../../../../../../components/ha-spinner";
+import "../../../../../../components/ha-button";
+import "../../../../../../components/ha-alert";
+
+@customElement("zwave-js-add-node-searching-devices")
+export class ZWaveJsAddNodeSearchingDevices extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property({ type: Boolean, attribute: "smart-start" })
+ public smartStart = false;
+
+ @property({ type: Boolean, attribute: "show-security-options" })
+ public showSecurityOptions = false;
+
+ @property({ type: Boolean, attribute: "show-add-another-device" })
+ public showAddAnotherDevice = false;
+
+ @property({ attribute: false }) public inclusionStrategy?: InclusionStrategy;
+
+ render() {
+ let inclusionStrategyTranslationKey = "";
+ if (this.inclusionStrategy !== undefined) {
+ switch (this.inclusionStrategy) {
+ case InclusionStrategy.Security_S0:
+ inclusionStrategyTranslationKey = "s0";
+ break;
+ case InclusionStrategy.Insecure:
+ inclusionStrategyTranslationKey = "insecure";
+ break;
+ default:
+ inclusionStrategyTranslationKey = "default";
+ }
+ }
+
+ return html`
+
+
+ ${this.smartStart
+ ? html`
+
+ ${this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.specific_device.turn_on_device_description"
+ )}
+
+
+ ${this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.specific_device.close_description"
+ )}
+
`
+ : html`
+
+ ${this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.follow_device_instructions"
+ )}
+
+ `}
+ ${this.showSecurityOptions && !inclusionStrategyTranslationKey
+ ? html`
+ ${this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.security_options"
+ )}
+ `
+ : inclusionStrategyTranslationKey
+ ? html`
+ ${this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.select_strategy.inclusion_strategy",
+ {
+ strategy: this.hass.localize(
+ `ui.panel.config.zwave_js.add_node.select_strategy.${inclusionStrategyTranslationKey}_label`
+ ),
+ }
+ )}
+ `
+ : nothing}
+ ${this.showAddAnotherDevice
+ ? html`
+ ${this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.specific_device.add_another_z_wave_device"
+ )}
+ `
+ : nothing}
+
+ `;
+ }
+
+ private _handleSecurityOptions() {
+ fireEvent(this, "show-z-wave-security-options");
+ }
+
+ private _handleAddAnotherDevice() {
+ fireEvent(this, "add-another-z-wave-device");
+ }
+
+ static styles = css`
+ :host {
+ text-align: center;
+ display: block;
+ }
+ ha-alert {
+ margin-top: 32px;
+ display: block;
+ }
+ .note {
+ font-size: 12px;
+ color: var(--secondary-text-color);
+ }
+ .searching-spinner {
+ margin-left: auto;
+ margin-right: auto;
+ position: relative;
+ width: 128px;
+ height: 128px;
+ }
+ .searching-spinner .circle {
+ border-radius: 50%;
+ background-color: var(--light-primary-color);
+ position: absolute;
+ width: calc(100% - 32px);
+ height: calc(100% - 32px);
+ margin: 16px;
+ }
+ .searching-spinner .spinner {
+ z-index: 1;
+ position: absolute;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ --ha-spinner-divider-color: var(--light-primary-color);
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "zwave-js-add-node-searching-devices": ZWaveJsAddNodeSearchingDevices;
+ }
+
+ interface HASSDomEvents {
+ "show-z-wave-security-options": undefined;
+ "add-another-z-wave-device": undefined;
+ }
+}
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-select-method.ts b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-select-method.ts
new file mode 100644
index 0000000000..e60595dbff
--- /dev/null
+++ b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-select-method.ts
@@ -0,0 +1,112 @@
+import { customElement, property } from "lit/decorators";
+import { css, html, LitElement, nothing } from "lit";
+import { fireEvent } from "../../../../../../common/dom/fire_event";
+import type { HomeAssistant } from "../../../../../../types";
+
+import "../../../../../../components/ha-md-list";
+import "../../../../../../components/ha-md-list-item";
+import "../../../../../../components/ha-alert";
+import "../../../../../../components/ha-icon-next";
+
+@customElement("zwave-js-add-node-select-method")
+export class ZWaveJsAddNodeSelectMethod extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property({ type: Boolean, attribute: "hide-qr-webcam" })
+ public hideQrWebcam = false;
+
+ render() {
+ return html`
+ ${!this.hideQrWebcam && !window.isSecureContext
+ ? html`
+ ${this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.select_method.webcam_unsupported"
+ )} `
+ : nothing}
+
+ ${!this.hideQrWebcam
+ ? html`
+
+ ${this.hass.localize(
+ `ui.panel.config.zwave_js.add_node.select_method.qr_code_webcam`
+ )}
+
+
+ ${this.hass.localize(
+ `ui.panel.config.zwave_js.add_node.select_method.qr_code_webcam_description`
+ )}
+
+
+ `
+ : nothing}
+
+
+ ${this.hass.localize(
+ `ui.panel.config.zwave_js.add_node.select_method.qr_code_manual`
+ )}
+
+
+ ${this.hass.localize(
+ `ui.panel.config.zwave_js.add_node.select_method.qr_code_manual_description`
+ )}
+
+
+
+
+
+ ${this.hass.localize(
+ `ui.panel.config.zwave_js.add_node.select_method.search_device`
+ )}
+
+
+ ${this.hass.localize(
+ `ui.panel.config.zwave_js.add_node.select_method.search_device_description`
+ )}
+
+
+
+
+ `;
+ }
+
+ private _selectMethod(event: any) {
+ const method = event.currentTarget.value;
+ if (method !== "qr_code_webcam" || window.isSecureContext) {
+ fireEvent(this, "z-wave-method-selected", { method });
+ }
+ }
+
+ static styles = css`
+ ha-md-list {
+ padding: 0;
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "zwave-js-add-node-select-method": ZWaveJsAddNodeSelectMethod;
+ }
+ interface HASSDomEvents {
+ "z-wave-method-selected": {
+ method: "qr_code_webcam" | "qr_code_manual" | "search_device";
+ };
+ }
+}
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-select-security-strategy.ts b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-select-security-strategy.ts
new file mode 100644
index 0000000000..05adc59102
--- /dev/null
+++ b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-select-security-strategy.ts
@@ -0,0 +1,107 @@
+import { customElement, property, state } from "lit/decorators";
+import { css, html, LitElement } from "lit";
+import memoizeOne from "memoize-one";
+
+import { fireEvent } from "../../../../../../common/dom/fire_event";
+import type { HomeAssistant } from "../../../../../../types";
+import { InclusionStrategy } from "../../../../../../data/zwave_js";
+import type { LocalizeFunc } from "../../../../../../common/translations/localize";
+import type { HaFormSchema } from "../../../../../../components/ha-form/types";
+
+import "../../../../../../components/ha-form/ha-form";
+
+@customElement("zwave-js-add-node-select-security-strategy")
+export class ZWaveJsAddNodeSelectMethod extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @state() public _inclusionStrategy?: InclusionStrategy;
+
+ private _getSchema = memoizeOne((localize: LocalizeFunc): HaFormSchema[] => [
+ {
+ name: "strategy",
+ required: true,
+ selector: {
+ select: {
+ box_max_columns: 1,
+ mode: "box",
+ options: [
+ {
+ value: InclusionStrategy.Default.toString(),
+ label: localize(
+ "ui.panel.config.zwave_js.add_node.select_strategy.default_label"
+ ),
+ description: localize(
+ "ui.panel.config.zwave_js.add_node.select_strategy.default_description"
+ ),
+ },
+ {
+ value: InclusionStrategy.Security_S0.toString(),
+ label: localize(
+ "ui.panel.config.zwave_js.add_node.select_strategy.s0_label"
+ ),
+ description: localize(
+ "ui.panel.config.zwave_js.add_node.select_strategy.s0_description"
+ ),
+ },
+ {
+ value: InclusionStrategy.Insecure.toString(),
+ label: localize(
+ "ui.panel.config.zwave_js.add_node.select_strategy.insecure_label"
+ ),
+ description: localize(
+ "ui.panel.config.zwave_js.add_node.select_strategy.insecure_description"
+ ),
+ },
+ ],
+ },
+ },
+ },
+ ]);
+
+ render() {
+ return html`
+
+
+ `;
+ }
+
+ private _computeLabel = () =>
+ this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.select_strategy.title"
+ );
+
+ private _selectStrategy(event: any) {
+ const selectedStrategy = Number(
+ event.detail.value.strategy
+ ) as InclusionStrategy;
+ fireEvent(this, "z-wave-strategy-selected", {
+ strategy: selectedStrategy,
+ });
+ }
+
+ static styles = css`
+ :host {
+ display: block;
+ padding: 16px;
+ }
+ ha-md-list {
+ padding: 0;
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "zwave-js-add-node-select-security-strategy": ZWaveJsAddNodeSelectMethod;
+ }
+ interface HASSDomEvents {
+ "z-wave-strategy-selected": {
+ strategy: InclusionStrategy;
+ };
+ }
+}
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-add-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave_js-add-node.ts
similarity index 85%
rename from src/panels/config/integrations/integration-panels/zwave_js/zwave_js-add-node.ts
rename to src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave_js-add-node.ts
index 6c7a36486b..83f919c544 100644
--- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-add-node.ts
+++ b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave_js-add-node.ts
@@ -1,7 +1,7 @@
/* eslint-disable lit/lifecycle-super */
import { customElement } from "lit/decorators";
-import { navigate } from "../../../../../common/navigate";
-import type { HomeAssistant } from "../../../../../types";
+import { navigate } from "../../../../../../common/navigate";
+import type { HomeAssistant } from "../../../../../../types";
import { showZWaveJSAddNodeDialog } from "./show-dialog-zwave_js-add-node";
@customElement("zwave_js-add-node")
@@ -21,6 +21,7 @@ export class DialogZWaveJSAddNode extends HTMLElement {
replace: true,
}
);
+
showZWaveJSAddNodeDialog(this, {
entry_id: this.configEntryId,
});
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-add-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-add-node.ts
deleted file mode 100644
index 2151578fce..0000000000
--- a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-add-node.ts
+++ /dev/null
@@ -1,1017 +0,0 @@
-import "@material/mwc-button/mwc-button";
-import { mdiAlertCircle, mdiCheckCircle, mdiQrcodeScan } from "@mdi/js";
-import type { UnsubscribeFunc } from "home-assistant-js-websocket";
-import type { CSSResultGroup } from "lit";
-import { css, html, LitElement, nothing } from "lit";
-import { customElement, property, query, state } from "lit/decorators";
-import { ifDefined } from "lit/directives/if-defined";
-import { fireEvent } from "../../../../../common/dom/fire_event";
-import "../../../../../components/ha-alert";
-import type { HaCheckbox } from "../../../../../components/ha-checkbox";
-import "../../../../../components/ha-spinner";
-import { createCloseHeading } from "../../../../../components/ha-dialog";
-import "../../../../../components/ha-checkbox";
-import "../../../../../components/ha-formfield";
-import "../../../../../components/ha-qr-scanner";
-import "../../../../../components/ha-radio";
-import "../../../../../components/ha-switch";
-import "../../../../../components/ha-textfield";
-import type { HaTextField } from "../../../../../components/ha-textfield";
-import type {
- QRProvisioningInformation,
- RequestedGrant,
-} from "../../../../../data/zwave_js";
-import {
- cancelSecureBootstrapS2,
- InclusionStrategy,
- MINIMUM_QR_STRING_LENGTH,
- provisionZwaveSmartStartNode,
- SecurityClass,
- stopZwaveInclusion,
- subscribeAddZwaveNode,
- ZWaveFeature,
- zwaveGrantSecurityClasses,
- zwaveParseQrCode,
- zwaveSupportsFeature,
- zwaveTryParseDskFromQrCode,
- zwaveValidateDskAndEnterPin,
-} from "../../../../../data/zwave_js";
-import { haStyle, haStyleDialog } from "../../../../../resources/styles";
-import type { HomeAssistant } from "../../../../../types";
-import type { ZWaveJSAddNodeDialogParams } from "./show-dialog-zwave_js-add-node";
-
-export interface ZWaveJSAddNodeDevice {
- id: string;
- name: string;
-}
-
-@customElement("dialog-zwave_js-add-node")
-class DialogZWaveJSAddNode extends LitElement {
- @property({ attribute: false }) public hass!: HomeAssistant;
-
- @state() private _params?: ZWaveJSAddNodeDialogParams;
-
- @state() private _entryId?: string;
-
- @state() private _status?:
- | "loading"
- | "started"
- | "started_specific"
- | "choose_strategy"
- | "qr_scan"
- | "interviewing"
- | "failed"
- | "timed_out"
- | "finished"
- | "provisioned"
- | "validate_dsk_enter_pin"
- | "grant_security_classes"
- | "waiting_for_device";
-
- @state() private _device?: ZWaveJSAddNodeDevice;
-
- @state() private _stages?: string[];
-
- @state() private _inclusionStrategy?: InclusionStrategy;
-
- @state() private _dsk?: string;
-
- @state() private _error?: string;
-
- @state() private _requestedGrant?: RequestedGrant;
-
- @state() private _securityClasses: SecurityClass[] = [];
-
- @state() private _lowSecurity = false;
-
- @state() private _lowSecurityReason?: number;
-
- @state() private _supportsSmartStart?: boolean;
-
- private _addNodeTimeoutHandle?: number;
-
- private _subscribed?: Promise;
-
- private _qrProcessing = false;
-
- public connectedCallback(): void {
- super.connectedCallback();
- window.addEventListener("beforeunload", this._onBeforeUnload);
- }
-
- public disconnectedCallback(): void {
- super.disconnectedCallback();
- this._unsubscribe();
- }
-
- public async showDialog(params: ZWaveJSAddNodeDialogParams): Promise {
- if (this._status) {
- // already started
- return;
- }
- this._params = params;
- this._entryId = params.entry_id;
- this._status = "loading";
- this._checkSmartStartSupport();
- if (params.dsk) {
- this._status = "validate_dsk_enter_pin";
- this._dsk = params.dsk;
- }
- this._startInclusion();
- }
-
- @query("#pin-input") private _pinInput?: HaTextField;
-
- protected render() {
- if (!this._entryId) {
- return nothing;
- }
-
- // Prevent accidentally closing the dialog in certain stages
- const preventClose = this._shouldPreventClose();
-
- const heading = this.hass.localize(
- "ui.panel.config.zwave_js.add_node.title"
- );
-
- return html`
-
- ${this._status === "loading"
- ? html`
-
-
`
- : this._status === "waiting_for_device"
- ? html`
-
-
- ${this.hass.localize(
- "ui.panel.config.zwave_js.add_node.waiting_for_device"
- )}
-
-
`
- : this._status === "choose_strategy"
- ? html`Choose strategy
-
-
Secure if possible
-
- Requires user interaction during inclusion. Fast and
- secure with S2 when supported. Allows manually
- selecting which security keys to grant. Fallback to
- legacy S0 or no encryption when necessary.
-
`}
- >
-
-
-
-
Legacy Secure
-
- Uses the older S0 security that is secure, but slow
- due to a lot of overhead. Allows securely including S2
- capable devices which fail to be included with S2.
-
`}
- >
-
-
-
-
Insecure
- Do not use encryption.
`}
- >
-
-
-
-
-
- Search device
- `
- : this._status === "qr_scan"
- ? html`
-
- ${this.hass.localize(
- "ui.panel.config.zwave_js.common.back"
- )}
- `
- : this._status === "validate_dsk_enter_pin"
- ? html`
-
- Please enter the 5-digit PIN for your device and verify that
- the rest of the device-specific key matches the one that can
- be found on your device or the manual.
-
- ${
- this._error
- ? html`
- ${this._error}
- `
- : ""
- }
-
-
- ${this._dsk}
-
-
- Submit
-
-
- `
- : this._status === "grant_security_classes"
- ? html`
-
- The device has requested the following security
- classes:
-
- ${this._error
- ? html`${this._error} `
- : ""}
-
- ${this._requestedGrant?.securityClasses
- .sort((a, b) => {
- // Put highest security classes at the top, S0 at the bottom
- if (a === SecurityClass.S0_Legacy) return 1;
- if (b === SecurityClass.S0_Legacy) return -1;
- return b - a;
- })
- .map(
- (securityClass) =>
- html`
${this.hass.localize(
- `ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.title`
- )}
-
- ${this.hass.localize(
- `ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.description`
- )}
-
`}
- >
-
-
- `
- )}
-
-
- Submit
-
- `
- : this._status === "timed_out"
- ? html`
- Timed out!
-
- We have not found any device in inclusion mode. Make
- sure the device is active and in inclusion mode.
-
-
- Retry
-
- `
- : this._status === "started_specific"
- ? html`
- ${this.hass.localize(
- "ui.panel.config.zwave_js.add_node.searching_device"
- )}
-
-
-
- ${this.hass.localize(
- "ui.panel.config.zwave_js.add_node.follow_device_instructions"
- )}
-
`
- : this._status === "started"
- ? html`
-
-
-
- ${this.hass.localize(
- "ui.panel.config.zwave_js.add_node.searching_device"
- )}
-
-
-
- ${this.hass.localize(
- "ui.panel.config.zwave_js.add_node.follow_device_instructions"
- )}
-
-
-
- ${this.hass.localize(
- "ui.panel.config.zwave_js.add_node.choose_inclusion_strategy"
- )}
-
-
-
- ${this._supportsSmartStart
- ? html`
-
- ${this.hass.localize(
- "ui.panel.config.zwave_js.add_node.qr_code"
- )}
-
-
-
- ${this.hass.localize(
- "ui.panel.config.zwave_js.add_node.qr_code_paragraph"
- )}
-
-
-
- ${this.hass.localize(
- "ui.panel.config.zwave_js.add_node.scan_qr_code"
- )}
-
-
-
`
- : ""}
-
-
- ${this.hass.localize("ui.common.cancel")}
-
- `
- : this._status === "interviewing"
- ? html`
-
-
-
-
- ${this.hass.localize(
- "ui.panel.config.zwave_js.add_node.interview_started"
- )}
-
- ${this._lowSecurity
- ? html`
- ${this.hass.localize(
- "ui.panel.config.zwave_js.add_node.added_insecurely_text"
- )}
- ${typeof this._lowSecurityReason !==
- "undefined"
- ? html`
- ${this.hass.localize(
- `ui.panel.config.zwave_js.add_node.low_security_reason.${this._lowSecurityReason}`
- )}
-
`
- : ""}
- `
- : ""}
- ${this._stages
- ? html`
- ${this._stages.map(
- (stage) => html`
-
-
- ${stage}
-
- `
- )}
-
`
- : ""}
-
-
-
- ${this.hass.localize("ui.common.close")}
-
- `
- : this._status === "failed"
- ? html`
-
-
-
- ${this._error ||
- this.hass.localize(
- "ui.panel.config.zwave_js.add_node.check_logs"
- )}
-
- ${this._stages
- ? html`
- ${this._stages.map(
- (stage) => html`
-
-
- ${stage}
-
- `
- )}
-
`
- : ""}
-
-
-
- ${this.hass.localize("ui.common.close")}
-
- `
- : this._status === "finished"
- ? html`
-
-
-
-
- ${this.hass.localize(
- "ui.panel.config.zwave_js.add_node.inclusion_finished"
- )}
-
- ${this._lowSecurity
- ? html`
- ${this.hass.localize(
- "ui.panel.config.zwave_js.add_node.added_insecurely_text"
- )}
- ${typeof this
- ._lowSecurityReason !==
- "undefined"
- ? html`
- ${this.hass.localize(
- `ui.panel.config.zwave_js.add_node.low_security_reason.${this._lowSecurityReason}`
- )}
-
`
- : nothing}
- `
- : ""}
-
-
- ${this.hass.localize(
- "ui.panel.config.zwave_js.add_node.view_device"
- )}
-
-
- ${this._stages
- ? html`
- ${this._stages.map(
- (stage) => html`
-
-
- ${stage}
-
- `
- )}
-
`
- : ""}
-
-
-
- ${this.hass.localize("ui.common.close")}
-
- `
- : this._status === "provisioned"
- ? html`
-
-
-
- ${this.hass.localize(
- "ui.panel.config.zwave_js.add_node.provisioning_finished"
- )}
-
-
-
-
- ${this.hass.localize("ui.common.close")}
- `
- : ""}
-
- `;
- }
-
- private _shouldPreventClose(): boolean {
- return (
- this._status === "started_specific" ||
- this._status === "validate_dsk_enter_pin" ||
- this._status === "grant_security_classes" ||
- this._status === "waiting_for_device"
- );
- }
-
- private _chooseInclusionStrategy(): void {
- this._unsubscribe();
- this._status = "choose_strategy";
- }
-
- private _handleStrategyChange(ev: CustomEvent): void {
- this._inclusionStrategy = (ev.target as any).value;
- }
-
- private _handleSecurityClassChange(ev: CustomEvent): void {
- const checkbox = ev.currentTarget as HaCheckbox;
- const securityClass = Number(checkbox.value);
- if (checkbox.checked && !this._securityClasses.includes(securityClass)) {
- this._securityClasses = [...this._securityClasses, securityClass];
- } else if (!checkbox.checked) {
- this._securityClasses = this._securityClasses.filter(
- (val) => val !== securityClass
- );
- }
- }
-
- private async _scanQRCode(): Promise {
- this._unsubscribe();
- this._status = "qr_scan";
- }
-
- private _qrCodeScanned(ev: CustomEvent): void {
- if (this._qrProcessing) {
- return;
- }
- this._handleQrCodeScanned(ev.detail.value);
- }
-
- private _qrCodeError(ev: CustomEvent): void {
- this._error = ev.detail.message;
- }
-
- private async _handleQrCodeScanned(qrCodeString: string): Promise {
- this._error = undefined;
- if (this._status !== "qr_scan" || this._qrProcessing) {
- return;
- }
- this._qrProcessing = true;
- const dsk = await zwaveTryParseDskFromQrCode(
- this.hass,
- this._entryId!,
- qrCodeString
- );
- if (dsk) {
- this._status = "loading";
- // wait for QR scanner to be removed before resetting qr processing
- this.updateComplete.then(() => {
- this._qrProcessing = false;
- });
- this._inclusionStrategy = InclusionStrategy.Security_S2;
- this._startInclusion(undefined, dsk);
- return;
- }
- if (
- qrCodeString.length < MINIMUM_QR_STRING_LENGTH ||
- !qrCodeString.startsWith("90")
- ) {
- this._qrProcessing = false;
- this._error = `Invalid QR code (${qrCodeString})`;
- return;
- }
- let provisioningInfo: QRProvisioningInformation;
- try {
- provisioningInfo = await zwaveParseQrCode(
- this.hass,
- this._entryId!,
- qrCodeString
- );
- } catch (err: any) {
- this._qrProcessing = false;
- this._error = err.message;
- return;
- }
- this._status = "loading";
- // wait for QR scanner to be removed before resetting qr processing
- this.updateComplete.then(() => {
- this._qrProcessing = false;
- });
- if (provisioningInfo.version === 1) {
- try {
- await provisionZwaveSmartStartNode(
- this.hass,
- this._entryId!,
- provisioningInfo
- );
- this._status = "provisioned";
- } catch (err: any) {
- this._error = err.message;
- this._status = "failed";
- }
- } else if (provisioningInfo.version === 0) {
- this._inclusionStrategy = InclusionStrategy.Security_S2;
- this._startInclusion(provisioningInfo);
- } else {
- this._error = "This QR code is not supported";
- this._status = "failed";
- }
- }
-
- private _handlePinKeyUp(ev: KeyboardEvent) {
- if (ev.key === "Enter") {
- this._validateDskAndEnterPin();
- }
- }
-
- private async _validateDskAndEnterPin(): Promise {
- this._status = "waiting_for_device";
- this._error = undefined;
- try {
- await zwaveValidateDskAndEnterPin(
- this.hass,
- this._entryId!,
- this._pinInput!.value as string
- );
- } catch (err: any) {
- this._error = err.message;
- this._status = "validate_dsk_enter_pin";
- await this.updateComplete;
- this._pinInput?.focus();
- }
- }
-
- private async _grantSecurityClasses(): Promise {
- this._status = "waiting_for_device";
- this._error = undefined;
- try {
- await zwaveGrantSecurityClasses(
- this.hass,
- this._entryId!,
- this._securityClasses
- );
- } catch (err: any) {
- this._error = err.message;
- this._status = "grant_security_classes";
- }
- }
-
- private _startManualInclusion() {
- if (!this._inclusionStrategy) {
- this._inclusionStrategy = InclusionStrategy.Default;
- }
- this._startInclusion();
- }
-
- private async _checkSmartStartSupport() {
- this._supportsSmartStart = (
- await zwaveSupportsFeature(
- this.hass,
- this._entryId!,
- ZWaveFeature.SmartStart
- )
- ).supported;
- }
-
- private _startOver(_ev: Event) {
- this._startInclusion();
- }
-
- private _startInclusion(
- qrProvisioningInformation?: QRProvisioningInformation,
- dsk?: string
- ): void {
- if (!this.hass) {
- return;
- }
- this._lowSecurity = false;
- const specificDevice = qrProvisioningInformation || dsk;
- this._subscribed = subscribeAddZwaveNode(
- this.hass,
- this._entryId!,
- (message) => {
- if (message.event === "inclusion started") {
- this._status = specificDevice ? "started_specific" : "started";
- }
- if (message.event === "inclusion failed") {
- this._unsubscribe();
- this._status = "failed";
- }
- if (message.event === "inclusion stopped") {
- // We either found a device, or it failed, either way, cancel the timeout as we are no longer searching
- if (this._addNodeTimeoutHandle) {
- clearTimeout(this._addNodeTimeoutHandle);
- }
- this._addNodeTimeoutHandle = undefined;
- }
-
- if (message.event === "node found") {
- // The user may have to enter a PIN. Until then prevent accidentally
- // closing the dialog
- this._status = "waiting_for_device";
- }
-
- if (message.event === "validate dsk and enter pin") {
- this._status = "validate_dsk_enter_pin";
- this._dsk = message.dsk;
- }
-
- if (message.event === "grant security classes") {
- if (this._inclusionStrategy === undefined) {
- zwaveGrantSecurityClasses(
- this.hass,
- this._entryId!,
- message.requested_grant.securityClasses,
- message.requested_grant.clientSideAuth
- );
- return;
- }
- this._requestedGrant = message.requested_grant;
- this._securityClasses = message.requested_grant.securityClasses;
- this._status = "grant_security_classes";
- }
-
- if (message.event === "device registered") {
- this._device = message.device;
- }
- if (message.event === "node added") {
- this._status = "interviewing";
- this._lowSecurity = message.node.low_security;
- this._lowSecurityReason = message.node.low_security_reason;
- }
-
- if (message.event === "interview completed") {
- this._unsubscribe();
- this._status = "finished";
- }
-
- if (message.event === "interview stage completed") {
- if (this._stages === undefined) {
- this._stages = [message.stage];
- } else {
- this._stages = [...this._stages, message.stage];
- }
- }
- },
- qrProvisioningInformation,
- undefined,
- undefined,
- dsk,
- this._inclusionStrategy
- ).catch((err) => {
- this._error = err.message;
- this._status = "failed";
- return undefined;
- });
- this._addNodeTimeoutHandle = window.setTimeout(() => {
- this._unsubscribe();
- this._status = "timed_out";
- }, 300000);
- }
-
- private _onBeforeUnload = (event: BeforeUnloadEvent) => {
- if (this._shouldPreventClose()) {
- event.preventDefault();
- }
- event.returnValue = true;
- };
-
- private _unsubscribe(): void {
- if (this._subscribed) {
- this._subscribed.then((unsub) => unsub && unsub());
- this._subscribed = undefined;
- }
- if (this._entryId) {
- stopZwaveInclusion(this.hass, this._entryId);
- if (
- this._status &&
- [
- "waiting_for_device",
- "validate_dsk_enter_pin",
- "grant_security_classes",
- ].includes(this._status)
- ) {
- cancelSecureBootstrapS2(this.hass, this._entryId);
- }
- if (this._params?.onStop) {
- this._params.onStop();
- }
- }
- this._requestedGrant = undefined;
- this._dsk = undefined;
- this._securityClasses = [];
- this._status = undefined;
- if (this._addNodeTimeoutHandle) {
- clearTimeout(this._addNodeTimeoutHandle);
- }
- this._addNodeTimeoutHandle = undefined;
- window.removeEventListener("beforeunload", this._onBeforeUnload);
- }
-
- public closeDialog(): void {
- this._unsubscribe();
- this._inclusionStrategy = undefined;
- this._entryId = undefined;
- this._status = undefined;
- this._device = undefined;
- this._stages = undefined;
- this._error = undefined;
- fireEvent(this, "dialog-closed", { dialog: this.localName });
- }
-
- static get styles(): CSSResultGroup {
- return [
- haStyleDialog,
- haStyle,
- css`
- h3 {
- margin-top: 0;
- }
-
- .success {
- color: var(--success-color);
- }
-
- .warning {
- color: var(--warning-color);
- }
-
- .stages {
- margin-top: 16px;
- display: grid;
- }
-
- .flex-container .stage ha-svg-icon {
- width: 16px;
- height: 16px;
- margin-right: 0px;
- margin-inline-end: 0px;
- margin-inline-start: initial;
- }
- .stage {
- padding: 8px;
- }
-
- .flex-container {
- display: flex;
- align-items: center;
- }
-
- .flex-column {
- display: flex;
- flex-direction: column;
- }
-
- .flex-column ha-formfield {
- padding: 8px 0;
- }
-
- .select-inclusion {
- display: flex;
- align-items: center;
- }
-
- .select-inclusion .outline:nth-child(2) {
- margin-left: 16px;
- margin-inline-start: 16px;
- margin-inline-end: initial;
- }
-
- .select-inclusion .outline {
- border: 1px solid var(--divider-color);
- border-radius: 4px;
- padding: 16px;
- min-height: 250px;
- text-align: center;
- flex: 1;
- }
-
- @media all and (max-width: 500px) {
- .select-inclusion {
- flex-direction: column;
- }
-
- .select-inclusion .outline:nth-child(2) {
- margin-left: 0;
- margin-inline-start: 0;
- margin-inline-end: initial;
- margin-top: 16px;
- }
- }
-
- ha-svg-icon {
- width: 68px;
- height: 48px;
- }
- ha-textfield {
- display: block;
- }
- .secondary {
- color: var(--secondary-text-color);
- }
-
- .flex-container ha-spinner,
- .flex-container ha-svg-icon {
- margin-right: 20px;
- margin-inline-end: 20px;
- margin-inline-start: initial;
- }
-
- .status {
- flex: 1;
- }
- `,
- ];
- }
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- "dialog-zwave_js-add-node": DialogZWaveJSAddNode;
- }
-}
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts
index 27190a1622..d6edc2aea5 100644
--- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts
+++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts
@@ -49,7 +49,7 @@ import "../../../../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
-import { showZWaveJSAddNodeDialog } from "./show-dialog-zwave_js-add-node";
+import { showZWaveJSAddNodeDialog } from "./add-node/show-dialog-zwave_js-add-node";
import { showZWaveJSRebuildNetworkRoutesDialog } from "./show-dialog-zwave_js-rebuild-network-routes";
import { showZWaveJSRemoveNodeDialog } from "./show-dialog-zwave_js-remove-node";
import { configTabs } from "./zwave_js-config-router";
@@ -101,7 +101,7 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
const inclusion_state = this._network?.controller.inclusion_state;
// show dialog if inclusion/exclusion is already in progress
if (inclusion_state === InclusionState.Including) {
- this._addNodeClicked();
+ this._openInclusionDialog(undefined, true);
} else if (inclusion_state === InclusionState.Excluding) {
this._removeNodeClicked();
}
@@ -743,7 +743,7 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
input.value = "";
}
- private _openInclusionDialog(dsk?: string) {
+ private _openInclusionDialog(dsk?: string, inclusionOngoing = false) {
if (!this._dialogOpen) {
// Unsubscribe from S2 inclusion before opening dialog
if (this._s2InclusionUnsubscribe) {
@@ -755,6 +755,8 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
entry_id: this.configEntryId!,
dsk,
onStop: this._handleInclusionDialogClosed,
+ longRangeSupported: !!this._network?.controller?.supports_long_range,
+ inclusionOngoing,
});
this._dialogOpen = true;
}
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-router.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-router.ts
index 28e1110e46..83cac24c77 100644
--- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-router.ts
+++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-router.ts
@@ -42,7 +42,7 @@ class ZWaveJSConfigRouter extends HassRouterPage {
},
add: {
tag: "zwave_js-add-node",
- load: () => import("./zwave_js-add-node"),
+ load: () => import("./add-node/zwave_js-add-node"),
},
node_config: {
tag: "zwave_js-node-config",
diff --git a/src/translations/en.json b/src/translations/en.json
index 6e9e10bff6..b99f043ce4 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -1106,7 +1106,15 @@
"only_https_supported": "You can only use your camera to scan a QR code when using HTTPS.",
"not_supported": "Your browser doesn't support QR scanning.",
"manual_input": "You can scan the QR code with another QR scanner and paste the code in the input below",
- "enter_qr_code": "Enter QR code value"
+ "enter_qr_code": "Enter QR code value",
+ "retry": "Retry",
+ "wrong_code": "Wrong barcode scanned! {format}: {rawValue}, we need a QR code.",
+ "no_camera_found": "No camera found",
+ "app": {
+ "title": "Scan QR code",
+ "description": "Find the code on your device",
+ "alternativeOptionLabel": "More options"
+ }
},
"climate-control": {
"temperature_up": "Increase temperature",
@@ -5876,33 +5884,91 @@
},
"add_node": {
"title": "Add a Z-Wave device",
- "searching_device": "Searching for devices…",
- "follow_device_instructions": "Follow the directions that came with your device to trigger pairing on the device.",
+ "searching_devices": "Searching for devices",
+ "follow_device_instructions": "Follow the directions provided with your device to put it into pairing mode (sometimes called \"inclusion mode\")",
+ "security_options": "Advanced security options",
"choose_inclusion_strategy": "How do you want to add your device",
- "qr_code": "QR Code",
- "qr_code_paragraph": "If your device supports SmartStart you can scan the QR code for easy pairing.",
- "scan_qr_code": "Scan QR code",
+ "add_device_failed": "Add device failed",
"inclusion_failed": "The device could not be added.",
+ "getting_device_information": "Getting device information",
+ "saving_device": "Saving device",
"check_logs": "Please check the logs for more information.",
- "inclusion_finished": "The device has been added.",
- "provisioning_finished": "The device has been added. Once you power it on, it will become available.",
- "view_device": "View Device",
- "interview_started": "The device is being interviewed. This may take some time.",
- "interview_failed": "The device interview failed. Additional information may be available in the logs.",
- "waiting_for_device": "Communicating with the device. Please wait.",
- "adding_insecurely": "The device is being added insecurely",
- "added_insecurely": "The device was added insecurely",
- "added_insecurely_text": "There was an error during secure inclusion. You can try again by excluding the device and adding it again.",
- "low_security_reason": {
- "0": "Security bootstrapping was canceled by the user.",
- "1": "The required security keys were not configured in the driver.",
- "2": "No Security S2 user callbacks (or provisioning info) were provided to grant security classes and/or validate the DSK.",
- "3": "An expected message was not received within the corresponding timeout.",
- "4": "There was no possible match in encryption parameters between the controller and the node.",
- "5": "Security bootstrapping was canceled by the included node.",
- "6": "The PIN was incorrect, so the included node could not decode the key exchange commands.",
- "7": "There was a mismatch in security keys between the controller and the node.",
- "8": "Unknown error occurred."
+ "timeout_error": "No device found after {minutes} minutes. Please check the device and try again.",
+ "select_method": {
+ "webcam_unsupported": "You can only use your camera to scan a QR code when using a secure connection with the app or over HTTPS.",
+ "qr_code_webcam": "Scan QR code using webcam",
+ "qr_code_webcam_description": "Find and scan the code on your device.",
+ "qr_code_manual": "Enter QR code manually",
+ "qr_code_manual_description": "You can scan the QR code with another QR scanner and paste the code.",
+ "search_device": "Search for device",
+ "search_device_description": "Use this option to manually include a device that does not have a SmartStart QR code."
+ },
+ "qr": {
+ "manual": {
+ "title": "Enter QR code manually",
+ "text": "You can scan the QR code with another QR scanner and paste the code here.",
+ "placeholder": "Code"
+ },
+ "scan_code": "Scan QR code",
+ "other_add_options": "Other add options",
+ "invalid_code": "Invalid QR code ({code})",
+ "unsupported_code": "This QR code is not supported \"{code}\""
+ },
+ "specific_device": {
+ "title": "Searching for device",
+ "turn_on_device": "Turn on the device",
+ "turn_on_device_description": "If your device is already turned on, you might need to turn it off and on again. Consult your device manual if you are unsure.",
+ "add_another_z_wave_device": "Add another Z-Wave device",
+ "close_description": "Feel free to close this screen, as this process will continue in the background."
+ },
+ "select_strategy": {
+ "title": "Choose security strategy",
+ "default_label": "Secure if possible",
+ "default_description": "Requires user interaction during inclusion. Fast and secure with S2 when supported. Allows manually selecting which security keys to grant. Fallback to legacy S0 or no encryption when necessary.",
+ "s0_label": "Legacy Secure",
+ "s0_description": "Uses the older S0 security that is secure, but slow due to a lot of overhead. Allows securely including S2 capable devices which fail to be included with S2.",
+ "insecure_label": "Insecure",
+ "insecure_description": "Do not use encryption.",
+ "inclusion_strategy": "Inclusion strategy: {strategy}"
+ },
+ "configure_device": {
+ "title": "Add device",
+ "device_name": "Name",
+ "device_area": "Area",
+ "choose_network_type": "Choose network type",
+ "long_range_label": "Direct long range",
+ "long_range_description": "Direct connection to Home Assistant for extended coverage, without a mesh network.",
+ "mesh_label": "Mesh network",
+ "mesh_description": "Devices relay signals to each other, enhancing coverage and reliability.",
+ "add_device": "Add device",
+ "save_device_failed": "Saving new device info failed"
+ },
+ "validate_dsk_pin": {
+ "title": "Verify key",
+ "text": "Find the key on your device or manual and enter the 5-digit PIN.",
+ "placeholder": "PIN"
+ },
+ "added_insecure": {
+ "title": "Device added insecurely",
+ "text": "The device {name} has been added to your Z-Wave network.",
+ "added_insecurely_text": "{deviceName} has been added without secure inclusion. You can still use your device.",
+ "try_again_text": "You can try again by removing the device and adding it again.",
+ "view_device": "View device",
+ "low_security_reason": {
+ "0": "Security bootstrapping was canceled by the user.",
+ "1": "The required security keys were not configured in the driver.",
+ "2": "No Security S2 user callbacks (or provisioning info) were provided to grant security classes and/or validate the DSK.",
+ "3": "An expected message was not received within the corresponding timeout.",
+ "4": "There was no possible match in encryption parameters between the controller and the node.",
+ "5": "Security bootstrapping was canceled by the included node.",
+ "6": "The PIN was incorrect, so the included node could not decode the key exchange commands.",
+ "7": "There was a mismatch in security keys between the controller and the node.",
+ "8": "Unknown error occurred."
+ }
+ },
+ "grant_security_classes": {
+ "title": "Security classes",
+ "description": "The device has requested the following security classes"
}
},
"provisioned": {