diff --git a/build-scripts/gulp/gather-static.js b/build-scripts/gulp/gather-static.js
index c0f36d02af..306ae3158a 100644
--- a/build-scripts/gulp/gather-static.js
+++ b/build-scripts/gulp/gather-static.js
@@ -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 () => {
diff --git a/package.json b/package.json
index 0d9726b250..df1ce7d236 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/ha-qr-scanner.ts b/src/components/ha-qr-scanner.ts
new file mode 100644
index 0000000000..0872c2556d
--- /dev/null
+++ b/src/components/ha-qr-scanner.ts
@@ -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`
+ ${this._cameras!.map(
+ (camera) => html`
+ ${camera.label}
+ `
+ )}
+ `
+ : ""}
+ ${this._error
+ ? html`${this._error}`
+ : ""}
+ ${navigator.mediaDevices
+ ? html`
+
`
+ : html`${!window.isSecureContext
+ ? "You can only use your camera to scan a QR core when using HTTPS."
+ : "Your browser doesn't support QR scanning."}`}`;
+ }
+
+ 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 {
+ 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 => {
+ 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;
+ }
+}
diff --git a/src/data/zwave_js.ts b/src/data/zwave_js.ts
index fbd9113d5c..cf278eaf2b 100644
--- a/src/data/zwave_js.ts
+++ b/src/data/zwave_js.ts
@@ -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 =>
@@ -206,7 +245,7 @@ export const fetchNetworkStatus = (
entry_id,
});
-export const fetchDataCollectionStatus = (
+export const fetchZwaveDataCollectionStatus = (
hass: HomeAssistant,
entry_id: string
): Promise =>
@@ -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 =>
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 =>
+ 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 =>
+ 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 =>
@@ -370,7 +452,7 @@ export const healNetwork = (
entry_id,
});
-export const stopHealNetwork = (
+export const stopHealZwaveNetwork = (
hass: HomeAssistant,
entry_id: string
): Promise =>
@@ -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) {
diff --git a/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-actions-zwave_js.ts b/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-actions-zwave_js.ts
index cd6b6e5f7d..2cd06cbea6 100644
--- a/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-actions-zwave_js.ts
+++ b/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-actions-zwave_js.ts
@@ -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;
}
diff --git a/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts b/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts
index b69484180e..dc24356fff 100644
--- a/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts
+++ b/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts
@@ -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 {
diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-migration.ts b/src/panels/config/integrations/integration-panels/zwave/zwave-migration.ts
index 99cf52fb36..ee19b1aae6 100644
--- a/src/panels/config/integrations/integration-panels/zwave/zwave-migration.ts
+++ b/src/panels/config/integrations/integration-panels/zwave/zwave-migration.ts
@@ -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, () => {
- this._getZwaveJSNodesStatus();
- })
+ 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);
}
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 b33c340cc5..b9f846fa09 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
@@ -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;
+ private _qrProcessing = false;
+
public disconnectedCallback(): void {
super.disconnectedCallback();
this._unsubscribe();
@@ -76,6 +93,7 @@ class DialogZWaveJSAddNode extends LitElement {
public async showDialog(params: ZWaveJSAddNodeDialogParams): Promise {
this._entryId = params.entry_id;
this._status = "loading";
+ this._checkSmartStartSupport();
this._startInclusion();
}
@@ -157,6 +175,22 @@ class DialogZWaveJSAddNode extends LitElement {
>
Search device
`
+ : this._status === "qr_scan"
+ ? html`
+
+ If scanning doesn't work, you can enter the QR code value
+ manually:
+
+ `
: this._status === "validate_dsk_enter_pin"
? html`
@@ -241,18 +275,28 @@ class DialogZWaveJSAddNode extends LitElement {
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.controller_in_inclusion_mode"
- )}
-
+
+
+
+ ${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"
@@ -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"
+ )}
+ ${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.panel.config.zwave_js.add_node.cancel_inclusion"
- )}
+ ${this.hass.localize("ui.common.cancel")}
`
: this._status === "interviewing"
@@ -310,16 +376,18 @@ class DialogZWaveJSAddNode extends LitElement {
: this._status === "failed"
? html`
-
-
- ${this.hass.localize(
+
+ >
+ ${this._error ||
+ this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.check_logs"
+ )}
+
${this._stages
? html`
${this._stages.map(
@@ -391,6 +459,23 @@ class DialogZWaveJSAddNode extends LitElement {
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
`
+ : this._status === "provisioned"
+ ? html`
+
+
+
+ ${this.hass.localize(
+ "ui.panel.config.zwave_js.add_node.provisioning_finished"
+ )}
+
+
+
+
+ ${this.hass.localize("ui.panel.config.zwave_js.common.close")}
+ `
: ""}
`;
@@ -417,6 +502,83 @@ class DialogZWaveJSAddNode extends LitElement {
}
}
+ private async _scanQRCode(): Promise
{
+ 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 {
+ 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;
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-network.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-network.ts
index 33b60fe685..f99b073869 100644
--- a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-network.ts
+++ b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-network.ts
@@ -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";
}
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-node.ts
index 632c19286b..6db1483fb7 100644
--- a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-node.ts
+++ b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-node.ts
@@ -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) {
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-reinterview-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-reinterview-node.ts
index a56f2ed050..fb4e12785c 100644
--- a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-reinterview-node.ts
+++ b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-reinterview-node.ts
@@ -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!,
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-remove-failed-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-remove-failed-node.ts
index aa8a264649..32faabced2 100644
--- a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-remove-failed-node.ts
+++ b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-remove-failed-node.ts
@@ -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!,
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts
index dbd163508f..e7e97addf4 100644
--- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts
+++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts
@@ -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
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts
index 9b2e0a4bdf..19b7d24dd8 100644
--- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts
+++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts
@@ -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!),
]);
}
diff --git a/src/translations/en.json b/src/translations/en.json
index 1846d12698..a5cb447e03 100755
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -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."
diff --git a/yarn.lock b/yarn.lock
index 0bfb5c39ae..1322aea8a2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"