1
0
mirror of https://github.com/home-assistant/frontend.git synced 2025-12-25 05:06:18 +00:00
Files
frontend/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-add-node.ts
renovate[bot] 15395275ba Update dependency prettier to v3.1.0 (#18675)
* Update dependency prettier to v3.1.0

* Reformat

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2023-11-19 23:31:56 -05:00

896 lines
32 KiB
TypeScript

import "@material/mwc-button/mwc-button";
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 { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-alert";
import { HaCheckbox } from "../../../../../components/ha-checkbox";
import "../../../../../components/ha-circular-progress";
import { createCloseHeading } from "../../../../../components/ha-dialog";
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 {
InclusionStrategy,
MINIMUM_QR_STRING_LENGTH,
provisionZwaveSmartStartNode,
QRProvisioningInformation,
RequestedGrant,
SecurityClass,
stopZwaveInclusion,
subscribeAddZwaveNode,
ZWaveFeature,
zwaveGrantSecurityClasses,
zwaveParseQrCode,
zwaveSupportsFeature,
zwaveTryParseDskFromQrCode,
zwaveValidateDskAndEnterPin,
} from "../../../../../data/zwave_js";
import { haStyle, haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import { 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";
@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 _supportsSmartStart?: boolean;
private _addNodeTimeoutHandle?: number;
private _subscribed?: Promise<UnsubscribeFunc>;
private _qrProcessing = false;
public disconnectedCallback(): void {
super.disconnectedCallback();
this._unsubscribe();
}
public async showDialog(params: ZWaveJSAddNodeDialogParams): Promise<void> {
this._params = params;
this._entryId = params.entry_id;
this._status = "loading";
this._checkSmartStartSupport();
this._startInclusion();
}
@query("#pin-input") private _pinInput?: HaTextField;
protected render() {
if (!this._entryId) {
return nothing;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.zwave_js.add_node.title")
)}
>
${this._status === "loading"
? html`<div style="display: flex; justify-content: center;">
<ha-circular-progress size="large" active></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}
>
</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>
</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>
</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"
)}
</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
be found on your device or the manual.
</p>
${
this._error
? html`<ha-alert alert-type="error">
${this._error}
</ha-alert>`
: ""
}
<div class="flex-container">
<ha-textfield
label="PIN"
id="pin-input"
@keyup=${this._handlePinKeyUp}
></ha-textfield>
${this._dsk}
</div>
<mwc-button
slot="primaryAction"
@click=${this._validateDskAndEnterPin}
>
Submit
</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()
.reverse()
.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"
? 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 active></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
active
></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
active
></ha-circular-progress>
<div class="status">
<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(
(stage) => html`
<span class="stage">
<ha-svg-icon
.path=${mdiCheckCircle}
class="success"
></ha-svg-icon>
${stage}
</span>
`
)}
</div>`
: ""}
</div>
</div>
<mwc-button
slot="primaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "failed"
? html`
<div class="flex-container">
<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>
${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
slot="primaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "finished"
? 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"
)}
</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
slot="primaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.close")}
</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 _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<void> {
this._unsubscribe();
this._status = "qr_scan";
}
private _qrCodeScanned(ev: CustomEvent): void {
if (this._qrProcessing) {
return;
}
this._handleQrCodeScanned(ev.detail.value);
}
private async _handleQrCodeScanned(qrCodeString: string): Promise<void> {
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";
if (this._params?.addedCallback) {
this._params.addedCallback();
}
} 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<void> {
this._status = "loading";
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<void> {
this._status = "loading";
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 === "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;
}
if (message.event === "interview completed") {
this._unsubscribe();
this._status = "finished";
if (this._params?.addedCallback) {
this._params.addedCallback();
}
}
if (message.event === "interview stage completed") {
if (this._stages === undefined) {
this._stages = [message.stage];
} else {
this._stages = [...this._stages, message.stage];
}
}
},
this._inclusionStrategy,
qrProvisioningInformation,
undefined,
undefined,
dsk
);
this._addNodeTimeoutHandle = window.setTimeout(() => {
this._unsubscribe();
this._status = "timed_out";
}, 90000);
}
private _unsubscribe(): void {
if (this._subscribed) {
this._subscribed.then((unsub) => unsub());
this._subscribed = undefined;
}
if (this._entryId) {
stopZwaveInclusion(this.hass, this._entryId);
}
this._requestedGrant = undefined;
this._dsk = undefined;
this._securityClasses = [];
this._status = undefined;
if (this._addNodeTimeoutHandle) {
clearTimeout(this._addNodeTimeoutHandle);
}
this._addNodeTimeoutHandle = undefined;
}
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;
}
.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;
}
.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-top: 16px;
}
}
ha-svg-icon {
width: 68px;
height: 48px;
}
ha-textfield {
display: block;
}
.secondary {
color: var(--secondary-text-color);
}
.flex-container ha-circular-progress,
.flex-container ha-svg-icon {
margin-right: 20px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-zwave_js-add-node": DialogZWaveJSAddNode;
}
}