mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Add support for native QR code scanner (#21187)
This commit is contained in:
parent
7603fa3aa8
commit
49c42fc757
@ -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`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
protected render() {
|
||||
if (this._nativeBarcodeScanner && !this._manual) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`${this.error
|
||||
? html`<ha-alert alert-type="error">${this.error}</ha-alert>`
|
||||
: ""}
|
||||
${navigator.mediaDevices
|
||||
${navigator.mediaDevices && !this._manual
|
||||
? html`<video></video>
|
||||
<div id="canvas-container">
|
||||
${this._cameras && this._cameras.length > 1
|
||||
@ -80,21 +100,26 @@ class HaQrScanner extends LitElement {
|
||||
></ha-icon-button>
|
||||
${this._cameras!.map(
|
||||
(camera) => html`
|
||||
<mwc-list-item
|
||||
<ha-list-item
|
||||
.value=${camera.id}
|
||||
@click=${this._cameraChanged}
|
||||
>${camera.label}</mwc-list-item
|
||||
>
|
||||
${camera.label}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-button-menu>`
|
||||
: ""}
|
||||
: nothing}
|
||||
</div>`
|
||||
: html`<ha-alert alert-type="warning">
|
||||
${!window.isSecureContext
|
||||
? this.localize("ui.components.qr-scanner.only_https_supported")
|
||||
: this.localize("ui.components.qr-scanner.not_supported")}
|
||||
</ha-alert>
|
||||
: html`${this._manual
|
||||
? nothing
|
||||
: html`<ha-alert alert-type="warning">
|
||||
${!window.isSecureContext
|
||||
? this.localize(
|
||||
"ui.components.qr-scanner.only_https_supported"
|
||||
)
|
||||
: this.localize("ui.components.qr-scanner.not_supported")}
|
||||
</ha-alert>`}
|
||||
<p>${this.localize("ui.components.qr-scanner.manual_input")}</p>
|
||||
<div class="row">
|
||||
<ha-textfield
|
||||
@ -102,33 +127,44 @@ class HaQrScanner extends LitElement {
|
||||
@keyup=${this._manualKeyup}
|
||||
@paste=${this._manualPaste}
|
||||
></ha-textfield>
|
||||
<mwc-button @click=${this._manualSubmit}
|
||||
>${this.localize("ui.common.submit")}</mwc-button
|
||||
>
|
||||
<mwc-button @click=${this._manualSubmit}>
|
||||
${this.localize("ui.common.submit")}
|
||||
</mwc-button>
|
||||
</div>`}`;
|
||||
}
|
||||
|
||||
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<void> => {
|
||||
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 {
|
||||
|
@ -205,14 +205,13 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
Search device
|
||||
</mwc-button>`
|
||||
: this._status === "qr_scan"
|
||||
? html`${this._error
|
||||
? html`<ha-alert alert-type="error"
|
||||
>${this._error}</ha-alert
|
||||
>`
|
||||
: ""}
|
||||
<ha-qr-scanner
|
||||
? html` <ha-qr-scanner
|
||||
.hass=${this.hass}
|
||||
.localize=${this.hass.localize}
|
||||
.error=${this._error}
|
||||
@qr-code-scanned=${this._qrCodeScanned}
|
||||
@qr-code-error=${this._qrCodeError}
|
||||
@qr-code-closed=${this._startOver}
|
||||
></ha-qr-scanner>
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
@ -361,7 +360,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
</p>
|
||||
</div>
|
||||
${this._supportsSmartStart
|
||||
? html` <div class="outline">
|
||||
? html`<div class="outline">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.qr_code"
|
||||
@ -498,9 +497,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
<a
|
||||
href=${`/config/devices/device/${
|
||||
this._device?.id
|
||||
}`}
|
||||
href=${`/config/devices/device/${this._device?.id}`}
|
||||
>
|
||||
<mwc-button>
|
||||
${this.hass.localize(
|
||||
@ -599,6 +596,10 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
this._handleQrCodeScanned(ev.detail.value);
|
||||
}
|
||||
|
||||
private _qrCodeError(ev: CustomEvent): void {
|
||||
this._error = ev.detail.message;
|
||||
}
|
||||
|
||||
private async _handleQrCodeScanned(qrCodeString: string): Promise<void> {
|
||||
this._error = undefined;
|
||||
if (this._status !== "qr_scan" || this._qrProcessing) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user