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
This commit is contained in:
AlCalzone 2024-06-12 13:48:40 +02:00 committed by GitHub
parent c7b4e8f37c
commit 9a3f7df25e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 348 additions and 296 deletions

View File

@ -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`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.zwave_js.add_node.title")
)}
.heading=${preventClose
? heading
: createCloseHeading(this.hass, heading)}
scrimClickAction=${ifDefined(preventClose ? "" : undefined)}
escapeKeyAction=${ifDefined(preventClose ? "" : undefined)}
>
${this._status === "loading"
? html`<div style="display: flex; justify-content: center;">
@ -122,81 +137,93 @@ class DialogZWaveJSAddNode extends LitElement {
indeterminate
></ha-circular-progress>
</div>`
: this._status === "choose_strategy"
? html`<h3>Choose strategy</h3>
<div class="flex-column">
<ha-formfield
.label=${html`<b>Secure if possible</b>
<div class="secondary">
Requires user interaction during inclusion. Fast and
secure with S2 when supported. Fallback to legacy S0 or
no encryption when necessary.
</div>`}
>
<ha-radio
name="strategy"
@change=${this._handleStrategyChange}
.value=${InclusionStrategy.Default}
.checked=${this._inclusionStrategy ===
InclusionStrategy.Default ||
this._inclusionStrategy === undefined}
: this._status === "waiting_for_device"
? html`<div class="flex-container">
<ha-circular-progress indeterminate></ha-circular-progress>
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.waiting_for_device"
)}
</p>
</div>`
: this._status === "choose_strategy"
? html`<h3>Choose strategy</h3>
<div class="flex-column">
<ha-formfield
.label=${html`<b>Secure if possible</b>
<div class="secondary">
Requires user interaction during inclusion. Fast and
secure with S2 when supported. Fallback to legacy S0
or no encryption when necessary.
</div>`}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${html`<b>Legacy Secure</b>
<div class="secondary">
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.
</div>`}
>
<ha-radio
name="strategy"
@change=${this._handleStrategyChange}
.value=${InclusionStrategy.Security_S0}
.checked=${this._inclusionStrategy ===
InclusionStrategy.Security_S0}
<ha-radio
name="strategy"
@change=${this._handleStrategyChange}
.value=${InclusionStrategy.Default}
.checked=${this._inclusionStrategy ===
InclusionStrategy.Default ||
this._inclusionStrategy === undefined}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${html`<b>Legacy Secure</b>
<div class="secondary">
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.
</div>`}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${html`<b>Insecure</b>
<div class="secondary">Do not use encryption.</div>`}
>
<ha-radio
name="strategy"
@change=${this._handleStrategyChange}
.value=${InclusionStrategy.Insecure}
.checked=${this._inclusionStrategy ===
InclusionStrategy.Insecure}
<ha-radio
name="strategy"
@change=${this._handleStrategyChange}
.value=${InclusionStrategy.Security_S0}
.checked=${this._inclusionStrategy ===
InclusionStrategy.Security_S0}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${html`<b>Insecure</b>
<div class="secondary">Do not use encryption.</div>`}
>
</ha-radio>
</ha-formfield>
</div>
<mwc-button
slot="primaryAction"
@click=${this._startManualInclusion}
>
Search device
</mwc-button>`
: this._status === "qr_scan"
? html`${this._error
? html`<ha-alert alert-type="error"
>${this._error}</ha-alert
>`
: ""}
<ha-qr-scanner
.localize=${this.hass.localize}
@qr-code-scanned=${this._qrCodeScanned}
></ha-qr-scanner>
<mwc-button slot="secondaryAction" @click=${this._startOver}>
${this.hass.localize(
"ui.panel.config.zwave_js.common.back"
)}
<ha-radio
name="strategy"
@change=${this._handleStrategyChange}
.value=${InclusionStrategy.Insecure}
.checked=${this._inclusionStrategy ===
InclusionStrategy.Insecure}
>
</ha-radio>
</ha-formfield>
</div>
<mwc-button
slot="primaryAction"
@click=${this._startManualInclusion}
>
Search device
</mwc-button>`
: this._status === "validate_dsk_enter_pin"
? html`
: this._status === "qr_scan"
? html`${this._error
? html`<ha-alert alert-type="error"
>${this._error}</ha-alert
>`
: ""}
<ha-qr-scanner
.localize=${this.hass.localize}
@qr-code-scanned=${this._qrCodeScanned}
></ha-qr-scanner>
<mwc-button
slot="secondaryAction"
@click=${this._startOver}
>
${this.hass.localize(
"ui.panel.config.zwave_js.common.back"
)}
</mwc-button>`
: this._status === "validate_dsk_enter_pin"
? html`
<p>
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 {
</mwc-button>
</div>
`
: this._status === "grant_security_classes"
? html`
<h3>
The device has requested the following security classes:
</h3>
${this._error
? html`<ha-alert alert-type="error"
>${this._error}</ha-alert
>`
: ""}
<div class="flex-column">
${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`<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}
.checked=${this._securityClasses.includes(
securityClass
)}
>
</ha-checkbox>
</ha-formfield>`
)}
</div>
<mwc-button
slot="primaryAction"
.disabled=${!this._securityClasses.length}
@click=${this._grantSecurityClasses}
>
Submit
</mwc-button>
`
: this._status === "timed_out"
: this._status === "grant_security_classes"
? html`
<h3>Timed out!</h3>
<p>
We have not found any device in inclusion mode. Make
sure the device is active and in inclusion mode.
</p>
<mwc-button
slot="primaryAction"
@click=${this._startOver}
>
Retry
</mwc-button>
`
: this._status === "started_specific"
? html`<h3>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.searching_device"
)}
</h3>
<ha-circular-progress
indeterminate
></ha-circular-progress>
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
)}
</p>`
: this._status === "started"
? html`
<div class="select-inclusion">
<div class="outline">
<h2>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.searching_device"
)}
</h2>
<ha-circular-progress
indeterminate
></ha-circular-progress>
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
)}
</p>
<p>
<button
class="link"
@click=${this._chooseInclusionStrategy}
>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.choose_inclusion_strategy"
)}
</button>
</p>
</div>
${this._supportsSmartStart
? html` <div class="outline">
<h2>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.qr_code"
)}
</h2>
<ha-svg-icon
.path=${mdiQrcodeScan}
></ha-svg-icon>
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.qr_code_paragraph"
)}
</p>
<p>
<mwc-button @click=${this._scanQRCode}>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.scan_qr_code"
)}
</mwc-button>
</p>
</div>`
: ""}
</div>
<mwc-button
slot="primaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</mwc-button>
`
: this._status === "interviewing"
? html`
<div class="flex-container">
<ha-circular-progress
indeterminate
></ha-circular-progress>
<div class="status">
<p>
<b
<h3>
The device has requested the following security
classes:
</h3>
${this._error
? html`<ha-alert alert-type="error"
>${this._error}</ha-alert
>`
: ""}
<div class="flex-column">
${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`<ha-formfield
.label=${html`<b
>${this.hass.localize(
"ui.panel.config.zwave_js.add_node.interview_started"
`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}
.checked=${this._securityClasses.includes(
securityClass
)}
>
</ha-checkbox>
</ha-formfield>`
)}
</div>
<mwc-button
slot="primaryAction"
.disabled=${!this._securityClasses.length}
@click=${this._grantSecurityClasses}
>
Submit
</mwc-button>
`
: this._status === "timed_out"
? html`
<h3>Timed out!</h3>
<p>
We have not found any device in inclusion mode. Make
sure the device is active and in inclusion mode.
</p>
<mwc-button
slot="primaryAction"
@click=${this._startOver}
>
Retry
</mwc-button>
`
: this._status === "started_specific"
? html`<h3>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.searching_device"
)}
</h3>
<ha-circular-progress
indeterminate
></ha-circular-progress>
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
)}
</p>`
: this._status === "started"
? html`
<div class="select-inclusion">
<div class="outline">
<h2>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.searching_device"
)}
</h2>
<ha-circular-progress
indeterminate
></ha-circular-progress>
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
)}
</p>
<p>
<button
class="link"
@click=${this._chooseInclusionStrategy}
>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.choose_inclusion_strategy"
)}
</button>
</p>
${this._stages
? html` <div class="stages">
${this._stages.map(
(stage) => html`
<span class="stage">
<ha-svg-icon
.path=${mdiCheckCircle}
class="success"
></ha-svg-icon>
${stage}
</span>
`
)}
</div>`
: ""}
</div>
${this._supportsSmartStart
? html` <div class="outline">
<h2>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.qr_code"
)}
</h2>
<ha-svg-icon
.path=${mdiQrcodeScan}
></ha-svg-icon>
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.qr_code_paragraph"
)}
</p>
<p>
<mwc-button @click=${this._scanQRCode}>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.scan_qr_code"
)}
</mwc-button>
</p>
</div>`
: ""}
</div>
<mwc-button
slot="primaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.close")}
${this.hass.localize("ui.common.cancel")}
</mwc-button>
`
: this._status === "failed"
: this._status === "interviewing"
? html`
<div class="flex-container">
<ha-circular-progress
indeterminate
></ha-circular-progress>
<div class="status">
<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>
<p>
<b
>${this.hass.localize(
"ui.panel.config.zwave_js.add_node.interview_started"
)}</b
>
</p>
${this._stages
? html` <div class="stages">
${this._stages.map(
@ -441,45 +430,21 @@ class DialogZWaveJSAddNode extends LitElement {
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "finished"
: this._status === "failed"
? html`
<div class="flex-container">
<ha-svg-icon
.path=${this._lowSecurity
? mdiAlertCircle
: mdiCheckCircle}
class=${this._lowSecurity
? "warning"
: "success"}
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.inclusion_finished"
<ha-alert
alert-type="error"
.title=${this.hass.localize(
"ui.panel.config.zwave_js.add_node.inclusion_failed"
)}
</p>
${this._lowSecurity
? html`<ha-alert
alert-type="warning"
title="The device was added insecurely"
>
There was an error during secure
inclusion. You can try again by
excluding the device and adding it
again.
</ha-alert>`
: ""}
<a
href=${`/config/devices/device/${
this._device!.id
}`}
>
<mwc-button>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.view_device"
)}
</mwc-button>
</a>
${this._error ||
this.hass.localize(
"ui.panel.config.zwave_js.add_node.check_logs"
)}
</ha-alert>
${this._stages
? html` <div class="stages">
${this._stages.map(
@ -504,18 +469,60 @@ class DialogZWaveJSAddNode extends LitElement {
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "provisioned"
? html` <div class="flex-container">
: this._status === "finished"
? html`
<div class="flex-container">
<ha-svg-icon
.path=${mdiCheckCircle}
class="success"
.path=${this._lowSecurity
? mdiAlertCircle
: mdiCheckCircle}
class=${this._lowSecurity
? "warning"
: "success"}
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.provisioning_finished"
"ui.panel.config.zwave_js.add_node.inclusion_finished"
)}
</p>
${this._lowSecurity
? html`<ha-alert
alert-type="warning"
title="The device was added insecurely"
>
There was an error during secure
inclusion. You can try again by
excluding the device and adding it
again.
</ha-alert>`
: ""}
<a
href=${`/config/devices/device/${
this._device?.id
}`}
>
<mwc-button>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.view_device"
)}
</mwc-button>
</a>
${this._stages
? html` <div class="stages">
${this._stages.map(
(stage) => html`
<span class="stage">
<ha-svg-icon
.path=${mdiCheckCircle}
class="success"
></ha-svg-icon>
${stage}
</span>
`
)}
</div>`
: ""}
</div>
</div>
<mwc-button
@ -523,12 +530,42 @@ class DialogZWaveJSAddNode extends LitElement {
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.close")}
</mwc-button>`
: ""}
</mwc-button>
`
: this._status === "provisioned"
? html` <div class="flex-container">
<ha-svg-icon
.path=${mdiCheckCircle}
class="success"
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.provisioning_finished"
)}
</p>
</div>
</div>
<mwc-button
slot="primaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.close")}
</mwc-button>`
: ""}
</ha-dialog>
`;
}
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<void> {
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<void> {
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 {

View File

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