Implement new Z-Wave add device flow (#24667)

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
This commit is contained in:
Wendelin 2025-04-17 13:12:04 +02:00 committed by GitHub
parent c73a9fccb8
commit 933fb1327a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 2476 additions and 1105 deletions

View File

@ -0,0 +1,15 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M76.9105 39.4999C77.739 39.4999 78.4105 38.8283 78.4105 37.9999C78.4105 37.1715 77.739 36.4999 76.9105 36.4999V39.4999ZM37.5 39.4999L76.9105 39.4999V36.4999L37.5 36.4999L37.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M30.8239 22.3365L38.8239 38.8365L30.3239 50.3365" stroke="black" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/>
<mask id="mask0_1110_23734" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
<path d="M45.75 42.075C45.75 42.4462 45.4462 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/>
</mask>
<g mask="url(#mask0_1110_23734)">
<rect x="30" y="27" width="18" height="18" fill="#212121"/>
</g>
<path d="M82 37.9999C82 36.343 83.3431 34.9999 85 34.9999C86.6569 34.9999 88 36.343 88 37.9999C88 39.6567 86.6569 40.9999 85 40.9999C83.3431 40.9999 82 39.6567 82 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
<rect x="23" y="11" width="8" height="8" rx="4" fill="black" fill-opacity="0.32"/>
<rect x="22" y="52" width="8" height="8" rx="4" fill="black" fill-opacity="0.32"/>
<path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="black" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/>
<circle cx="39" cy="36" r="33.5" stroke="black" stroke-opacity="0.12"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,15 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M30.824 22.3365L38.824 38.8365L30.324 50.3365" stroke="white" stroke-opacity="0.24" stroke-width="3" stroke-linecap="round"/>
<mask id="mask0_1180_4955" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
<path d="M45.75 42.075C45.75 42.4462 45.4462 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/>
</mask>
<g mask="url(#mask0_1180_4955)">
<rect x="30" y="27" width="18" height="18" fill="#00AFFF"/>
</g>
<path d="M76.9105 39.4999C77.739 39.4999 78.4105 38.8283 78.4105 37.9999C78.4105 37.1715 77.739 36.4999 76.9105 36.4999V39.4999ZM37.5 39.4999L76.9105 39.4999V36.4999L37.5 36.4999L37.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M82 37.9999C82 36.343 83.3431 34.9999 85 34.9999C86.6569 34.9999 88 36.343 88 37.9999C88 39.6567 86.6569 40.9999 85 40.9999C83.3431 40.9999 82 39.6567 82 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
<rect x="23" y="11" width="8" height="8" rx="4" fill="white" fill-opacity="0.48"/>
<rect x="22" y="52" width="8" height="8" rx="4" fill="white" fill-opacity="0.48"/>
<path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="white" stroke-opacity="0.24" stroke-width="3" stroke-linecap="round"/>
<circle cx="39" cy="36" r="33.5" stroke="white" stroke-opacity="0.24"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,19 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M63.1358 38.5084C63.9608 38.4334 64.5688 37.7037 64.4938 36.8787C64.4188 36.0537 63.6892 35.4457 62.8642 35.5207L63.1358 38.5084ZM46.6358 40.0084L63.1358 38.5084L62.8642 35.5207L46.3642 37.0207L46.6358 40.0084Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<circle cx="47" cy="36" r="34" fill="white"/>
<circle cx="47" cy="36" r="33.5" stroke="black" stroke-opacity="0.12"/>
<path d="M41.8777 12.5216C43.4905 12.1798 45.1631 12 46.8777 12C58.2401 12 67.7582 19.8959 70.2445 30.5M40 59C42.1788 59.6506 44.4874 60 46.8777 60C56.9498 60 65.5728 53.7955 69.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<mask id="mask0_1110_23775" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="38" y="27" width="18" height="18">
<path d="M53.75 42.075C53.75 42.4462 53.4462 42.75 53.075 42.75H40.925C40.5538 42.75 40.25 42.4462 40.25 42.075V36.675C40.25 36.3037 40.4649 35.7851 40.7276 35.5224L46.5224 29.7275C46.7851 29.4649 47.2143 29.4649 47.477 29.7275L53.2724 35.523C53.5351 35.7857 53.75 36.3043 53.75 36.6755V42.075Z" fill="black"/>
</mask>
<g mask="url(#mask0_1110_23775)">
<rect x="38" y="27" width="18" height="18" fill="#212121"/>
</g>
<path d="M63.5 39.4999C64.3284 39.4999 65 38.8283 65 37.9999C65 37.1715 64.3284 36.4999 63.5 36.4999L63.5 39.4999ZM49.5 39.4999L63.5 39.4999L63.5 36.4999L49.5 36.4999L49.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
<rect x="31" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<rect x="30" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<path d="M29.5715 19.5C25.4983 23.801 23 29.6087 23 36C23 41.9085 25.1351 47.3183 28.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M68 37.9999C68 36.343 69.3431 34.9999 71 34.9999C72.6569 34.9999 74 36.343 74 37.9999C74 39.6567 72.6569 40.9999 71 40.9999C69.3431 40.9999 68 39.6567 68 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,19 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M63.1358 38.5084C63.9608 38.4334 64.5688 37.7037 64.4938 36.8787C64.4188 36.0537 63.6892 35.4457 62.8642 35.5207L63.1358 38.5084ZM46.6358 40.0084L63.1358 38.5084L62.8642 35.5207L46.3642 37.0207L46.6358 40.0084Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<circle cx="47" cy="36" r="34" fill="#1C1C1C"/>
<circle cx="47" cy="36" r="33.5" stroke="white" stroke-opacity="0.24"/>
<path d="M41.8777 12.5216C43.4905 12.1798 45.1631 12 46.8777 12C58.2401 12 67.7582 19.8959 70.2445 30.5M40 59C42.1788 59.6506 44.4874 60 46.8777 60C56.9498 60 65.5728 53.7955 69.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<mask id="mask0_1180_4965" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="38" y="27" width="18" height="18">
<path d="M53.75 42.075C53.75 42.4462 53.4462 42.75 53.075 42.75H40.925C40.5538 42.75 40.25 42.4462 40.25 42.075V36.675C40.25 36.3037 40.4649 35.7851 40.7276 35.5224L46.5224 29.7275C46.7851 29.4649 47.2143 29.4649 47.477 29.7275L53.2724 35.523C53.5351 35.7857 53.75 36.3043 53.75 36.6755V42.075Z" fill="black"/>
</mask>
<g mask="url(#mask0_1180_4965)">
<rect x="38" y="27" width="18" height="18" fill="#00AFFF"/>
</g>
<path d="M63.5 39.4999C64.3284 39.4999 65 38.8283 65 37.9999C65 37.1715 64.3284 36.4999 63.5 36.4999L63.5 39.4999ZM49.5 39.4999L63.5 39.4999L63.5 36.4999L49.5 36.4999L49.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
<rect x="31" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<rect x="30" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<path d="M29.5715 19.5C25.4983 23.801 23 29.6087 23 36C23 41.9085 25.1351 47.3183 28.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M68 37.9999C68 36.343 69.3431 34.9999 71 34.9999C72.6569 34.9999 74 36.343 74 37.9999C74 39.6567 72.6569 40.9999 71 40.9999C69.3431 40.9999 68 39.6567 68 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -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";

View File

@ -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`<ha-alert alert-type="error">${this.error}</ha-alert>`
: ""}
${navigator.mediaDevices && !this._manual
return html`${this._error || this._warning
? html`<ha-alert
.alertType=${this._error ? "error" : "warning"}
class=${this._error ? "" : "warning"}
>
${this._error || this._warning}
${this._error
? html` <ha-button @click=${this._retry} slot="action">
${this.hass.localize("ui.components.qr-scanner.retry")}
</ha-button>`
: nothing}
</ha-alert>`
: nothing}
${navigator.mediaDevices
? html`<video></video>
<div id="canvas-container">
${this._cameras && this._cameras.length > 1
${this._loading
? html`<div class="loading">
<ha-spinner active></ha-spinner>
</div>`
: nothing}
${!this._loading &&
!this._error &&
this._cameras &&
this._cameras.length > 1
? html`<ha-button-menu fixed @closed=${stopPropagation}>
<ha-icon-button
slot="trigger"
.label=${this.localize(
.label=${this.hass.localize(
"ui.components.qr-scanner.select_camera"
)}
.path=${mdiCamera}
@ -128,25 +142,25 @@ class HaQrScanner extends LitElement {
</ha-button-menu>`
: nothing}
</div>`
: html`${this._manual
? nothing
: html`<ha-alert alert-type="warning">
${!window.isSecureContext
? this.localize(
? this.hass.localize(
"ui.components.qr-scanner.only_https_supported"
)
: this.localize("ui.components.qr-scanner.not_supported")}
</ha-alert>`}
<p>${this.localize("ui.components.qr-scanner.manual_input")}</p>
: this.hass.localize("ui.components.qr-scanner.not_supported")}
</ha-alert>
<p>${this.hass.localize("ui.components.qr-scanner.manual_input")}</p>
<div class="row">
<ha-textfield
.label=${this.localize("ui.components.qr-scanner.enter_qr_code")}
.label=${this.hass.localize(
"ui.components.qr-scanner.enter_qr_code"
)}
@keyup=${this._manualKeyup}
@paste=${this._manualPaste}
></ha-textfield>
<mwc-button @click=${this._manualSubmit}>
${this.localize("ui.common.submit")}
</mwc-button>
<ha-button @click=${this._manualSubmit}>
${this.hass.localize("ui.common.submit")}
</ha-button>
</div>`}`;
}
@ -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 {

View File

@ -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<DeviceConfig> =>
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<QRProvisioningInformation> =>
protocol?: Protocols,
device_name?: string,
area_id?: string
): Promise<string> =>
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<UnsubscribeFunc> =>
hass.connection.subscribeMessage((message) => callbackFunction(message), {
type: "zwave_js/subscribe_new_devices",
entry_id: entry_id,
});
export const fetchZwaveNodeStatus = (
hass: HomeAssistant,
device_id: string

View File

@ -0,0 +1,54 @@
import type { QRProvisioningInformation } from "../../../../../../data/zwave_js";
export const backButtonStages: Partial<ZWaveJSAddNodeStage>[] = [
"qr_scan",
"select_other_method",
"qr_code_input",
"choose_security_strategy",
"configure_device",
];
export const closeButtonStages: Partial<ZWaveJSAddNodeStage>[] = [
"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;
}

View File

@ -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;
}

