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`