diff --git a/package.json b/package.json index 48258f74c1a2..d67d582af2a9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pyproject.toml b/pyproject.toml index 28a3be951139..48b7f8fcea7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/src/components/ha-qr-scanner.ts b/src/components/ha-qr-scanner.ts index b65f9ba07aa1..b6fdef9e8984 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/components/ha-selector/ha-selector-area.ts b/src/components/ha-selector/ha-selector-area.ts index 7690c387adfd..47230ff88d11 100644 --- a/src/components/ha-selector/ha-selector-area.ts +++ b/src/components/ha-selector/ha-selector-area.ts @@ -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, @@ -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) { @@ -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() { @@ -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; diff --git a/src/components/ha-selector/ha-selector-device.ts b/src/components/ha-selector/ha-selector-device.ts index b3ffebd4b667..0131b9b69854 100644 --- a/src/components/ha-selector/ha-selector-device.ts +++ b/src/components/ha-selector/ha-selector-device.ts @@ -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, @@ -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; @@ -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() { @@ -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; diff --git a/src/components/ha-selector/ha-selector-floor.ts b/src/components/ha-selector/ha-selector-floor.ts index eac63f414edb..6d4d0d29c834 100644 --- a/src/components/ha-selector/ha-selector-floor.ts +++ b/src/components/ha-selector/ha-selector-floor.ts @@ -11,6 +11,7 @@ import { fetchEntitySourcesWithCache, } from "../../data/entity_sources"; import type { FloorSelector } from "../../data/selector"; +import { ConfigEntry, getConfigEntries } from "../../data/config_entries"; import { filterSelectorDevices, filterSelectorEntities, @@ -37,6 +38,8 @@ export class HaFloorSelector extends LitElement { @state() private _entitySources?: EntitySources; + @state() private _configEntries?: ConfigEntry[]; + private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup); private _hasIntegration(selector: FloorSelector) { @@ -72,6 +75,12 @@ export class HaFloorSelector extends LitElement { this._entitySources = sources; }); } + if (!this._configEntries && this._hasIntegration(this.selector)) { + this._configEntries = []; + getConfigEntries(this.hass).then((entries) => { + this._configEntries = entries; + }); + } } protected render() { @@ -136,7 +145,9 @@ export class HaFloorSelector 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; diff --git a/src/data/device_registry.ts b/src/data/device_registry.ts index 80e9a5b05373..90bd894e8a62 100644 --- a/src/data/device_registry.ts +++ b/src/data/device_registry.ts @@ -5,6 +5,7 @@ import type { EntityRegistryDisplayEntry, EntityRegistryEntry, } from "./entity_registry"; +import { ConfigEntry } from "./config_entries"; import type { EntitySources } from "./entity_sources"; export { @@ -142,9 +143,11 @@ export const getDeviceEntityDisplayLookup = ( export const getDeviceIntegrationLookup = ( entitySources: EntitySources, - entities: EntityRegistryDisplayEntry[] | EntityRegistryEntry[] -): Record => { - const deviceIntegrations: Record = {}; + entities: EntityRegistryDisplayEntry[] | EntityRegistryEntry[], + devices?: DeviceRegistryEntry[], + configEntries?: ConfigEntry[] +): Record> => { + const deviceIntegrations: Record> = {}; for (const entity of entities) { const source = entitySources[entity.entity_id]; @@ -152,10 +155,22 @@ export const getDeviceIntegrationLookup = ( continue; } - if (!deviceIntegrations[entity.device_id!]) { - deviceIntegrations[entity.device_id!] = []; + deviceIntegrations[entity.device_id!] = + deviceIntegrations[entity.device_id!] || new Set(); + deviceIntegrations[entity.device_id!].add(source.domain); + } + // Lookup devices that have no entities + if (devices && configEntries) { + for (const device of devices) { + for (const config_entry_id of device.config_entries) { + const entry = configEntries.find((e) => e.entry_id === config_entry_id); + if (entry?.domain) { + deviceIntegrations[device.id] = + deviceIntegrations[device.id] || new Set(); + deviceIntegrations[device.id].add(entry.domain); + } + } } - deviceIntegrations[entity.device_id!].push(source.domain); } return deviceIntegrations; }; diff --git a/src/data/selector.ts b/src/data/selector.ts index bb90c792f48b..60b9e4973b1c 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -696,7 +696,7 @@ export const entityMeetsTargetSelector = ( export const filterSelectorDevices = ( filterDevice: DeviceSelectorFilter, device: DeviceRegistryEntry, - deviceIntegrationLookup?: Record | undefined + deviceIntegrationLookup?: Record> | undefined ): boolean => { const { manufacturer: filterManufacturer, @@ -713,7 +713,7 @@ export const filterSelectorDevices = ( } if (filterIntegration && deviceIntegrationLookup) { - if (!deviceIntegrationLookup?.[device.id]?.includes(filterIntegration)) { + if (!deviceIntegrationLookup?.[device.id]?.has(filterIntegration)) { return false; } } 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 29c982bbfe7f..530073bb44b2 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`
+ ? html`

${this.hass.localize( "ui.panel.config.zwave_js.add_node.qr_code" @@ -498,9 +497,7 @@ class DialogZWaveJSAddNode extends LitElement { ` : ""} ${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 { this._error = undefined; if (this._status !== "qr_scan" || this._qrProcessing) { diff --git a/src/panels/lovelace/cards/hui-card.ts b/src/panels/lovelace/cards/hui-card.ts index a892d4764dcf..b4ca36b94e86 100644 --- a/src/panels/lovelace/cards/hui-card.ts +++ b/src/panels/lovelace/cards/hui-card.ts @@ -1,4 +1,4 @@ -import { PropertyValueMap, PropertyValues, ReactiveElement } from "lit"; +import { PropertyValues, ReactiveElement } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; import { MediaQueriesListener } from "../../../common/dom/media_query"; @@ -23,35 +23,29 @@ declare global { @customElement("hui-card") export class HuiCard extends ReactiveElement { - @property({ attribute: false }) public hass?: HomeAssistant; - @property({ type: Boolean }) public preview = false; @property({ type: Boolean }) public isPanel = false; - set config(config: LovelaceCardConfig | undefined) { - if (!config) return; - if (config.type !== this._config?.type) { - this._buildElement(config); - } else if (config !== this.config) { - this._element?.setConfig(config); - fireEvent(this, "card-updated"); - } - this._config = config; - } + @property({ attribute: false }) public config?: LovelaceCardConfig; - @property({ attribute: false }) - public get config() { - return this._config; - } + @property({ attribute: false }) public hass?: HomeAssistant; - private _config?: LovelaceCardConfig; + public load() { + if (!this.config) { + throw new Error("Cannot build card without config"); + } + this._loadElement(this.config); + } private _element?: LovelaceCard; private _listeners: MediaQueriesListener[] = []; protected createRenderRoot() { + const style = document.createElement("style"); + style.textContent = `hui-card { display: contents }`; + this.append(style); return this; } @@ -90,57 +84,76 @@ export class HuiCard extends ReactiveElement { return this._element?.getLayoutOptions?.() ?? {}; } - private _createElement(config: LovelaceCardConfig) { - const element = createCardElement(config); - element.hass = this.hass; - element.preview = this.preview; + private _loadElement(config: LovelaceCardConfig) { + this._element = createCardElement(config); + if (this.hass) { + this._element.hass = this.hass; + } + this._element.preview = this.preview; // For backwards compatibility - (element as any).editMode = this.preview; + (this._element as any).editMode = this.preview; // Update element when the visibility of the card changes (e.g. conditional card or filter card) - element.addEventListener("card-visibility-changed", (ev: Event) => { + this._element.addEventListener("card-visibility-changed", (ev: Event) => { ev.stopPropagation(); this._updateVisibility(); }); - element.addEventListener( + this._element.addEventListener( "ll-upgrade", (ev: Event) => { ev.stopPropagation(); - element.hass = this.hass; - element.preview = this.preview; + if (this.hass) { + this._element!.hass = this.hass; + } fireEvent(this, "card-updated"); }, { once: true } ); - element.addEventListener( + this._element.addEventListener( "ll-rebuild", (ev: Event) => { ev.stopPropagation(); - this._buildElement(config); + this._loadElement(config); fireEvent(this, "card-updated"); }, { once: true } ); - return element; - } - - private _buildElement(config: LovelaceCardConfig) { - this._element = this._createElement(config); - while (this.lastChild) { this.removeChild(this.lastChild); } this._updateVisibility(); } + protected willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); + + if (!this._element) { + this.load(); + } + } + protected update(changedProps: PropertyValues) { super.update(changedProps); if (this._element) { + if (changedProps.has("config") && this.hasUpdated) { + const oldConfig = changedProps.get("config"); + if (this.config !== oldConfig && this.config) { + const typeChanged = this.config?.type !== oldConfig?.type; + if (typeChanged) { + this._loadElement(this.config); + } else { + this._element?.setConfig(this.config); + fireEvent(this, "card-updated"); + } + } + } if (changedProps.has("hass")) { try { - this._element.hass = this.hass; + if (this.hass) { + this._element.hass = this.hass; + } } catch (e: any) { - this._buildElement(createErrorCardConfig(e.message, null)); + this._loadElement(createErrorCardConfig(e.message, null)); } } if (changedProps.has("preview")) { @@ -149,18 +162,14 @@ export class HuiCard extends ReactiveElement { // For backwards compatibility (this._element as any).editMode = this.preview; } catch (e: any) { - this._buildElement(createErrorCardConfig(e.message, null)); + this._loadElement(createErrorCardConfig(e.message, null)); } } if (changedProps.has("isPanel")) { this._element.isPanel = this.isPanel; } } - } - protected willUpdate( - changedProps: PropertyValueMap | Map - ): void { if (changedProps.has("hass") || changedProps.has("preview")) { this._updateVisibility(); } diff --git a/src/panels/lovelace/cards/hui-conditional-card.ts b/src/panels/lovelace/cards/hui-conditional-card.ts index 9a57b3e5a343..630a7494c9b0 100644 --- a/src/panels/lovelace/cards/hui-conditional-card.ts +++ b/src/panels/lovelace/cards/hui-conditional-card.ts @@ -41,6 +41,7 @@ class HuiConditionalCard extends HuiConditionalBase implements LovelaceCard { element.hass = this.hass; element.preview = this.preview; element.config = cardConfig; + element.load(); return element; } diff --git a/src/panels/lovelace/cards/hui-entity-filter-card.ts b/src/panels/lovelace/cards/hui-entity-filter-card.ts index ca8372b4a7c0..d79e88ad0daf 100644 --- a/src/panels/lovelace/cards/hui-entity-filter-card.ts +++ b/src/panels/lovelace/cards/hui-entity-filter-card.ts @@ -249,6 +249,7 @@ export class HuiEntityFilterCard element.hass = this.hass; element.preview = this.preview; element.config = cardConfig; + element.load(); return element; } } diff --git a/src/panels/lovelace/cards/hui-grid-card.ts b/src/panels/lovelace/cards/hui-grid-card.ts index 97b336c529cb..0838db77d9ba 100644 --- a/src/panels/lovelace/cards/hui-grid-card.ts +++ b/src/panels/lovelace/cards/hui-grid-card.ts @@ -92,6 +92,7 @@ class HuiGridCard extends HuiStackCard { } :host([square]) #root > *:not([hidden]) { + display: block; grid-row: 1 / 1; grid-column: 1 / 1; } diff --git a/src/panels/lovelace/cards/hui-horizontal-stack-card.ts b/src/panels/lovelace/cards/hui-horizontal-stack-card.ts index 502e54db4dd7..4e867e5b0e2f 100644 --- a/src/panels/lovelace/cards/hui-horizontal-stack-card.ts +++ b/src/panels/lovelace/cards/hui-horizontal-stack-card.ts @@ -30,7 +30,7 @@ export class HuiHorizontalStackCard extends HuiStackCard { height: 100%; gap: var(--horizontal-stack-card-gap, var(--stack-card-gap, 8px)); } - #root > * { + #root > hui-card > * { flex: 1 1 0; min-width: 0; } diff --git a/src/panels/lovelace/cards/hui-stack-card.ts b/src/panels/lovelace/cards/hui-stack-card.ts index a308e368eb59..93bc5c4931f9 100644 --- a/src/panels/lovelace/cards/hui-stack-card.ts +++ b/src/panels/lovelace/cards/hui-stack-card.ts @@ -56,7 +56,7 @@ export abstract class HuiStackCard card.hass = this.hass; }); } - if (changedProperties.has("editMode")) { + if (changedProperties.has("preview")) { this._cards.forEach((card) => { card.preview = this.preview; }); @@ -69,6 +69,7 @@ export abstract class HuiStackCard element.hass = this.hass; element.preview = this.preview; element.config = cardConfig; + element.load(); return element; } diff --git a/src/panels/lovelace/cards/hui-tile-card.ts b/src/panels/lovelace/cards/hui-tile-card.ts index 634b52ca1478..fa8c2cc6d067 100644 --- a/src/panels/lovelace/cards/hui-tile-card.ts +++ b/src/panels/lovelace/cards/hui-tile-card.ts @@ -238,6 +238,14 @@ export class HuiTileCard extends LitElement implements LovelaceCard { > `; } + if (content === "last-updated") { + return html` + + `; + } if (content === "last_triggered") { return html` !HIDDEN_ATTRIBUTES.includes(a)) .map((attribute) => ({ diff --git a/src/panels/lovelace/sections/hui-section.ts b/src/panels/lovelace/sections/hui-section.ts index b662072a13e4..07dc7a78f8af 100644 --- a/src/panels/lovelace/sections/hui-section.ts +++ b/src/panels/lovelace/sections/hui-section.ts @@ -64,6 +64,7 @@ export class HuiSection extends ReactiveElement { ev.stopPropagation(); this._cards = [...this._cards]; }); + element.load(); return element; } diff --git a/src/panels/lovelace/views/hui-view.ts b/src/panels/lovelace/views/hui-view.ts index 06df93b7465b..ea0bb5a89512 100644 --- a/src/panels/lovelace/views/hui-view.ts +++ b/src/panels/lovelace/views/hui-view.ts @@ -82,6 +82,7 @@ export class HUIView extends ReactiveElement { ev.stopPropagation(); this._cards = [...this._cards]; }); + element.load(); return element; } diff --git a/src/translations/en.json b/src/translations/en.json index 822152b2214e..ffd25ca6bba2 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5987,7 +5987,8 @@ "state_content": "State content", "state_content_options": { "state": "State", - "last-changed": "Last changed" + "last-changed": "Last changed", + "last-updated": "Last updated" } }, "vertical-stack": { diff --git a/yarn.lock b/yarn.lock index 97db273ab73c..88dd85f65445 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4036,10 +4036,10 @@ __metadata: languageName: node linkType: hard -"@types/chromecast-caf-receiver@npm:6.0.15": - version: 6.0.15 - resolution: "@types/chromecast-caf-receiver@npm:6.0.15" - checksum: 10/532c926d01b8173013c0aa96fad3b4e3e8b8f02c993b52cbc654b4263c7e396cc7a1497e00561428cccdbe6bb2014824577e7fdf44f1ebd63e68a3815500fd86 +"@types/chromecast-caf-receiver@npm:6.0.16": + version: 6.0.16 + resolution: "@types/chromecast-caf-receiver@npm:6.0.16" + checksum: 10/8d60a8fb0a7c4c90d8d8bb7fd55007ef0b2367f0a2129b83895bd857dfdc21296934cb57829248806093d92f66a2fc05475c616b7656ebc27c9498011e2f1f01 languageName: node linkType: hard @@ -8986,7 +8986,7 @@ __metadata: "@rollup/plugin-replace": "npm:5.0.7" "@thomasloven/round-slider": "npm:0.6.0" "@types/babel__plugin-transform-runtime": "npm:7.9.5" - "@types/chromecast-caf-receiver": "npm:6.0.15" + "@types/chromecast-caf-receiver": "npm:6.0.16" "@types/chromecast-caf-sender": "npm:1.0.10" "@types/color-name": "npm:1.1.4" "@types/glob": "npm:8.1.0"