View File

@ -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`
<sl-animation name="zoomIn" .iterations=${1} play>
<ha-svg-icon .path=${mdiCheckCircleOutline}></ha-svg-icon>
</sl-animation>
<ha-alert alert-type="warning">
${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`<b>${this.deviceName}</b>`,
}
)}
<p>
${this.hass.localize(
`ui.panel.config.zwave_js.add_node.added_insecure.try_again_text`
)}
</p>
</ha-alert>
`;
}
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;
}
}

View File

@ -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`
<p>${this.description}</p>
${this.error
? html`<ha-alert alert-type="error">${this.error}</ha-alert>`
: nothing}
<ha-textfield
.placeholder=${this.placeholder}
.value=${this.value}
@input=${this._handleChange}
@keyup=${this._handleKeyup}
required
autofocus
></ha-textfield>
${this.referenceKey
? html`<div>
<span>${this.value.padEnd(5, "·")}</span>${this.referenceKey}
</div> `
: 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";
}
}

View File

@ -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`
<ha-form
.hass=${this.hass}
.schema=${this._getSchema(this.hass.localize, this.longRangeSupported)}
.data=${this._options!}
@value-changed=${this._setOptions}
.computeLabel=${this._computeLabel}
>
</ha-form>
`;
}
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;
}
}

View File

@ -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`
<ha-alert
alert-type="error"
.title=${this.hass.localize(
"ui.panel.config.zwave_js.add_node.inclusion_failed"
)}
>
${this.error ||
this.hass.localize("ui.panel.config.zwave_js.add_node.check_logs")}
</ha-alert>
${this.error
? html`<div class="note">
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.check_logs"
)}
</div>`
: nothing}
${this.device?.id
? html`<a href=${`/config/devices/device/${this.device.id}`}>
<ha-button>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.view_device"
)}
</ha-button>
</a>`
: 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;
}
}

