20241105.0 (#22679)

This commit is contained in:
Paul Bottein 2024-11-05 18:47:04 +01:00 committed by GitHub
commit a6971d61d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 330 additions and 164 deletions

View File

@ -106,6 +106,14 @@ function copyMapPanel(staticDir) {
);
}
function copyZXingWasm(staticDir) {
const staticPath = genStaticPath(staticDir);
copyFileDir(
npmPath("zxing-wasm/dist/reader/zxing_reader.wasm"),
staticPath("js")
);
}
gulp.task("copy-locale-data", async () => {
const staticDir = paths.app_output_static;
copyLocaleData(staticDir);
@ -143,6 +151,7 @@ gulp.task("copy-static-app", async () => {
copyMapPanel(staticDir);
// Qr Scanner assets
copyZXingWasm(staticDir);
copyQrScannerWorker(staticDir);
});

View File

@ -1,16 +0,0 @@
import { html } from "lit";
import type { DemoConfig } from "../types";
export const demoLovelaceDescription: DemoConfig["description"] = (
localize
) => html`
<p>
${localize("ui.panel.page-demo.config.sections.description", {
blog_post: html`<a
href="https://www.home-assistant.io/blog/2024/03/04/dashboard-chapter-1/"
target="_blank"
>${localize("ui.panel.page-demo.config.sections.description_blog_post")}
</a>`,
})}
</p>
`;

View File

@ -1,5 +1,4 @@
import type { DemoConfig } from "../types";
import { demoLovelaceDescription } from "./description";
import { demoEntitiesSections } from "./entities";
import { demoLovelaceSections } from "./lovelace";
@ -7,7 +6,6 @@ export const demoSections: DemoConfig = {
authorName: "Home Assistant",
authorUrl: "https://github.com/home-assistant/frontend/",
name: "Home Demo",
description: demoLovelaceDescription,
lovelace: demoLovelaceSections,
entities: demoEntitiesSections,
theme: () => ({}),

View File

@ -98,6 +98,7 @@
"@webcomponents/scoped-custom-element-registry": "0.0.9",
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"barcode-detector": "2.2.11",
"chart.js": "4.4.6",
"color-name": "2.0.0",
"comlink": "4.4.1",

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20241104.0"
version = "20241105.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"

View File

@ -1185,6 +1185,7 @@ export class HaDataTable extends LitElement {
.group-header {
padding-top: 12px;
height: var(--data-table-row-height, 52px);
padding-left: 12px;
padding-inline-start: 12px;
padding-inline-end: initial;

View File

@ -7,6 +7,8 @@ import {
type PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { computeStateName } from "../common/entity/compute_state_name";
import { supportsFeature } from "../common/entity/supports-feature";
import {
@ -24,6 +26,13 @@ import type { HomeAssistant } from "../types";
import "./ha-hls-player";
import "./ha-web-rtc-player";
const MJPEG_STREAM = "mjpeg";
type Stream = {
type: StreamType | typeof MJPEG_STREAM;
visible: boolean;
};
@customElement("ha-camera-stream")
export class HaCameraStream extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@ -46,8 +55,6 @@ export class HaCameraStream extends LitElement {
@state() private _capabilities?: CameraCapabilities;
@state() private _streamType?: StreamType;
@state() private _hlsStreams?: { hasAudio: boolean; hasVideo: boolean };
@state() private _webRtcStreams?: { hasAudio: boolean; hasVideo: boolean };
@ -55,7 +62,6 @@ export class HaCameraStream extends LitElement {
public willUpdate(changedProps: PropertyValues): void {
if (
changedProps.has("stateObj") &&
!this._shouldRenderMJPEG &&
this.stateObj &&
(changedProps.get("stateObj") as CameraEntity | undefined)?.entity_id !==
this.stateObj.entity_id
@ -79,52 +85,63 @@ export class HaCameraStream extends LitElement {
if (!this.stateObj) {
return nothing;
}
if (__DEMO__ || this._shouldRenderMJPEG) {
const streams = this._streams(
this._capabilities?.frontend_stream_types,
this._hlsStreams,
this._webRtcStreams
);
return html`${repeat(
streams,
(stream) => stream.type + this.stateObj!.entity_id,
(stream) => this._renderStream(stream)
)}`;
}
private _renderStream(stream: Stream) {
if (!this.stateObj) {
return nothing;
}
if (stream.type === MJPEG_STREAM) {
return html`<img
.src=${__DEMO__
? this.stateObj.attributes.entity_picture!
: this._connected
? computeMJPEGStreamUrl(this.stateObj)
: ""}
: this._posterUrl || ""}
alt=${`Preview of the ${computeStateName(this.stateObj)} camera.`}
/>`;
}
return html`${this._streamType === STREAM_TYPE_HLS ||
(!this._streamType &&
this._capabilities?.frontend_stream_types.includes(STREAM_TYPE_HLS))
? html`<ha-hls-player
autoplay
playsinline
.allowExoPlayer=${this.allowExoPlayer}
.muted=${this.muted}
.controls=${this.controls}
.hass=${this.hass}
.entityid=${this.stateObj.entity_id}
.posterUrl=${this._posterUrl}
@streams=${this._handleHlsStreams}
class=${!this._streamType && this._webRtcStreams?.hasVideo
? "hidden"
: ""}
></ha-hls-player>`
: nothing}
${this._streamType === STREAM_TYPE_WEB_RTC ||
(!this._streamType &&
this._capabilities?.frontend_stream_types.includes(STREAM_TYPE_WEB_RTC))
? html`<ha-web-rtc-player
autoplay
playsinline
.muted=${this.muted}
.controls=${this.controls}
.hass=${this.hass}
.entityid=${this.stateObj.entity_id}
.posterUrl=${this._posterUrl}
@streams=${this._handleWebRtcStreams}
class=${this._streamType !== STREAM_TYPE_WEB_RTC &&
!this._webRtcStreams
? "hidden"
: ""}
></ha-web-rtc-player>`
: nothing}`;
if (stream.type === STREAM_TYPE_HLS) {
return html`<ha-hls-player
autoplay
playsinline
.allowExoPlayer=${this.allowExoPlayer}
.muted=${this.muted}
.controls=${this.controls}
.hass=${this.hass}
.entityid=${this.stateObj.entity_id}
.posterUrl=${this._posterUrl}
@streams=${this._handleHlsStreams}
class=${stream.visible ? "" : "hidden"}
></ha-hls-player>`;
}
if (stream.type === STREAM_TYPE_WEB_RTC) {
return html`<ha-web-rtc-player
autoplay
playsinline
.muted=${this.muted}
.controls=${this.controls}
.hass=${this.hass}
.entityid=${this.stateObj.entity_id}
.posterUrl=${this._posterUrl}
@streams=${this._handleWebRtcStreams}
class=${stream.visible ? "" : "hidden"}
></ha-web-rtc-player>`;
}
return nothing;
}
private async _getCapabilities() {
@ -132,35 +149,13 @@ export class HaCameraStream extends LitElement {
this._hlsStreams = undefined;
this._webRtcStreams = undefined;
if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) {
this._capabilities = { frontend_stream_types: [] };
return;
}
this._capabilities = await fetchCameraCapabilities(
this.hass!,
this.stateObj!.entity_id
);
if (this._capabilities.frontend_stream_types.length === 1) {
this._streamType = this._capabilities.frontend_stream_types[0];
}
}
private get _shouldRenderMJPEG() {
if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) {
// Steaming is not supported by the camera so fallback to MJPEG stream
return true;
}
if (
this._capabilities &&
(!this._capabilities.frontend_stream_types.includes(STREAM_TYPE_HLS) ||
this._hlsStreams?.hasVideo === false) &&
(!this._capabilities.frontend_stream_types.includes(
STREAM_TYPE_WEB_RTC
) ||
this._webRtcStreams?.hasVideo === false)
) {
// No video in HLS stream and no video in WebRTC stream
return true;
}
return false;
}
private async _getPosterUrl(): Promise<void> {
@ -179,28 +174,87 @@ export class HaCameraStream extends LitElement {
private _handleHlsStreams(ev: CustomEvent) {
this._hlsStreams = ev.detail;
this._pickStreamType();
}
private _handleWebRtcStreams(ev: CustomEvent) {
this._webRtcStreams = ev.detail;
this._pickStreamType();
}
private _pickStreamType() {
if (!this._hlsStreams || !this._webRtcStreams) {
return;
private _streams = memoizeOne(
(
supportedTypes?: StreamType[],
hlsStreams?: { hasAudio: boolean; hasVideo: boolean },
webRtcStreams?: { hasAudio: boolean; hasVideo: boolean }
): Stream[] => {
if (__DEMO__) {
return [{ type: MJPEG_STREAM, visible: true }];
}
if (!supportedTypes) {
return [];
}
if (supportedTypes.length === 0) {
// doesn't support any stream type, fallback to mjpeg
return [{ type: MJPEG_STREAM, visible: true }];
}
if (supportedTypes.length === 1) {
// only 1 stream type, no need to choose
if (
(supportedTypes[0] === STREAM_TYPE_HLS &&
hlsStreams?.hasVideo === false) ||
(supportedTypes[0] === STREAM_TYPE_WEB_RTC &&
webRtcStreams?.hasVideo === false)
) {
// stream failed to load, fallback to mjpeg
return [{ type: MJPEG_STREAM, visible: true }];
}
return [{ type: supportedTypes[0], visible: true }];
}
if (hlsStreams && webRtcStreams) {
// fully loaded
if (
hlsStreams.hasVideo &&
hlsStreams.hasAudio &&
!webRtcStreams.hasAudio
) {
// webRTC stream is missing audio, use HLS
return [{ type: STREAM_TYPE_HLS, visible: true }];
}
if (webRtcStreams.hasVideo) {
return [{ type: STREAM_TYPE_WEB_RTC, visible: true }];
}
// both streams failed to load, fallback to mjpeg
return [{ type: MJPEG_STREAM, visible: true }];
}
if (hlsStreams?.hasVideo !== webRtcStreams?.hasVideo) {
// one of the two streams is loaded, or errored
// choose the one that has video or is still loading
if (hlsStreams?.hasVideo) {
return [
{ type: STREAM_TYPE_HLS, visible: true },
{ type: STREAM_TYPE_WEB_RTC, visible: false },
];
}
if (hlsStreams?.hasVideo === false) {
return [{ type: STREAM_TYPE_WEB_RTC, visible: true }];
}
if (webRtcStreams?.hasVideo) {
return [
{ type: STREAM_TYPE_WEB_RTC, visible: true },
{ type: STREAM_TYPE_HLS, visible: false },
];
}
if (webRtcStreams?.hasVideo === false) {
return [{ type: STREAM_TYPE_HLS, visible: true }];
}
}
return [
{ type: STREAM_TYPE_HLS, visible: true },
{ type: STREAM_TYPE_WEB_RTC, visible: false },
];
}
if (
this._hlsStreams.hasVideo &&
this._hlsStreams.hasAudio &&
!this._webRtcStreams.hasAudio
) {
this._streamType = STREAM_TYPE_HLS;
} else if (this._webRtcStreams.hasVideo) {
this._streamType = STREAM_TYPE_WEB_RTC;
}
}
);
static get styles(): CSSResultGroup {
return css`

View File

@ -182,6 +182,10 @@ export class HaMdDialog extends MdDialog {
display: contents;
}
.scroller {
overflow: var(--dialog-content-overflow, auto);
}
slot[name="content"]::slotted(*) {
padding: var(--dialog-content-padding, 24px);
}

View File

@ -0,0 +1,26 @@
import { MdSelectOption } from "@material/web/select/select-option";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-md-select-option")
export class HaMdSelectOption extends MdSelectOption {
static override styles = [
...super.styles,
css`
:host {
--ha-icon-display: block;
--md-sys-color-primary: var(--primary-text-color);
--md-sys-color-secondary: var(--secondary-text-color);
--md-sys-color-surface: var(--card-background-color);
--md-sys-color-on-surface: var(--primary-text-color);
--md-sys-color-on-surface-variant: var(--secondary-text-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-md-select-option": HaMdSelectOption;
}
}

View File

@ -0,0 +1,32 @@
import { MdFilledSelect } from "@material/web/select/filled-select";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-md-select")
export class HaMdSelect extends MdFilledSelect {
static override styles = [
...super.styles,
css`
:host {
--ha-icon-display: block;
--md-sys-color-primary: var(--primary-text-color);
--md-sys-color-secondary: var(--secondary-text-color);
--md-sys-color-surface: var(--card-background-color);
--md-sys-color-on-surface-variant: var(--secondary-text-color);
--md-sys-color-surface-container-highest: var(--input-fill-color);
--md-sys-color-on-surface: var(--input-ink-color);
--md-sys-color-surface-container: var(--input-fill-color);
--md-sys-color-secondary-container: var(--input-fill-color);
--md-menu-container-color: var(--card-background-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-md-select": HaMdSelect;
}
}

View File

@ -4,6 +4,11 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
// The BarcodeDetector Web API is not yet supported in all browsers,
// and "qr-scanner" defaults to a suboptimal implementation if it is not available.
// The following import makes a better implementation available that is based on a
// WebAssembly port of ZXing:
import { setZXingModuleOverrides } from "barcode-detector";
import type QrScanner from "qr-scanner";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
@ -16,6 +21,15 @@ import "./ha-list-item";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
setZXingModuleOverrides({
locateFile: (path: string, prefix: string) => {
if (path.endsWith(".wasm")) {
return "/static/js/zxing_reader.wasm";
}
return prefix + path;
},
});
@customElement("ha-qr-scanner")
class HaQrScanner extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -174,7 +188,7 @@ class HaQrScanner extends LitElement {
}
private _qrCodeError = (err: any) => {
if (err === "No QR code found") {
if (err.endsWith("No QR code found")) {
this._qrNotFoundCount++;
if (this._qrNotFoundCount === 250) {
this._reportError(err);

View File

@ -1,4 +1,3 @@
/* eslint-disable no-console */
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement } from "lit";
@ -108,18 +107,18 @@ class HaWebRtcPlayer extends LitElement {
return;
}
console.time("WebRTC");
this._error = undefined;
console.timeLog("WebRTC", "start clientConfig");
this._startTimer();
this._logEvent("start clientConfig");
this._clientConfig = await fetchWebRtcClientConfiguration(
this.hass,
this.entityid
);
console.timeLog("WebRTC", "end clientConfig", this._clientConfig);
this._logEvent("end clientConfig", this._clientConfig);
this._peerConnection = new RTCPeerConnection(
this._clientConfig.configuration
@ -141,11 +140,10 @@ class HaWebRtcPlayer extends LitElement {
this._peerConnection.onsignalingstatechange = (ev) => {
switch ((ev.target as RTCPeerConnection).signalingState) {
case "stable":
console.timeLog("WebRTC", "ICE negotiation complete");
this._logEvent("ICE negotiation complete");
break;
default:
console.timeLog(
"WebRTC",
this._logEvent(
"Signaling state changed",
(ev.target as RTCPeerConnection).signalingState
);
@ -170,7 +168,7 @@ class HaWebRtcPlayer extends LitElement {
offerToReceiveVideo: true,
};
console.timeLog("WebRTC", "start createOffer", offerOptions);
this._logEvent("start createOffer", offerOptions);
const offer: RTCSessionDescriptionInit =
await this._peerConnection.createOffer(offerOptions);
@ -179,9 +177,9 @@ class HaWebRtcPlayer extends LitElement {
return;
}
console.timeLog("WebRTC", "end createOffer", offer);
this._logEvent("end createOffer", offer);
console.timeLog("WebRTC", "start setLocalDescription");
this._logEvent("start setLocalDescription");
await this._peerConnection.setLocalDescription(offer);
@ -189,7 +187,7 @@ class HaWebRtcPlayer extends LitElement {
return;
}
console.timeLog("WebRTC", "end setLocalDescription");
this._logEvent("end setLocalDescription");
let candidates = "";
@ -203,11 +201,7 @@ class HaWebRtcPlayer extends LitElement {
resolve();
}
console.timeLog(
"WebRTC",
"Ice gathering state changed",
iceGatheringState
);
this._logEvent("Ice gathering state changed", iceGatheringState);
};
});
@ -225,7 +219,7 @@ class HaWebRtcPlayer extends LitElement {
const offer_sdp = offer.sdp! + candidates;
console.timeLog("WebRTC", "start webRtcOffer", offer_sdp);
this._logEvent("start webRtcOffer", offer_sdp);
try {
this._unsub = webRtcOffer(this.hass, this.entityid, offer_sdp, (event) =>
@ -238,8 +232,7 @@ class HaWebRtcPlayer extends LitElement {
};
private _iceConnectionStateChanged = () => {
console.timeLog(
"WebRTC",
this._logEvent(
"ice connection state change",
this._peerConnection?.iceConnectionState
);
@ -265,18 +258,19 @@ class HaWebRtcPlayer extends LitElement {
this._candidatesList = [];
}
if (event.type === "answer") {
console.timeLog("WebRTC", "answer", event.answer);
this._logEvent("answer", event.answer);
this._handleAnswer(event);
}
if (event.type === "candidate") {
console.timeLog("WebRTC", "remote ice candidate", event.candidate);
this._logEvent("remote ice candidate", event.candidate);
try {
await this._peerConnection?.addIceCandidate(
new RTCIceCandidate({ candidate: event.candidate, sdpMid: "0" })
);
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(err);
}
}
@ -291,11 +285,7 @@ class HaWebRtcPlayer extends LitElement {
return;
}
console.timeLog(
"WebRTC",
"local ice candidate",
event.candidate?.candidate
);
this._logEvent("local ice candidate", event.candidate?.candidate);
if (this._sessionId) {
addWebRtcCandidate(
@ -334,19 +324,16 @@ class HaWebRtcPlayer extends LitElement {
sdp: event.answer,
});
try {
console.timeLog("WebRTC", "start setRemoteDescription", remoteDesc);
this._logEvent("start setRemoteDescription", remoteDesc);
await this._peerConnection.setRemoteDescription(remoteDesc);
} catch (err: any) {
this._error = "Failed to connect WebRTC stream: " + err.message;
this._cleanUp();
}
console.timeLog("WebRTC", "end setRemoteDescription");
this._logEvent("end setRemoteDescription");
}
private _cleanUp() {
console.timeLog("WebRTC", "stopped");
console.timeEnd("WebRTC");
if (this._remoteStream) {
this._remoteStream.getTracks().forEach((track) => {
track.stop();
@ -372,6 +359,9 @@ class HaWebRtcPlayer extends LitElement {
this._peerConnection.onsignalingstatechange = null;
this._peerConnection = undefined;
this._logEvent("stopped");
this._stopTimer();
}
this._unsub?.then((unsub) => unsub());
this._unsub = undefined;
@ -380,17 +370,43 @@ class HaWebRtcPlayer extends LitElement {
}
private _loadedData() {
console.timeLog("WebRTC", "loadedData");
console.timeEnd("WebRTC");
const video = this._videoEl;
const stream = video.srcObject as MediaStream;
fireEvent(this, "load");
fireEvent(this, "streams", {
const data = {
hasAudio: Boolean(stream?.getAudioTracks().length),
hasVideo: Boolean(stream?.getVideoTracks().length),
});
};
fireEvent(this, "load");
fireEvent(this, "streams", data);
this._logEvent("loadedData", data);
this._stopTimer();
}
private _startTimer() {
if (!__DEV__) {
return;
}
// eslint-disable-next-line no-console
console.time("WebRTC");
}
private _stopTimer() {
if (!__DEV__) {
return;
}
// eslint-disable-next-line no-console
console.timeEnd("WebRTC");
}
private _logEvent(msg: string, ...args: unknown[]) {
if (!__DEV__) {
return;
}
// eslint-disable-next-line no-console
console.timeLog("WebRTC", msg, ...args);
}
static get styles(): CSSResultGroup {

View File

@ -690,9 +690,6 @@ class DialogZWaveJSAddNode extends LitElement {
provisioningInfo
);
this._status = "provisioned";
if (this._params?.addedCallback) {
this._params.addedCallback();
}
} catch (err: any) {
this._error = err.message;
this._status = "failed";
@ -831,9 +828,6 @@ class DialogZWaveJSAddNode extends LitElement {
if (message.event === "interview completed") {
this._unsubscribe();
this._status = "finished";
if (this._params?.addedCallback) {
this._params.addedCallback();
}
}
if (message.event === "interview stage completed") {
@ -874,6 +868,9 @@ class DialogZWaveJSAddNode extends LitElement {
}
if (this._entryId) {
stopZwaveInclusion(this.hass, this._entryId);
if (this._params?.onStop) {
this._params.onStop();
}
}
this._requestedGrant = undefined;
this._dsk = undefined;

View File

@ -2,7 +2,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
export interface ZWaveJSAddNodeDialogParams {
entry_id: string;
addedCallback?: () => void;
onStop?: () => void;
}
export const loadAddNodeDialog = () => import("./dialog-zwave_js-add-node");

View File

@ -564,7 +564,8 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
private async _addNodeClicked() {
showZWaveJSAddNodeDialog(this, {
entry_id: this.configEntryId!,
addedCallback: () => this._fetchData(),
// refresh the data after the dialog is closed. add a small delay for the inclusion state to update
onStop: () => setTimeout(() => this._fetchData(), 100),
});
}

View File

@ -2,21 +2,20 @@ import { mdiClose } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import "../../../components/ha-md-dialog";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-button";
import "../../../components/ha-dialog-header";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../components/ha-md-dialog";
import type { HomeAssistant } from "../../../types";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import { fireEvent } from "../../../common/dom/fire_event";
import type { DownloadLogsDialogParams } from "./show-dialog-download-logs";
import "../../../components/ha-select";
import "../../../components/ha-list-item";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { getHassioLogDownloadLinesUrl } from "../../../data/hassio/supervisor";
import "../../../components/ha-md-select";
import "../../../components/ha-md-select-option";
import { getSignedPath } from "../../../data/auth";
import { getHassioLogDownloadLinesUrl } from "../../../data/hassio/supervisor";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { fileDownload } from "../../../util/file_download";
import type { DownloadLogsDialogParams } from "./show-dialog-download-logs";
@customElement("dialog-download-logs")
class DownloadLogsDialog extends LitElement {
@ -78,22 +77,19 @@ class DownloadLogsDialog extends LitElement {
"ui.panel.config.logs.select_number_of_lines"
)}:
</div>
<ha-select
<ha-md-select
.label=${this.hass.localize("ui.panel.config.logs.lines")}
@selected=${this._setNumberOfLogs}
fixedMenuPosition
naturalMenuWidth
@closed=${stopPropagation}
@change=${this._setNumberOfLogs}
.value=${String(this._lineCount)}
>
${numberOfLinesOptions.map(
(option) => html`
<ha-list-item .value=${String(option)}>
<ha-md-select-option .value=${String(option)}>
${option}
</ha-list-item>
</ha-md-select-option>
`
)}
</ha-select>
</ha-md-select>
</div>
<div slot="actions">
<ha-button @click=${this.closeDialog}>
@ -137,6 +133,7 @@ class DownloadLogsDialog extends LitElement {
css`
:host {
direction: var(--direction);
--dialog-content-overflow: visible;
}
.content {
display: flex;

View File

@ -6887,8 +6887,6 @@
}
},
"sections": {
"description": "This dashboard is using the sections view released in Home Assistant 2024.3. Learn more about it in this {blog_post}.",
"description_blog_post": "blog post",
"titles": {
"welcome": "Welcome",
"living_room": "Living room",

View File

@ -3944,6 +3944,20 @@ __metadata:
languageName: node
linkType: hard
"@types/dom-webcodecs@npm:^0.1.13":
version: 0.1.13
resolution: "@types/dom-webcodecs@npm:0.1.13"
checksum: 10/99cb227416725efd4b22175ef18988ae3fc728480fe6ed2192777d7dba52d18af540b4df49fcfa3cf73753d1dcf9d5399b089702bde215d78679284848b078f7
languageName: node
linkType: hard
"@types/emscripten@npm:^1.39.13":
version: 1.39.13
resolution: "@types/emscripten@npm:1.39.13"
checksum: 10/02c0446150f9cc2c74dc3a551f86ce13df266c33d8b98d11d9f17263e2d98a6a6b4d36bdd15066c4e1547ae1ed2d52eed9420116b4935d119009e0f53ddbb041
languageName: node
linkType: hard
"@types/eslint-scope@npm:^3.7.7":
version: 3.7.7
resolution: "@types/eslint-scope@npm:3.7.7"
@ -5742,6 +5756,16 @@ __metadata:
languageName: node
linkType: hard
"barcode-detector@npm:2.2.11":
version: 2.2.11
resolution: "barcode-detector@npm:2.2.11"
dependencies:
"@types/dom-webcodecs": "npm:^0.1.13"
zxing-wasm: "npm:1.2.14"
checksum: 10/91f04ac8a73a5fccf15d08c2b148e3f9584448956de9b2506f5c5c3213ba133c504cd6890926f4fde2b1294e42b9943f979820440bc8373388d5a86cb6c764c5
languageName: node
linkType: hard
"bare-events@npm:^2.2.0":
version: 2.5.0
resolution: "bare-events@npm:2.5.0"
@ -8833,6 +8857,7 @@ __metadata:
app-datepicker: "npm:5.1.1"
babel-loader: "npm:9.2.1"
babel-plugin-template-html-minifier: "npm:4.1.0"
barcode-detector: "npm:2.2.11"
browserslist-useragent-regexp: "npm:4.1.3"
chai: "npm:5.1.2"
chart.js: "npm:4.4.6"
@ -15465,3 +15490,12 @@ __metadata:
checksum: 10/f2e05b767ed3141e6372a80af9caa4715d60969227f38b1a4370d60bffe153c9c5b33a862905609afc9b375ec57cd40999810d20e5e10229a204e8bde7ef255c
languageName: node
linkType: hard
"zxing-wasm@npm:1.2.14":
version: 1.2.14
resolution: "zxing-wasm@npm:1.2.14"
dependencies:
"@types/emscripten": "npm:^1.39.13"
checksum: 10/02ea0408553f1aebb412a97c5e11887c58da1b2adff636302232535e752b91bdb3f358411259b6cddd34b341df565336bc03f48895e362f61316018d9bbff8c3
languageName: node
linkType: hard