Skip to content

Commit

Permalink
20240627.0 (#21192)
Browse files Browse the repository at this point in the history
  • Loading branch information
bramkragten authored Jun 27, 2024
2 parents 5bc3ad4 + da2865d commit d72e8c3
Show file tree
Hide file tree
Showing 22 changed files with 280 additions and 107 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@
"@rollup/plugin-node-resolve": "15.2.3",
"@rollup/plugin-replace": "5.0.7",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.15",
"@types/chromecast-caf-receiver": "6.0.16",
"@types/chromecast-caf-sender": "1.0.10",
"@types/color-name": "1.1.4",
"@types/glob": "8.1.0",
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "home-assistant-frontend"
version = "20240626.2"
version = "20240627.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
Expand Down
161 changes: 127 additions & 34 deletions src/components/ha-qr-scanner.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -80,55 +100,71 @@ 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
.label=${this.localize("ui.components.qr-scanner.enter_qr_code")}
@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);
}
}

Expand All @@ -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 });
};
Expand All @@ -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%;
Expand Down Expand Up @@ -210,6 +302,7 @@ declare global {
interface HASSDomEvents {
"qr-code-scanned": { value: string };
"qr-code-error": { message: string };
"qr-code-closed": undefined;
}

interface HTMLElementTagNameMap {
Expand Down
13 changes: 12 additions & 1 deletion src/components/ha-selector/ha-selector-area.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
fetchEntitySourcesWithCache,
} from "../../data/entity_sources";
import type { AreaSelector } from "../../data/selector";
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
import {
filterSelectorDevices,
filterSelectorEntities,
Expand All @@ -37,6 +38,8 @@ export class HaAreaSelector extends LitElement {

@state() private _entitySources?: EntitySources;

@state() private _configEntries?: ConfigEntry[];

private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);

private _hasIntegration(selector: AreaSelector) {
Expand Down Expand Up @@ -72,6 +75,12 @@ export class HaAreaSelector extends LitElement {
this._entitySources = sources;
});
}
if (!this._configEntries && this._hasIntegration(this.selector)) {
this._configEntries = [];
getConfigEntries(this.hass).then((entries) => {
this._configEntries = entries;
});
}
}

protected render() {
Expand Down Expand Up @@ -136,7 +145,9 @@ export class HaAreaSelector extends LitElement {
const deviceIntegrations = this._entitySources
? this._deviceIntegrationLookup(
this._entitySources,
Object.values(this.hass.entities)
Object.values(this.hass.entities),
Object.values(this.hass.devices),
this._configEntries
)
: undefined;

Expand Down
13 changes: 12 additions & 1 deletion src/components/ha-selector/ha-selector-device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
fetchEntitySourcesWithCache,
} from "../../data/entity_sources";
import type { DeviceSelector } from "../../data/selector";
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
import {
filterSelectorDevices,
filterSelectorEntities,
Expand All @@ -27,6 +28,8 @@ export class HaDeviceSelector extends LitElement {

@state() private _entitySources?: EntitySources;

@state() private _configEntries?: ConfigEntry[];

@property() public value?: any;

@property() public label?: string;
Expand Down Expand Up @@ -75,6 +78,12 @@ export class HaDeviceSelector extends LitElement {
this._entitySources = sources;
});
}
if (!this._configEntries && this._hasIntegration(this.selector)) {
this._configEntries = [];
getConfigEntries(this.hass).then((entries) => {
this._configEntries = entries;
});
}
}

protected render() {
Expand Down Expand Up @@ -123,7 +132,9 @@ export class HaDeviceSelector extends LitElement {
const deviceIntegrations = this._entitySources
? this._deviceIntegrationLookup(
this._entitySources,
Object.values(this.hass.entities)
Object.values(this.hass.entities),
Object.values(this.hass.devices),
this._configEntries
)
: undefined;

Expand Down
Loading

0 comments on commit d72e8c3

Please sign in to comment.