View File

@ -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`<ha-alert alert-type="error"> ${this.error} </ha-alert>`
: nothing}
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.grant_security_classes.description"
)}
</p>
<div class="flex-column">
${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`<ha-formfield
.label=${html`<b
>${this.hass.localize(
`ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.title`
)}</b
>
<div class="secondary">
${this.hass.localize(
`ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.description`
)}
</div>`}
>
<ha-checkbox
@change=${this._handleSecurityClassChange}
.value=${securityClass.toString()}
.checked=${this.selectedSecurityClasses.includes(
securityClass
)}
>
</ha-checkbox>
</ha-formfield>`
)}
</div>
`;
}
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;
}
}

View File

@ -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`
<ha-fade-in .delay=${this.delay}>
<div class="loading">
<ha-spinner size="large"></ha-spinner>
</div>
${this.description ? html`<p>${this.description}</p>` : nothing}
</ha-fade-in>
`;
}
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;
}
}

View File

@ -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`
<div class="searching-devices">
<div class="searching-spinner">
<div class="spinner">
<ha-spinner></ha-spinner>
</div>
<sl-animation name="pulse" easing="linear" .duration=${2000} play>
<div class="circle"></div>
</sl-animation>
</div>
${this.smartStart
? html`<ha-alert
.title=${this.hass.localize(
"ui.panel.config.zwave_js.add_node.specific_device.turn_on_device"
)}
>
<ha-svg-icon slot="icon" .path=${mdiRestart}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.specific_device.turn_on_device_description"
)}
</ha-alert>
<p class="note">
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.specific_device.close_description"
)}
</p>`
: html`
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
)}
</p>
`}
${this.showSecurityOptions && !inclusionStrategyTranslationKey
? html`<ha-button @click=${this._handleSecurityOptions}>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.security_options"
)}
</ha-button>`
: inclusionStrategyTranslationKey
? html`<span class="note">
${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`
),
}
)}
</span>`
: nothing}
${this.showAddAnotherDevice
? html`<ha-button @click=${this._handleAddAnotherDevice}>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.specific_device.add_another_z_wave_device"
)}
</ha-button>`
: nothing}
</div>
`;
}
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;
}
}

