Add SmartStart/QR scan support for Z-Wave JS (#10726)

pull/10755/head
Bram Kragten 2021-12-01 23:12:52 +01:00 committed by GitHub
parent 68373e6372
commit 4b49da58b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 602 additions and 110 deletions

View File

@ -79,6 +79,11 @@ function copyFonts(staticDir) {
);
}
function copyQrScannerWorker(staticDir) {
const staticPath = genStaticPath(staticDir);
copyFileDir(npmPath("qr-scanner/qr-scanner-worker.min.js"), staticPath("js"));
}
function copyMapPanel(staticDir) {
const staticPath = genStaticPath(staticDir);
copyFileDir(
@ -125,6 +130,9 @@ gulp.task("copy-static-app", async () => {
// Panel assets
copyMapPanel(staticDir);
// Qr Scanner assets
copyQrScannerWorker(staticDir);
});
gulp.task("copy-static-demo", async () => {

View File

@ -115,6 +115,7 @@
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "^0.3.2",
"punycode": "^2.1.1",
"qr-scanner": "^1.3.0",
"qrcode": "^1.4.4",
"regenerator-runtime": "^0.13.8",
"resize-observer-polyfill": "^1.5.1",

View File

@ -0,0 +1,162 @@
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import type { Select } from "@material/mwc-select/mwc-select";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import type QrScanner from "qr-scanner";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { LocalizeFunc } from "../common/translations/localize";
import "./ha-alert";
@customElement("ha-qr-scanner")
class HaQrScanner extends LitElement {
@property() localize!: LocalizeFunc;
@state() private _cameras?: QrScanner.Camera[];
@state() private _error?: string;
private _qrScanner?: QrScanner;
private _qrNotFoundCount = 0;
@query("video", true) private _video!: HTMLVideoElement;
@query("#canvas-container", true) private _canvasContainer!: HTMLDivElement;
public disconnectedCallback(): void {
super.disconnectedCallback();
this._qrNotFoundCount = 0;
if (this._qrScanner) {
this._qrScanner.stop();
this._qrScanner.destroy();
this._qrScanner = undefined;
}
while (this._canvasContainer.lastChild) {
this._canvasContainer.removeChild(this._canvasContainer.lastChild);
}
}
public connectedCallback(): void {
super.connectedCallback();
if (this.hasUpdated && navigator.mediaDevices) {
this._loadQrScanner();
}
}
protected firstUpdated() {
if (navigator.mediaDevices) {
this._loadQrScanner();
}
}
protected updated(changedProps: PropertyValues) {
if (changedProps.has("_error") && this._error) {
fireEvent(this, "qr-code-error", { message: this._error });
}
}
protected render(): TemplateResult {
return html`${this._cameras && this._cameras.length > 1
? html`<mwc-select
.label=${this.localize(
"ui.panel.config.zwave_js.add_node.select_camera"
)}
fixedMenuPosition
naturalMenuWidth
@closed=${stopPropagation}
@selected=${this._cameraChanged}
>
${this._cameras!.map(
(camera) => html`
<mwc-list-item .value=${camera.id}>${camera.label}</mwc-list-item>
`
)}
</mwc-select>`
: ""}
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${navigator.mediaDevices
? html`<video></video>
<div id="canvas-container"></div>`
: html`<ha-alert alert-type="warning"
>${!window.isSecureContext
? "You can only use your camera to scan a QR core when using HTTPS."
: "Your browser doesn't support QR scanning."}</ha-alert
>`}`;
}
private async _loadQrScanner() {
const QrScanner = (await import("qr-scanner")).default;
if (!(await QrScanner.hasCamera())) {
this._error = "No camera found";
return;
}
QrScanner.WORKER_PATH = "/static/js/qr-scanner-worker.min.js";
this._listCameras(QrScanner);
this._qrScanner = new QrScanner(
this._video,
this._qrCodeScanned,
this._qrCodeError
);
// @ts-ignore
const canvas = this._qrScanner.$canvas;
this._canvasContainer.appendChild(canvas);
canvas.style.display = "block";
try {
await this._qrScanner.start();
} catch (err: any) {
this._error = err;
}
}
private async _listCameras(qrScanner: typeof QrScanner): Promise<void> {
this._cameras = await qrScanner.listCameras(true);
}
private _qrCodeError = (err: any) => {
if (err === "No QR code found") {
this._qrNotFoundCount++;
if (this._qrNotFoundCount === 250) {
this._error = err;
}
return;
}
this._error = err.message || err;
// eslint-disable-next-line no-console
console.log(err);
};
private _qrCodeScanned = async (qrCodeString: string): Promise<void> => {
this._qrNotFoundCount = 0;
fireEvent(this, "qr-code-scanned", { value: qrCodeString });
};
private _cameraChanged(ev: CustomEvent): void {
this._qrScanner?.setCamera((ev.target as Select).value);
}
static styles = css`
canvas {
width: 100%;
}
mwc-select {
width: 100%;
margin-bottom: 16px;
}
`;
}
declare global {
// for fire event
interface HASSDomEvents {
"qr-code-scanned": { value: string };
"qr-code-error": { message: string };
}
interface HTMLElementTagNameMap {
"ha-qr-scanner": HaQrScanner;
}
}

View File

@ -57,6 +57,45 @@ export enum SecurityClass {
S0_Legacy = 7,
}
/** A named list of Z-Wave features */
export enum ZWaveFeature {
// Available starting with Z-Wave SDK 6.81
SmartStart,
}
enum QRCodeVersion {
S2 = 0,
SmartStart = 1,
}
enum Protocols {
ZWave = 0,
ZWaveLongRange = 1,
}
export interface QRProvisioningInformation {
version: QRCodeVersion;
securityClasses: SecurityClass[];
dsk: string;
genericDeviceClass: number;
specificDeviceClass: number;
installerIconType: number;
manufacturerId: number;
productType: number;
productId: number;
applicationVersion: string;
maxInclusionRequestInterval?: number | undefined;
uuid?: string | undefined;
supportedProtocols?: Protocols[] | undefined;
}
export interface PlannedProvisioningEntry {
/** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */
dsk: string;
security_classes: SecurityClass[];
}
export const MINIMUM_QR_STRING_LENGTH = 52;
export interface ZWaveJSNodeIdentifiers {
home_id: string;
node_id: number;
@ -197,7 +236,7 @@ export const migrateZwave = (
dry_run,
});
export const fetchNetworkStatus = (
export const fetchZwaveNetworkStatus = (
hass: HomeAssistant,
entry_id: string
): Promise<ZWaveJSNetwork> =>
@ -206,7 +245,7 @@ export const fetchNetworkStatus = (
entry_id,
});
export const fetchDataCollectionStatus = (
export const fetchZwaveDataCollectionStatus = (
hass: HomeAssistant,
entry_id: string
): Promise<ZWaveJSDataCollectionStatus> =>
@ -215,7 +254,7 @@ export const fetchDataCollectionStatus = (
entry_id,
});
export const setDataCollectionPreference = (
export const setZwaveDataCollectionPreference = (
hass: HomeAssistant,
entry_id: string,
opted_in: boolean
@ -226,25 +265,31 @@ export const setDataCollectionPreference = (
opted_in,
});
export const subscribeAddNode = (
export const subscribeAddZwaveNode = (
hass: HomeAssistant,
entry_id: string,
callbackFunction: (message: any) => void,
inclusion_strategy: InclusionStrategy = InclusionStrategy.Default
inclusion_strategy: InclusionStrategy = InclusionStrategy.Default,
qr_provisioning_information?: QRProvisioningInformation,
qr_code_string?: string,
planned_provisioning_entry?: PlannedProvisioningEntry
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage((message) => callbackFunction(message), {
type: "zwave_js/add_node",
entry_id: entry_id,
inclusion_strategy,
qr_code_string,
qr_provisioning_information,
planned_provisioning_entry,
});
export const stopInclusion = (hass: HomeAssistant, entry_id: string) =>
export const stopZwaveInclusion = (hass: HomeAssistant, entry_id: string) =>
hass.callWS({
type: "zwave_js/stop_inclusion",
entry_id,
});
export const grantSecurityClasses = (
export const zwaveGrantSecurityClasses = (
hass: HomeAssistant,
entry_id: string,
security_classes: SecurityClass[],
@ -257,7 +302,7 @@ export const grantSecurityClasses = (
client_side_auth,
});
export const validateDskAndEnterPin = (
export const zwaveValidateDskAndEnterPin = (
hass: HomeAssistant,
entry_id: string,
pin: string
@ -268,7 +313,44 @@ export const validateDskAndEnterPin = (
pin,
});
export const fetchNodeStatus = (
export const zwaveSupportsFeature = (
hass: HomeAssistant,
entry_id: string,
feature: ZWaveFeature
): Promise<{ supported: boolean }> =>
hass.callWS({
type: "zwave_js/supports_feature",
entry_id,
feature,
});
export const zwaveParseQrCode = (
hass: HomeAssistant,
entry_id: string,
qr_code_string: string
): Promise<QRProvisioningInformation> =>
hass.callWS({
type: "zwave_js/parse_qr_code_string",
entry_id,
qr_code_string,
});
export const provisionZwaveSmartStartNode = (
hass: HomeAssistant,
entry_id: string,
qr_provisioning_information?: QRProvisioningInformation,
qr_code_string?: string,
planned_provisioning_entry?: PlannedProvisioningEntry
): Promise<QRProvisioningInformation> =>
hass.callWS({
type: "zwave_js/provision_smart_start_node",
entry_id,
qr_code_string,
qr_provisioning_information,
planned_provisioning_entry,
});
export const fetchZwaveNodeStatus = (
hass: HomeAssistant,
entry_id: string,
node_id: number
@ -279,7 +361,7 @@ export const fetchNodeStatus = (
node_id,
});
export const fetchNodeMetadata = (
export const fetchZwaveNodeMetadata = (
hass: HomeAssistant,
entry_id: string,
node_id: number
@ -290,7 +372,7 @@ export const fetchNodeMetadata = (
node_id,
});
export const fetchNodeConfigParameters = (
export const fetchZwaveNodeConfigParameters = (
hass: HomeAssistant,
entry_id: string,
node_id: number
@ -301,7 +383,7 @@ export const fetchNodeConfigParameters = (
node_id,
});
export const setNodeConfigParameter = (
export const setZwaveNodeConfigParameter = (
hass: HomeAssistant,
entry_id: string,
node_id: number,
@ -320,7 +402,7 @@ export const setNodeConfigParameter = (
return hass.callWS(data);
};
export const reinterviewNode = (
export const reinterviewZwaveNode = (
hass: HomeAssistant,
entry_id: string,
node_id: number,
@ -335,7 +417,7 @@ export const reinterviewNode = (
}
);
export const healNode = (
export const healZwaveNode = (
hass: HomeAssistant,
entry_id: string,
node_id: number
@ -346,7 +428,7 @@ export const healNode = (
node_id,
});
export const removeFailedNode = (
export const removeFailedZwaveNode = (
hass: HomeAssistant,
entry_id: string,
node_id: number,
@ -361,7 +443,7 @@ export const removeFailedNode = (
}
);
export const healNetwork = (
export const healZwaveNetwork = (
hass: HomeAssistant,
entry_id: string
): Promise<UnsubscribeFunc> =>
@ -370,7 +452,7 @@ export const healNetwork = (
entry_id,
});
export const stopHealNetwork = (
export const stopHealZwaveNetwork = (
hass: HomeAssistant,
entry_id: string
): Promise<UnsubscribeFunc> =>
@ -379,7 +461,7 @@ export const stopHealNetwork = (
entry_id,
});
export const subscribeNodeReady = (
export const subscribeZwaveNodeReady = (
hass: HomeAssistant,
entry_id: string,
node_id: number,
@ -394,7 +476,7 @@ export const subscribeNodeReady = (
}
);
export const subscribeHealNetworkProgress = (
export const subscribeHealZwaveNetworkProgress = (
hass: HomeAssistant,
entry_id: string,
callbackFunction: (message: ZWaveJSHealNetworkStatusMessage) => void
@ -407,7 +489,7 @@ export const subscribeHealNetworkProgress = (
}
);
export const getIdentifiersFromDevice = (
export const getZwaveJsIdentifiersFromDevice = (
device: DeviceRegistryEntry
): ZWaveJSNodeIdentifiers | undefined => {
if (!device) {

View File

@ -10,7 +10,7 @@ import {
import { customElement, property, state } from "lit/decorators";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import {
getIdentifiersFromDevice,
getZwaveJsIdentifiersFromDevice,
ZWaveJSNodeIdentifiers,
} from "../../../../../../data/zwave_js";
import { haStyle } from "../../../../../../resources/styles";
@ -34,7 +34,7 @@ export class HaDeviceActionsZWaveJS extends LitElement {
this._entryId = this.device.config_entries[0];
const identifiers: ZWaveJSNodeIdentifiers | undefined =
getIdentifiersFromDevice(this.device);
getZwaveJsIdentifiersFromDevice(this.device);
if (!identifiers) {
return;
}

View File

@ -13,8 +13,8 @@ import {
getConfigEntries,
} from "../../../../../../data/config_entries";
import {
fetchNodeStatus,
getIdentifiersFromDevice,
fetchZwaveNodeStatus,
getZwaveJsIdentifiersFromDevice,
nodeStatus,
ZWaveJSNodeStatus,
ZWaveJSNodeIdentifiers,
@ -42,7 +42,7 @@ export class HaDeviceInfoZWaveJS extends LitElement {
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("device")) {
const identifiers: ZWaveJSNodeIdentifiers | undefined =
getIdentifiersFromDevice(this.device);
getZwaveJsIdentifiersFromDevice(this.device);
if (!identifiers) {
return;
}
@ -76,7 +76,11 @@ export class HaDeviceInfoZWaveJS extends LitElement {
zwaveJsConfEntries++;
}
this._node = await fetchNodeStatus(this.hass, this._entryId, this._nodeId);
this._node = await fetchZwaveNodeStatus(
this.hass,
this._entryId,
this._nodeId
);
}
protected render(): TemplateResult {

View File

@ -21,10 +21,10 @@ import {
import {
migrateZwave,
ZWaveJsMigrationData,
fetchNetworkStatus as fetchZwaveJsNetworkStatus,
fetchNodeStatus,
getIdentifiersFromDevice,
subscribeNodeReady,
fetchZwaveNetworkStatus as fetchZwaveJsNetworkStatus,
fetchZwaveNodeStatus,
getZwaveJsIdentifiersFromDevice,
subscribeZwaveNodeReady,
} from "../../../../../data/zwave_js";
import {
fetchMigrationConfig,
@ -425,7 +425,7 @@ export class ZwaveMigration extends LitElement {
this._zwaveJsEntryId!
);
const nodeStatePromisses = networkStatus.controller.nodes.map((nodeId) =>
fetchNodeStatus(this.hass, this._zwaveJsEntryId!, nodeId)
fetchZwaveNodeStatus(this.hass, this._zwaveJsEntryId!, nodeId)
);
const nodesNotReady = (await Promise.all(nodeStatePromisses)).filter(
(node) => !node.ready
@ -436,13 +436,18 @@ export class ZwaveMigration extends LitElement {
return;
}
this._nodeReadySubscriptions = nodesNotReady.map((node) =>
subscribeNodeReady(this.hass, this._zwaveJsEntryId!, node.node_id, () => {
subscribeZwaveNodeReady(
this.hass,
this._zwaveJsEntryId!,
node.node_id,
() => {
this._getZwaveJSNodesStatus();
})
}
)
);
const deviceReg = await fetchDeviceRegistry(this.hass);
this._waitingOnDevices = deviceReg
.map((device) => getIdentifiersFromDevice(device))
.map((device) => getZwaveJsIdentifiersFromDevice(device))
.filter(Boolean);
}

View File

@ -1,30 +1,40 @@
import "@material/mwc-button/mwc-button";
import { mdiAlertCircle, mdiCheckCircle, mdiCloseCircle } from "@mdi/js";
import type { TextField } from "@material/mwc-textfield/mwc-textfield";
import "@material/mwc-textfield/mwc-textfield";
import { mdiAlertCircle, mdiCheckCircle, mdiQrcodeScan } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } 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-radio";
import "../../../../../components/ha-switch";
import {
grantSecurityClasses,
zwaveGrantSecurityClasses,
InclusionStrategy,
MINIMUM_QR_STRING_LENGTH,
zwaveParseQrCode,
provisionZwaveSmartStartNode,
QRProvisioningInformation,
RequestedGrant,
SecurityClass,
stopInclusion,
subscribeAddNode,
validateDskAndEnterPin,
stopZwaveInclusion,
subscribeAddZwaveNode,
zwaveSupportsFeature,
zwaveValidateDskAndEnterPin,
ZWaveFeature,
PlannedProvisioningEntry,
} from "../../../../../data/zwave_js";
import { haStyle, haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import { ZWaveJSAddNodeDialogParams } from "./show-dialog-zwave_js-add-node";
import "../../../../../components/ha-radio";
import { HaCheckbox } from "../../../../../components/ha-checkbox";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-qr-scanner";
export interface ZWaveJSAddNodeDevice {
id: string;
@ -40,11 +50,14 @@ class DialogZWaveJSAddNode extends LitElement {
@state() private _status?:
| "loading"
| "started"
| "started_specific"
| "choose_strategy"
| "qr_scan"
| "interviewing"
| "failed"
| "timed_out"
| "finished"
| "provisioned"
| "validate_dsk_enter_pin"
| "grant_security_classes";
@ -64,10 +77,14 @@ class DialogZWaveJSAddNode extends LitElement {
@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();
@ -76,6 +93,7 @@ class DialogZWaveJSAddNode extends LitElement {
public async showDialog(params: ZWaveJSAddNodeDialogParams): Promise<void> {
this._entryId = params.entry_id;
this._status = "loading";
this._checkSmartStartSupport();
this._startInclusion();
}
@ -157,6 +175,22 @@ class DialogZWaveJSAddNode extends LitElement {
>
Search device
</mwc-button>`
: this._status === "qr_scan"
? html`<ha-qr-scanner
.localize=${this.hass.localize}
@qr-code-scanned=${this._qrCodeScanned}
></ha-qr-scanner>
<p>
If scanning doesn't work, you can enter the QR code value
manually:
</p>
<mwc-textfield
.label=${this.hass.localize(
"ui.panel.config.zwave_js.add_node.enter_qr_code"
)}
.disabled=${this._qrProcessing}
@keydown=${this._qrKeyDown}
></mwc-textfield>`
: this._status === "validate_dsk_enter_pin"
? html`
<p>
@ -241,18 +275,28 @@ class DialogZWaveJSAddNode extends LitElement {
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="flex-container">
<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>
<div class="status">
<p>
<b
>${this.hass.localize(
"ui.panel.config.zwave_js.add_node.controller_in_inclusion_mode"
)}</b
>
</p>
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
@ -263,15 +307,37 @@ class DialogZWaveJSAddNode extends LitElement {
class="link"
@click=${this._chooseInclusionStrategy}
>
Advanced inclusion
${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.panel.config.zwave_js.add_node.cancel_inclusion"
)}
${this.hass.localize("ui.common.cancel")}
</mwc-button>
`
: this._status === "interviewing"
@ -310,16 +376,18 @@ class DialogZWaveJSAddNode extends LitElement {
: this._status === "failed"
? html`
<div class="flex-container">
<ha-svg-icon
.path=${mdiCloseCircle}
class="failed"
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
<ha-alert
alert-type="error"
.title=${this.hass.localize(
"ui.panel.config.zwave_js.add_node.inclusion_failed"
)}
</p>
>
${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(
@ -391,6 +459,23 @@ class DialogZWaveJSAddNode extends LitElement {
${this.hass.localize("ui.panel.config.zwave_js.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.panel.config.zwave_js.common.close")}
</mwc-button>`
: ""}
</ha-dialog>
`;
@ -417,6 +502,83 @@ class DialogZWaveJSAddNode extends LitElement {
}
}
private async _scanQRCode(): Promise<void> {
this._unsubscribe();
this._status = "qr_scan";
}
private _qrKeyDown(ev: KeyboardEvent) {
if (this._qrProcessing) {
return;
}
if (ev.key === "Enter") {
this._handleQrCodeScanned((ev.target as TextField).value);
}
}
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;
if (
qrCodeString.length < MINIMUM_QR_STRING_LENGTH ||
!qrCodeString.startsWith("90")
) {
this._qrProcessing = false;
this._error = `Invalid QR code (${qrCodeString})`;
return;
}
let provisioningInfo: QRProvisioningInformation;
try {
provisioningInfo = await zwaveParseQrCode(
this.hass,
this._entryId!,
qrCodeString
);
} catch (err: any) {
this._qrProcessing = false;
this._error = err.message;
return;
}
this._status = "loading";
// wait for QR scanner to be removed before resetting qr processing
this.updateComplete.then(() => {
this._qrProcessing = false;
});
if (provisioningInfo.version === 1) {
try {
await provisionZwaveSmartStartNode(
this.hass,
this._entryId!,
provisioningInfo
);
this._status = "provisioned";
} catch (err: any) {
this._error = err.message;
this._status = "failed";
}
} else if (provisioningInfo.version === 0) {
this._inclusionStrategy = InclusionStrategy.Security_S2;
// this._startInclusion(provisioningInfo);
this._startInclusion(undefined, undefined, {
dsk: "34673-15546-46480-39591-32400-22155-07715-45994",
security_classes: [0, 1, 7],
});
} else {
this._error = "This QR code is not supported";
this._status = "failed";
}
}
private _handlePinKeyUp(ev: KeyboardEvent) {
if (ev.key === "Enter") {
this._validateDskAndEnterPin();
@ -427,7 +589,7 @@ class DialogZWaveJSAddNode extends LitElement {
this._status = "loading";
this._error = undefined;
try {
await validateDskAndEnterPin(
await zwaveValidateDskAndEnterPin(
this.hass,
this._entryId!,
this._pinInput!.value as string
@ -442,7 +604,7 @@ class DialogZWaveJSAddNode extends LitElement {
this._status = "loading";
this._error = undefined;
try {
await grantSecurityClasses(
await zwaveGrantSecurityClasses(
this.hass,
this._entryId!,
this._securityClasses
@ -460,17 +622,33 @@ class DialogZWaveJSAddNode extends LitElement {
this._startInclusion();
}
private _startInclusion(): void {
private async _checkSmartStartSupport() {
this._supportsSmartStart = (
await zwaveSupportsFeature(
this.hass,
this._entryId!,
ZWaveFeature.SmartStart
)
).supported;
}
private _startInclusion(
qrProvisioningInformation?: QRProvisioningInformation,
qrCodeString?: string,
plannedProvisioningEntry?: PlannedProvisioningEntry
): void {
if (!this.hass) {
return;
}
this._lowSecurity = false;
this._subscribed = subscribeAddNode(
const specificDevice =
qrProvisioningInformation || qrCodeString || plannedProvisioningEntry;
this._subscribed = subscribeAddZwaveNode(
this.hass,
this._entryId!,
(message) => {
if (message.event === "inclusion started") {
this._status = "started";
this._status = specificDevice ? "started_specific" : "started";
}
if (message.event === "inclusion failed") {
this._unsubscribe();
@ -491,7 +669,7 @@ class DialogZWaveJSAddNode extends LitElement {
if (message.event === "grant security classes") {
if (this._inclusionStrategy === undefined) {
grantSecurityClasses(
zwaveGrantSecurityClasses(
this.hass,
this._entryId!,
message.requested_grant.securityClasses,
@ -525,7 +703,10 @@ class DialogZWaveJSAddNode extends LitElement {
}
}
},
this._inclusionStrategy
this._inclusionStrategy,
qrProvisioningInformation,
qrCodeString,
plannedProvisioningEntry
);
this._addNodeTimeoutHandle = window.setTimeout(() => {
this._unsubscribe();
@ -539,7 +720,7 @@ class DialogZWaveJSAddNode extends LitElement {
this._subscribed = undefined;
}
if (this._entryId) {
stopInclusion(this.hass, this._entryId);
stopZwaveInclusion(this.hass, this._entryId);
}
this._requestedGrant = undefined;
this._dsk = undefined;
@ -558,6 +739,7 @@ class DialogZWaveJSAddNode extends LitElement {
this._status = undefined;
this._device = undefined;
this._stages = undefined;
this._error = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@ -578,10 +760,6 @@ class DialogZWaveJSAddNode extends LitElement {
color: var(--warning-color);
}
.failed {
color: var(--error-color);
}
.stages {
margin-top: 16px;
display: grid;
@ -610,6 +788,39 @@ class DialogZWaveJSAddNode extends LitElement {
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;
}
}
mwc-textfield {
width: 100%;
}
ha-svg-icon {
width: 68px;
height: 48px;

View File

@ -7,10 +7,10 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { createCloseHeading } from "../../../../../components/ha-dialog";
import {
fetchNetworkStatus,
healNetwork,
stopHealNetwork,
subscribeHealNetworkProgress,
fetchZwaveNetworkStatus,
healZwaveNetwork,
stopHealZwaveNetwork,
subscribeHealZwaveNetworkProgress,
ZWaveJSHealNetworkStatusMessage,
ZWaveJSNetwork,
} from "../../../../../data/zwave_js";
@ -202,13 +202,13 @@ class DialogZWaveJSHealNetwork extends LitElement {
if (!this.hass) {
return;
}
const network: ZWaveJSNetwork = await fetchNetworkStatus(
const network: ZWaveJSNetwork = await fetchZwaveNetworkStatus(
this.hass!,
this.entry_id!
);
if (network.controller.is_heal_network_active) {
this._status = "started";
this._subscribed = subscribeHealNetworkProgress(
this._subscribed = subscribeHealZwaveNetworkProgress(
this.hass,
this.entry_id!,
this._handleMessage.bind(this)
@ -220,9 +220,9 @@ class DialogZWaveJSHealNetwork extends LitElement {
if (!this.hass) {
return;
}
healNetwork(this.hass, this.entry_id!);
healZwaveNetwork(this.hass, this.entry_id!);
this._status = "started";
this._subscribed = subscribeHealNetworkProgress(
this._subscribed = subscribeHealZwaveNetworkProgress(
this.hass,
this.entry_id!,
this._handleMessage.bind(this)
@ -233,7 +233,7 @@ class DialogZWaveJSHealNetwork extends LitElement {
if (!this.hass) {
return;
}
stopHealNetwork(this.hass, this.entry_id!);
stopHealZwaveNetwork(this.hass, this.entry_id!);
this._unsubscribe();
this._status = "cancelled";
}

View File

@ -10,8 +10,8 @@ import {
computeDeviceName,
} from "../../../../../data/device_registry";
import {
fetchNetworkStatus,
healNode,
fetchZwaveNetworkStatus,
healZwaveNode,
ZWaveJSNetwork,
} from "../../../../../data/zwave_js";
import { haStyleDialog } from "../../../../../resources/styles";
@ -206,7 +206,7 @@ class DialogZWaveJSHealNode extends LitElement {
if (!this.hass) {
return;
}
const network: ZWaveJSNetwork = await fetchNetworkStatus(
const network: ZWaveJSNetwork = await fetchZwaveNetworkStatus(
this.hass!,
this.entry_id!
);
@ -221,7 +221,11 @@ class DialogZWaveJSHealNode extends LitElement {
}
this._status = "started";
try {
this._status = (await healNode(this.hass, this.entry_id!, this.node_id!))
this._status = (await healZwaveNode(
this.hass,
this.entry_id!,
this.node_id!
))
? "finished"
: "failed";
} catch (err: any) {

View File

@ -6,7 +6,7 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-circular-progress";
import { createCloseHeading } from "../../../../../components/ha-dialog";
import { reinterviewNode } from "../../../../../data/zwave_js";
import { reinterviewZwaveNode } from "../../../../../data/zwave_js";
import { haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import { ZWaveJSReinterviewNodeDialogParams } from "./show-dialog-zwave_js-reinterview-node";
@ -157,7 +157,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
if (!this.hass) {
return;
}
this._subscribed = reinterviewNode(
this._subscribed = reinterviewZwaveNode(
this.hass,
this.entry_id!,
this.node_id!,

View File

@ -7,7 +7,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-circular-progress";
import { createCloseHeading } from "../../../../../components/ha-dialog";
import {
removeFailedNode,
removeFailedZwaveNode,
ZWaveJSRemovedNode,
} from "../../../../../data/zwave_js";
import { haStyleDialog } from "../../../../../resources/styles";
@ -164,7 +164,7 @@ class DialogZWaveJSRemoveFailedNode extends LitElement {
return;
}
this._status = "started";
this._subscribed = removeFailedNode(
this._subscribed = removeFailedZwaveNode(
this.hass,
this.entry_id!,
this.node_id!,

View File

@ -9,11 +9,11 @@ import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-svg-icon";
import { getSignedPath } from "../../../../../data/auth";
import {
fetchDataCollectionStatus,
fetchNetworkStatus,
fetchNodeStatus,
fetchZwaveDataCollectionStatus,
fetchZwaveNetworkStatus,
fetchZwaveNodeStatus,
NodeStatus,
setDataCollectionPreference,
setZwaveDataCollectionPreference,
ZWaveJSNetwork,
ZWaveJSNodeStatus,
} from "../../../../../data/zwave_js";
@ -317,8 +317,8 @@ class ZWaveJSConfigDashboard extends LitElement {
}
const [network, dataCollectionStatus] = await Promise.all([
fetchNetworkStatus(this.hass!, this.configEntryId),
fetchDataCollectionStatus(this.hass!, this.configEntryId),
fetchZwaveNetworkStatus(this.hass!, this.configEntryId),
fetchZwaveDataCollectionStatus(this.hass!, this.configEntryId),
]);
this._network = network;
@ -340,7 +340,7 @@ class ZWaveJSConfigDashboard extends LitElement {
return;
}
const nodeStatePromisses = this._network.controller.nodes.map((nodeId) =>
fetchNodeStatus(this.hass, this.configEntryId!, nodeId)
fetchZwaveNodeStatus(this.hass, this.configEntryId!, nodeId)
);
this._nodes = await Promise.all(nodeStatePromisses);
}
@ -364,7 +364,7 @@ class ZWaveJSConfigDashboard extends LitElement {
}
private _dataCollectionToggled(ev) {
setDataCollectionPreference(
setZwaveDataCollectionPreference(
this.hass!,
this.configEntryId!,
ev.target.checked

View File

@ -32,9 +32,9 @@ import {
subscribeDeviceRegistry,
} from "../../../../../data/device_registry";
import {
fetchNodeConfigParameters,
fetchNodeMetadata,
setNodeConfigParameter,
fetchZwaveNodeConfigParameters,
fetchZwaveNodeMetadata,
setZwaveNodeConfigParameter,
ZWaveJSNodeConfigParams,
ZwaveJSNodeMetadata,
ZWaveJSSetConfigParamResult,
@ -377,7 +377,7 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
private async _updateConfigParameter(target, value) {
const nodeId = getNodeId(this._device!);
try {
const result = await setNodeConfigParameter(
const result = await setZwaveNodeConfigParameter(
this.hass,
this.configEntryId!,
nodeId!,
@ -429,8 +429,8 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
}
[this._nodeMetadata, this._config] = await Promise.all([
fetchNodeMetadata(this.hass, this.configEntryId, nodeId!),
fetchNodeConfigParameters(this.hass, this.configEntryId, nodeId!),
fetchZwaveNodeMetadata(this.hass, this.configEntryId, nodeId!),
fetchZwaveNodeConfigParameters(this.hass, this.configEntryId, nodeId!),
]);
}

View File

@ -2855,11 +2855,18 @@
},
"add_node": {
"title": "Add a Z-Wave Device",
"cancel_inclusion": "Cancel Inclusion",
"controller_in_inclusion_mode": "Your Z-Wave controller is now in inclusion mode.",
"searching_device": "Searching for device",
"follow_device_instructions": "Follow the directions that came with your device to trigger pairing on the device.",
"inclusion_failed": "The device could not be added. Please check the logs for more information.",
"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",
"enter_qr_code": "Enter QR code value",
"select_camera": "Select camera",
"inclusion_failed": "The device could not be added.",
"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."

View File

@ -9139,6 +9139,7 @@ fsevents@^1.2.7:
prettier: ^2.4.1
proxy-polyfill: ^0.3.2
punycode: ^2.1.1
qr-scanner: ^1.3.0
qrcode: ^1.4.4
regenerator-runtime: ^0.13.8
require-dir: ^1.2.0
@ -13007,6 +13008,13 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"qr-scanner@npm:^1.3.0":
version: 1.3.0
resolution: "qr-scanner@npm:1.3.0"
checksum: 421ff00626252d0f9e50550fb550a463166e4d0438baffb469c9450079f1f802f6df22784509bb571ef50ece81aecaadc00f91d442959f37655ad29710c81c8b
languageName: node
linkType: hard
"qrcode@npm:^1.4.4":
version: 1.4.4
resolution: "qrcode@npm:1.4.4"