From 9a3f7df25ea378706aec612609ec7dfec4c7a701 Mon Sep 17 00:00:00 2001 From: AlCalzone Date: Wed, 12 Jun 2024 13:48:40 +0200 Subject: [PATCH] Z-Wave: Prevent closing the Add Device dialog when user input is required (#20999) * prevent closing the Z-Wave Add node dialog when user input is required * ask user for confirmation before leaving page during bootstrapping * fix: no non-null assertion --- .../zwave_js/dialog-zwave_js-add-node.ts | 641 ++++++++++-------- src/translations/en.json | 3 +- 2 files changed, 348 insertions(+), 296 deletions(-) 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 index 61ebc154a3..29c982bbfe 100644 --- 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 @@ -3,6 +3,7 @@ import { mdiAlertCircle, mdiCheckCircle, mdiQrcodeScan } from "@mdi/js"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, 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"; @@ -60,7 +61,8 @@ class DialogZWaveJSAddNode extends LitElement { | "finished" | "provisioned" | "validate_dsk_enter_pin" - | "grant_security_classes"; + | "grant_security_classes" + | "waiting_for_device"; @state() private _device?: ZWaveJSAddNodeDevice; @@ -86,6 +88,11 @@ class DialogZWaveJSAddNode extends LitElement { private _qrProcessing = false; + public connectedCallback(): void { + super.connectedCallback(); + window.addEventListener("beforeunload", this._onBeforeUnload); + } + public disconnectedCallback(): void { super.disconnectedCallback(); this._unsubscribe(); @@ -106,14 +113,22 @@ class DialogZWaveJSAddNode extends LitElement { 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`
@@ -122,81 +137,93 @@ class DialogZWaveJSAddNode extends LitElement { indeterminate >
` - : this._status === "choose_strategy" - ? html`

Choose strategy

-
- Secure if possible -
- Requires user interaction during inclusion. Fast and - secure with S2 when supported. Fallback to legacy S0 or - no encryption when necessary. -
`} - > - + +

+ ${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. 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. -
`} - > - + +
+ 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.
`} - > - + +
+ Insecure +
Do not use encryption.
`} > - -
-
- - Search device - ` - : this._status === "qr_scan" - ? html`${this._error - ? html`${this._error}` - : ""} - - - ${this.hass.localize( - "ui.panel.config.zwave_js.common.back" - )} + + + + + + Search device ` - : this._status === "validate_dsk_enter_pin" - ? html` + : this._status === "qr_scan" + ? html`${this._error + ? html`${this._error}` + : ""} + + + ${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 @@ -225,198 +252,160 @@ class DialogZWaveJSAddNode extends LitElement { ` - : 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" + : this._status === "grant_security_classes" ? 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._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` -
- -
-

- + 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.add_node.interview_started" + `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._stages - ? html`
- ${this._stages.map( - (stage) => html` - - - ${stage} - - ` - )} -
` - : ""}
+ ${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.close")} + ${this.hass.localize("ui.common.cancel")} ` - : this._status === "failed" + : this._status === "interviewing" ? html`
+
- - ${this._error || - this.hass.localize( - "ui.panel.config.zwave_js.add_node.check_logs" - )} - +

+ ${this.hass.localize( + "ui.panel.config.zwave_js.add_node.interview_started" + )} +

${this._stages ? html`
${this._stages.map( @@ -441,45 +430,21 @@ class DialogZWaveJSAddNode extends LitElement { ${this.hass.localize("ui.common.close")} ` - : this._status === "finished" + : this._status === "failed" ? html`
-
-

- ${this.hass.localize( - "ui.panel.config.zwave_js.add_node.inclusion_finished" + - ${this._lowSecurity - ? html` - There was an error during secure - inclusion. You can try again by - excluding the device and adding it - again. - ` - : ""} - - - ${this.hass.localize( - "ui.panel.config.zwave_js.add_node.view_device" - )} - - + ${this._error || + this.hass.localize( + "ui.panel.config.zwave_js.add_node.check_logs" + )} + ${this._stages ? html`

${this._stages.map( @@ -504,18 +469,60 @@ class DialogZWaveJSAddNode extends LitElement { ${this.hass.localize("ui.common.close")} ` - : this._status === "provisioned" - ? html`
+ : this._status === "finished" + ? html` +

${this.hass.localize( - "ui.panel.config.zwave_js.add_node.provisioning_finished" + "ui.panel.config.zwave_js.add_node.inclusion_finished" )}

+ ${this._lowSecurity + ? html` + There was an error during secure + inclusion. You can try again by + excluding the device and adding it + again. + ` + : ""} + + + ${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"; @@ -639,7 +676,7 @@ class DialogZWaveJSAddNode extends LitElement { } private async _validateDskAndEnterPin(): Promise { - this._status = "loading"; + this._status = "waiting_for_device"; this._error = undefined; try { await zwaveValidateDskAndEnterPin( @@ -656,7 +693,7 @@ class DialogZWaveJSAddNode extends LitElement { } private async _grantSecurityClasses(): Promise { - this._status = "loading"; + this._status = "waiting_for_device"; this._error = undefined; try { await zwaveGrantSecurityClasses( @@ -719,6 +756,12 @@ class DialogZWaveJSAddNode extends LitElement { 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; @@ -775,6 +818,13 @@ class DialogZWaveJSAddNode extends LitElement { }, 90000); } + private _onBeforeUnload = (event: BeforeUnloadEvent) => { + if (this._shouldPreventClose()) { + event.preventDefault(); + } + event.returnValue = true; + }; + private _unsubscribe(): void { if (this._subscribed) { this._subscribed.then((unsub) => unsub()); @@ -791,6 +841,7 @@ class DialogZWaveJSAddNode extends LitElement { clearTimeout(this._addNodeTimeoutHandle); } this._addNodeTimeoutHandle = undefined; + window.removeEventListener("beforeunload", this._onBeforeUnload); } public closeDialog(): void { diff --git a/src/translations/en.json b/src/translations/en.json index d5c1b5b00f..e6271ac6f4 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4863,7 +4863,8 @@ "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." + "interview_failed": "The device interview failed. Additional information may be available in the logs.", + "waiting_for_device": "Communicating with the device. Please wait." }, "provisioned": { "dsk": "DSK",