View File

@ -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`<ha-alert alert-type="warning">
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.select_method.webcam_unsupported"
)}</ha-alert
>`
: nothing}
<ha-md-list>
${!this.hideQrWebcam
? html`<ha-md-list-item
interactive
type="button"
@click=${this._selectMethod}
.value=${"qr_code_webcam"}
.disabled=${!window.isSecureContext}
>
<div slot="headline">
${this.hass.localize(
`ui.panel.config.zwave_js.add_node.select_method.qr_code_webcam`
)}
</div>
<div slot="supporting-text">
${this.hass.localize(
`ui.panel.config.zwave_js.add_node.select_method.qr_code_webcam_description`
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>`
: nothing}
<ha-md-list-item
interactive
type="button"
@click=${this._selectMethod}
.value=${"qr_code_manual"}
>
<div slot="headline">
${this.hass.localize(
`ui.panel.config.zwave_js.add_node.select_method.qr_code_manual`
)}
</div>
<div slot="supporting-text">
${this.hass.localize(
`ui.panel.config.zwave_js.add_node.select_method.qr_code_manual_description`
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
<ha-md-list-item
interactive
type="button"
@click=${this._selectMethod}
.value=${"search_device"}
>
<div slot="headline">
${this.hass.localize(
`ui.panel.config.zwave_js.add_node.select_method.search_device`
)}
</div>
<div slot="supporting-text">
${this.hass.localize(
`ui.panel.config.zwave_js.add_node.select_method.search_device_description`
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
</ha-md-list>
`;
}
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";
};
}
}

View File

@ -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`
<ha-form
.schema=${this._getSchema(this.hass.localize)}
.data=${{ strategy: this._inclusionStrategy?.toString() }}
@value-changed=${this._selectStrategy}
.computeLabel=${this._computeLabel}
>
</ha-form>
`;
}
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;
};
}
}

View File

@ -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,
});

View File

@ -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;
}

View File

@ -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",

View File

@ -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,23 +5884,76 @@
},
"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.",
"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.",
@ -5905,6 +5966,11 @@
"8": "Unknown error occurred."
}
},
"grant_security_classes": {
"title": "Security classes",
"description": "The device has requested the following security classes"
}
},
"provisioned": {
"dsk": "DSK",
"security_classes": "Security classes",