diff --git a/src/components/ha-qr-scanner.ts b/src/components/ha-qr-scanner.ts
index b65f9ba07a..b6fdef9e89 100644
--- a/src/components/ha-qr-scanner.ts
+++ b/src/components/ha-qr-scanner.ts
@@ -1,72 +1,92 @@
import "@material/mwc-button/mwc-button";
-import "@material/mwc-list/mwc-list-item";
import { mdiCamera } from "@mdi/js";
-import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
+import { UnsubscribeFunc } from "home-assistant-js-websocket";
+import { css, html, LitElement, nothing, PropertyValues } 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 { addExternalBarCodeListener } from "../external_app/external_app_entrypoint";
+import { HomeAssistant } from "../types";
import "./ha-alert";
import "./ha-button-menu";
+import "./ha-list-item";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@customElement("ha-qr-scanner")
class HaQrScanner extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
@property({ attribute: false }) public localize!: LocalizeFunc;
+ @property() public description?: string;
+
+ @property({ attribute: "alternative_option_label" })
+ public alternativeOptionLabel?: string;
+
+ @property() public error?: string;
+
@state() private _cameras?: QrScanner.Camera[];
- @state() private _error?: string;
+ @state() private _manual = false;
private _qrScanner?: QrScanner;
private _qrNotFoundCount = 0;
- @query("video", true) private _video!: HTMLVideoElement;
+ private _removeListener?: UnsubscribeFunc;
- @query("#canvas-container", true) private _canvasContainer!: HTMLDivElement;
+ @query("video", true) private _video?: HTMLVideoElement;
+
+ @query("#canvas-container", true) private _canvasContainer?: HTMLDivElement;
@query("ha-textfield") private _manualInput?: HaTextField;
public disconnectedCallback(): void {
super.disconnectedCallback();
this._qrNotFoundCount = 0;
+ if (this._nativeBarcodeScanner) {
+ this._closeExternalScanner();
+ }
if (this._qrScanner) {
this._qrScanner.stop();
this._qrScanner.destroy();
this._qrScanner = undefined;
}
- while (this._canvasContainer.lastChild) {
+ while (this._canvasContainer?.lastChild) {
this._canvasContainer.removeChild(this._canvasContainer.lastChild);
}
}
public connectedCallback(): void {
super.connectedCallback();
- if (this.hasUpdated && navigator.mediaDevices) {
+ if (this.hasUpdated) {
this._loadQrScanner();
}
}
protected firstUpdated() {
- if (navigator.mediaDevices) {
- this._loadQrScanner();
- }
+ this._loadQrScanner();
}
protected updated(changedProps: PropertyValues) {
- if (changedProps.has("_error") && this._error) {
- fireEvent(this, "qr-code-error", { message: this._error });
+ if (changedProps.has("error") && this.error) {
+ alert(`error: ${this.error}`);
+ this._notifyExternalScanner(this.error);
}
}
- protected render(): TemplateResult {
- return html`${this._error
- ? html`${this._error}`
+ protected render() {
+ if (this._nativeBarcodeScanner && !this._manual) {
+ return nothing;
+ }
+
+ return html`${this.error
+ ? html`${this.error}`
: ""}
- ${navigator.mediaDevices
+ ${navigator.mediaDevices && !this._manual
? html`
${this._cameras && this._cameras.length > 1
@@ -80,21 +100,26 @@ class HaQrScanner extends LitElement {
>
${this._cameras!.map(
(camera) => html`
- ${camera.label}
+ ${camera.label}
+
`
)}
`
- : ""}
+ : nothing}
`
- : html`
- ${!window.isSecureContext
- ? this.localize("ui.components.qr-scanner.only_https_supported")
- : this.localize("ui.components.qr-scanner.not_supported")}
-
+ : html`${this._manual
+ ? nothing
+ : html`
+ ${!window.isSecureContext
+ ? this.localize(
+ "ui.components.qr-scanner.only_https_supported"
+ )
+ : this.localize("ui.components.qr-scanner.not_supported")}
+ `}
${this.localize("ui.components.qr-scanner.manual_input")}
- ${this.localize("ui.common.submit")}
+
+ ${this.localize("ui.common.submit")}
+
`}`;
}
+ private get _nativeBarcodeScanner(): boolean {
+ return Boolean(this.hass.auth.external?.config.hasBarCodeScanner);
+ }
+
private async _loadQrScanner() {
+ if (this._nativeBarcodeScanner) {
+ this._openExternalScanner();
+ return;
+ }
+ if (!navigator.mediaDevices) {
+ return;
+ }
const QrScanner = (await import("qr-scanner")).default;
if (!(await QrScanner.hasCamera())) {
- this._error = "No camera found";
+ this._reportError("No camera found");
return;
}
QrScanner.WORKER_PATH = "/static/js/qr-scanner-worker.min.js";
this._listCameras(QrScanner);
this._qrScanner = new QrScanner(
- this._video,
+ this._video!,
this._qrCodeScanned,
this._qrCodeError
);
// @ts-ignore
const canvas = this._qrScanner.$canvas;
- this._canvasContainer.appendChild(canvas);
+ this._canvasContainer!.appendChild(canvas);
canvas.style.display = "block";
try {
await this._qrScanner.start();
} catch (err: any) {
- this._error = err;
+ this._reportError(err);
}
}
@@ -140,16 +176,16 @@ class HaQrScanner extends LitElement {
if (err === "No QR code found") {
this._qrNotFoundCount++;
if (this._qrNotFoundCount === 250) {
- this._error = err;
+ this._reportError(err);
}
return;
}
- this._error = err.message || err;
+ this._reportError(err.message || err);
// eslint-disable-next-line no-console
console.log(err);
};
- private _qrCodeScanned = async (qrCodeString: string): Promise => {
+ private _qrCodeScanned = (qrCodeString: string): void => {
this._qrNotFoundCount = 0;
fireEvent(this, "qr-code-scanned", { value: qrCodeString });
};
@@ -175,6 +211,62 @@ class HaQrScanner extends LitElement {
this._qrScanner?.setCamera((ev.target as any).value);
}
+ private _openExternalScanner() {
+ this._removeListener = addExternalBarCodeListener((msg) => {
+ if (msg.command === "bar_code/scan_result") {
+ if (msg.payload.format !== "qr_code") {
+ this._notifyExternalScanner(
+ `Wrong barcode scanned! ${msg.payload.format}: ${msg.payload.rawValue}, we need a QR code.`
+ );
+ } else {
+ this._qrCodeScanned(msg.payload.rawValue);
+ }
+ } else if (msg.command === "bar_code/aborted") {
+ this._closeExternalScanner();
+ if (msg.payload.reason === "canceled") {
+ fireEvent(this, "qr-code-closed");
+ } else {
+ this._manual = true;
+ }
+ }
+ return true;
+ });
+ this.hass.auth.external!.fireMessage({
+ type: "bar_code/scan",
+ payload: {
+ title: this.title || "Scan QR code",
+ description: this.description || "Scan a barcode.",
+ alternative_option_label:
+ this.alternativeOptionLabel || "Click to manually enter the barcode",
+ },
+ });
+ }
+
+ private _closeExternalScanner() {
+ this._removeListener?.();
+ this._removeListener = undefined;
+ this.hass.auth.external!.fireMessage({
+ type: "bar_code/close",
+ });
+ }
+
+ private _notifyExternalScanner(message: string) {
+ if (!this.hass.auth.external) {
+ return;
+ }
+ this.hass.auth.external.fireMessage({
+ type: "bar_code/notify",
+ payload: {
+ message,
+ },
+ });
+ this.error = undefined;
+ }
+
+ private _reportError(message: string) {
+ fireEvent(this, "qr-code-error", { message });
+ }
+
static styles = css`
canvas {
width: 100%;
@@ -210,6 +302,7 @@ declare global {
interface HASSDomEvents {
"qr-code-scanned": { value: string };
"qr-code-error": { message: string };
+ "qr-code-closed": undefined;
}
interface HTMLElementTagNameMap {
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 29c982bbfe..530073bb44 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
@@ -205,14 +205,13 @@ class DialogZWaveJSAddNode extends LitElement {
Search device
`
: this._status === "qr_scan"
- ? html`${this._error
- ? html`${this._error}`
- : ""}
-
${this._supportsSmartStart
- ? html`