From f87296d978aa08e97882e05381fc81b54a482dfd Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 15 Jul 2024 14:08:00 +0200 Subject: [PATCH] Add state content component (#21370) * Move state content into its own component for reusability * Add entity state content selector * Use live timer * Rename live timer to remaining time and remove remaining attribute from state content list * Move default in state content component * Fix picker --- .../entity/ha-entity-state-content-picker.ts | 314 ++++++++++++++++++ .../ha-selector-ui-state-content.ts | 48 +++ src/components/ha-selector/ha-selector.ts | 1 + src/data/selector.ts | 14 +- src/panels/lovelace/cards/hui-tile-card.ts | 156 +-------- .../config-elements/hui-tile-card-editor.ts | 112 +------ .../entity-rows/hui-timer-entity-row.ts | 6 +- ...ay-timer.ts => ha-timer-remaining-time.ts} | 6 +- src/state-display/state-display.ts | 174 ++++++++++ src/state-summary/state-card-timer.ts | 6 +- src/translations/en.json | 14 +- 11 files changed, 585 insertions(+), 266 deletions(-) create mode 100644 src/components/entity/ha-entity-state-content-picker.ts create mode 100644 src/components/ha-selector/ha-selector-ui-state-content.ts rename src/state-display/{state-display-timer.ts => ha-timer-remaining-time.ts} (92%) create mode 100644 src/state-display/state-display.ts diff --git a/src/components/entity/ha-entity-state-content-picker.ts b/src/components/entity/ha-entity-state-content-picker.ts new file mode 100644 index 000000000000..70272ad36118 --- /dev/null +++ b/src/components/entity/ha-entity-state-content-picker.ts @@ -0,0 +1,314 @@ +import { mdiDrag } from "@mdi/js"; +import { HassEntity } from "home-assistant-js-websocket"; +import { LitElement, PropertyValues, css, html, nothing } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import { repeat } from "lit/directives/repeat"; +import memoizeOne from "memoize-one"; +import { ensureArray } from "../../common/array/ensure-array"; +import { fireEvent } from "../../common/dom/fire_event"; +import { computeDomain } from "../../common/entity/compute_domain"; +import { + STATE_DISPLAY_SPECIAL_CONTENT, + STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS, +} from "../../state-display/state-display"; +import { HomeAssistant, ValueChangedEvent } from "../../types"; +import "../ha-combo-box"; +import type { HaComboBox } from "../ha-combo-box"; + +const HIDDEN_ATTRIBUTES = [ + "access_token", + "available_modes", + "battery_icon", + "battery_level", + "code_arm_required", + "code_format", + "color_modes", + "device_class", + "editable", + "effect_list", + "entity_id", + "entity_picture", + "event_types", + "fan_modes", + "fan_speed_list", + "friendly_name", + "frontend_stream_type", + "has_date", + "has_time", + "hvac_modes", + "icon", + "id", + "max_color_temp_kelvin", + "max_mireds", + "max_temp", + "max", + "min_color_temp_kelvin", + "min_mireds", + "min_temp", + "min", + "mode", + "operation_list", + "options", + "percentage_step", + "precipitation_unit", + "preset_modes", + "pressure_unit", + "remaining", + "sound_mode_list", + "source_list", + "state_class", + "step", + "supported_color_modes", + "supported_features", + "swing_modes", + "target_temp_step", + "temperature_unit", + "token", + "unit_of_measurement", + "visibility_unit", + "wind_speed_unit", +]; + +@customElement("ha-entity-state-content-picker") +class HaEntityStatePicker extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public entityId?: string; + + @property({ type: Boolean }) public autofocus = false; + + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) public required = false; + + @property() public label?: string; + + @property() public value?: string[] | string; + + @property() public helper?: string; + + @state() private _opened = false; + + @query("ha-combo-box", true) private _comboBox!: HaComboBox; + + protected shouldUpdate(changedProps: PropertyValues) { + return !(!changedProps.has("_opened") && this._opened); + } + + private options = memoizeOne((entityId?: string, stateObj?: HassEntity) => { + const domain = entityId ? computeDomain(entityId) : undefined; + return [ + { + label: this.hass.localize("ui.components.state-content-picker.state"), + value: "state", + }, + { + label: this.hass.localize( + "ui.components.state-content-picker.last_changed" + ), + value: "last_changed", + }, + { + label: this.hass.localize( + "ui.components.state-content-picker.last_updated" + ), + value: "last_updated", + }, + ...(domain + ? STATE_DISPLAY_SPECIAL_CONTENT.filter((content) => + STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain]?.includes(content) + ).map((content) => ({ + label: this.hass.localize( + `ui.components.state-content-picker.${content}` + ), + value: content, + })) + : []), + ...Object.keys(stateObj?.attributes ?? {}) + .filter((a) => !HIDDEN_ATTRIBUTES.includes(a)) + .map((attribute) => ({ + value: attribute, + label: this.hass.formatEntityAttributeName(stateObj!, attribute), + })), + ]; + }); + + private _filter = ""; + + protected render() { + if (!this.hass) { + return nothing; + } + + const value = this._value; + + const stateObj = this.entityId + ? this.hass.states[this.entityId] + : undefined; + + const options = this.options(this.entityId, stateObj); + const optionItems = options.filter( + (option) => !this._value.includes(option.value) + ); + + return html` + ${value?.length + ? html` + + + ${repeat( + this._value, + (item) => item, + (item, idx) => { + const label = + options.find((option) => option.value === item)?.label || + item; + return html` + + + + ${label} + + `; + } + )} + + + ` + : nothing} + + + `; + } + + private get _value() { + return !this.value ? [] : ensureArray(this.value); + } + + private _openedChanged(ev: ValueChangedEvent) { + this._opened = ev.detail.value; + } + + private _filterChanged(ev?: CustomEvent): void { + this._filter = ev?.detail.value || ""; + + const filteredItems = this._comboBox.items?.filter((item) => { + const label = item.label || item.value; + return label.toLowerCase().includes(this._filter?.toLowerCase()); + }); + + if (this._filter) { + filteredItems?.unshift({ label: this._filter, value: this._filter }); + } + + this._comboBox.filteredItems = filteredItems; + } + + private async _moveItem(ev: CustomEvent) { + ev.stopPropagation(); + const { oldIndex, newIndex } = ev.detail; + const value = this._value; + const newValue = value.concat(); + const element = newValue.splice(oldIndex, 1)[0]; + newValue.splice(newIndex, 0, element); + this._setValue(newValue); + await this.updateComplete; + this._filterChanged(); + } + + private async _removeItem(ev) { + ev.stopPropagation(); + const value: string[] = [...this._value]; + value.splice(ev.target.idx, 1); + this._setValue(value); + await this.updateComplete; + this._filterChanged(); + } + + private _comboBoxValueChanged(ev: CustomEvent): void { + ev.stopPropagation(); + const newValue = ev.detail.value; + + if (this.disabled || newValue === "") { + return; + } + + const currentValue = this._value; + + if (currentValue.includes(newValue)) { + return; + } + + setTimeout(() => { + this._filterChanged(); + this._comboBox.setInputValue(""); + }, 0); + + this._setValue([...currentValue, newValue]); + } + + private _setValue(value: string[]) { + const newValue = + value.length === 0 ? undefined : value.length === 1 ? value[0] : value; + this.value = newValue; + fireEvent(this, "value-changed", { + value: newValue, + }); + } + + static styles = css` + :host { + position: relative; + } + + ha-chip-set { + padding: 8px 0; + } + + .sortable-fallback { + display: none; + opacity: 0; + } + + .sortable-ghost { + opacity: 0.4; + } + + .sortable-drag { + cursor: grabbing; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-entity-state-content-picker": HaEntityStatePicker; + } +} diff --git a/src/components/ha-selector/ha-selector-ui-state-content.ts b/src/components/ha-selector/ha-selector-ui-state-content.ts new file mode 100644 index 000000000000..c0d521b93928 --- /dev/null +++ b/src/components/ha-selector/ha-selector-ui-state-content.ts @@ -0,0 +1,48 @@ +import { html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { UiStateContentSelector } from "../../data/selector"; +import { SubscribeMixin } from "../../mixins/subscribe-mixin"; +import { HomeAssistant } from "../../types"; +import "../entity/ha-entity-state-content-picker"; + +@customElement("ha-selector-ui_state_content") +export class HaSelectorUiStateContent extends SubscribeMixin(LitElement) { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public selector!: UiStateContentSelector; + + @property() public value?: string | string[]; + + @property() public label?: string; + + @property() public helper?: string; + + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) public required = true; + + @property({ attribute: false }) public context?: { + filter_entity?: string; + }; + + protected render() { + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-ui_state_content": HaSelectorUiStateContent; + } +} diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 8cab4393b8c9..11a9136abccf 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -57,6 +57,7 @@ const LOAD_ELEMENTS = { color_temp: () => import("./ha-selector-color-temp"), ui_action: () => import("./ha-selector-ui-action"), ui_color: () => import("./ha-selector-ui-color"), + ui_state_content: () => import("./ha-selector-ui-state-content"), }; const LEGACY_UI_SELECTORS = new Set(["ui-action", "ui-color"]); diff --git a/src/data/selector.ts b/src/data/selector.ts index 60b9e4973b1c..7e4ccc8f38da 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -2,6 +2,8 @@ import type { HassEntity } from "home-assistant-js-websocket"; import { ensureArray } from "../common/array/ensure-array"; import { computeStateDomain } from "../common/entity/compute_state_domain"; import { supportsFeature } from "../common/entity/supports-feature"; +import type { CropOptions } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog"; +import { isHelperDomain } from "../panels/config/helpers/const"; import { UiAction } from "../panels/lovelace/components/hui-action-editor"; import { HomeAssistant, ItemPath } from "../types"; import { @@ -13,8 +15,6 @@ import { EntityRegistryEntry, } from "./entity_registry"; import { EntitySources } from "./entity_sources"; -import { isHelperDomain } from "../panels/config/helpers/const"; -import type { CropOptions } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog"; export type Selector = | ActionSelector @@ -64,7 +64,8 @@ export type Selector = | TTSSelector | TTSVoiceSelector | UiActionSelector - | UiColorSelector; + | UiColorSelector + | UiStateContentSelector; export interface ActionSelector { action: { @@ -455,6 +456,13 @@ export interface UiColorSelector { ui_color: { default_color?: boolean } | null; } +export interface UiStateContentSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + ui_state_content: { + entity_id?: string; + } | null; +} + export const expandLabelTarget = ( hass: HomeAssistant, labelId: string, diff --git a/src/panels/lovelace/cards/hui-tile-card.ts b/src/panels/lovelace/cards/hui-tile-card.ts index 3208928d0273..419b5bdfc63d 100644 --- a/src/panels/lovelace/cards/hui-tile-card.ts +++ b/src/panels/lovelace/cards/hui-tile-card.ts @@ -1,19 +1,11 @@ import { mdiExclamationThick, mdiHelp } from "@mdi/js"; import { HassEntity } from "home-assistant-js-websocket"; -import { - CSSResultGroup, - LitElement, - TemplateResult, - css, - html, - nothing, -} from "lit"; +import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; -import { ensureArray } from "../../../common/array/ensure-array"; import { computeCssColor } from "../../../common/color/compute-color"; import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color"; import { DOMAINS_TOGGLE } from "../../../common/const"; @@ -30,17 +22,14 @@ import "../../../components/tile/ha-tile-image"; import type { TileImageStyle } from "../../../components/tile/ha-tile-image"; import "../../../components/tile/ha-tile-info"; import { cameraUrlWithWidthHeight } from "../../../data/camera"; -import { isUnavailableState } from "../../../data/entity"; import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; -import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor"; -import { UpdateEntity, computeUpdateStateDisplay } from "../../../data/update"; +import "../../../state-display/state-display"; import { HomeAssistant } from "../../../types"; import "../card-features/hui-card-features"; import { actionHandler } from "../common/directives/action-handler-directive"; import { findEntities } from "../common/find-entities"; import { handleAction } from "../common/handle-action"; import { hasAction } from "../common/has-action"; -import "../components/hui-timestamp-display"; import type { LovelaceCard, LovelaceCardEditor, @@ -49,8 +38,6 @@ import type { import { renderTileBadge } from "./tile/badges/tile-badge"; import type { ThermostatCardConfig, TileCardConfig } from "./types"; -const TIMESTAMP_STATE_DOMAINS = ["button", "input_button", "scene"]; - export const getEntityDefaultTileIconAction = (entityId: string) => { const domain = computeDomain(entityId); const supportsIconAction = @@ -208,127 +195,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard { } ); - private _renderStateContent( - stateObj: HassEntity, - stateContent: string | string[] - ) { - const contents = ensureArray(stateContent); - - const values = contents - .map((content) => { - if (content === "state") { - const domain = computeDomain(stateObj.entity_id); - if ( - (stateObj.attributes.device_class === - SENSOR_DEVICE_CLASS_TIMESTAMP || - TIMESTAMP_STATE_DOMAINS.includes(domain)) && - !isUnavailableState(stateObj.state) - ) { - return html` - - `; - } - - return this.hass!.formatEntityState(stateObj); - } - if (content === "last-changed") { - return html` - - `; - } - if (content === "last-updated") { - return html` - - `; - } - if (content === "last_triggered") { - return html` - - `; - } - if (stateObj.attributes[content] == null) { - return undefined; - } - return this.hass!.formatEntityAttributeValue(stateObj, content); - }) - .filter(Boolean); - - if (!values.length) { - return html`${this.hass!.formatEntityState(stateObj)}`; - } - - return html` - ${values.map( - (value, index, array) => - html`${value}${index < array.length - 1 ? " ⸱ " : nothing}` - )} - `; - } - - private _renderState(stateObj: HassEntity): TemplateResult | typeof nothing { - const domain = computeDomain(stateObj.entity_id); - const active = stateActive(stateObj); - - if (domain === "light" && active) { - return this._renderStateContent(stateObj, ["brightness"]); - } - - if (domain === "fan" && active) { - return this._renderStateContent(stateObj, ["percentage"]); - } - - if (domain === "cover" && active) { - return this._renderStateContent(stateObj, ["state", "current_position"]); - } - - if (domain === "valve" && active) { - return this._renderStateContent(stateObj, ["state", "current_position"]); - } - - if (domain === "humidifier") { - return this._renderStateContent(stateObj, ["state", "current_humidity"]); - } - - if (domain === "climate") { - return this._renderStateContent(stateObj, [ - "state", - "current_temperature", - ]); - } - - if (domain === "update") { - return html` - ${computeUpdateStateDisplay(stateObj as UpdateEntity, this.hass!)} - `; - } - - if (domain === "timer") { - import("../../../state-display/state-display-timer"); - return html` - - `; - } - - return this._renderStateContent(stateObj, "state"); - } - get hasCardAction() { return ( !this._config?.tap_action || @@ -375,17 +241,21 @@ export class HuiTileCard extends LitElement implements LovelaceCard { } const name = this._config.name || stateObj.attributes.friendly_name; - - const localizedState = this._config.hide_state - ? nothing - : this._config.state_content - ? this._renderStateContent(stateObj, this._config.state_content) - : this._renderState(stateObj); - const active = stateActive(stateObj); const color = this._computeStateColor(stateObj, this._config.color); const domain = computeDomain(stateObj.entity_id); + const localizedState = this._config.hide_state + ? nothing + : html` + + + `; + const style = { "--tile-color": color, }; diff --git a/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts index ca705fafb726..172d2c95ed3f 100644 --- a/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts @@ -1,5 +1,4 @@ import { mdiGestureTap, mdiPalette } from "@mdi/js"; -import { HassEntity } from "home-assistant-js-websocket"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; @@ -14,9 +13,7 @@ import { string, union, } from "superstruct"; -import { ensureArray } from "../../../../common/array/ensure-array"; import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event"; -import { formatEntityAttributeNameFunc } from "../../../../common/translations/entity-state"; import { LocalizeFunc } from "../../../../common/translations/localize"; import "../../../../components/ha-form/ha-form"; import type { @@ -38,59 +35,6 @@ import { EditSubElementEvent, SubElementEditorConfig } from "../types"; import { configElementStyle } from "./config-elements-style"; import "./hui-card-features-editor"; -const HIDDEN_ATTRIBUTES = [ - "access_token", - "available_modes", - "code_arm_required", - "code_format", - "color_modes", - "device_class", - "editable", - "effect_list", - "entity_id", - "entity_picture", - "event_types", - "fan_modes", - "fan_speed_list", - "friendly_name", - "frontend_stream_type", - "has_date", - "has_time", - "hvac_modes", - "icon", - "id", - "max_color_temp_kelvin", - "max_mireds", - "max_temp", - "max", - "min_color_temp_kelvin", - "min_mireds", - "min_temp", - "min", - "mode", - "operation_list", - "options", - "percentage_step", - "precipitation_unit", - "preset_modes", - "pressure_unit", - "sound_mode_list", - "source_list", - "state_class", - "step", - "supported_color_modes", - "supported_features", - "swing_modes", - "target_temp_step", - "temperature_unit", - "token", - "unit_of_measurement", - "visibility_unit", - "wind_speed_unit", - "battery_icon", - "battery_level", -]; - const cardConfigStruct = assign( baseLovelaceCardConfig, object({ @@ -127,9 +71,7 @@ export class HuiTileCardEditor private _schema = memoizeOne( ( localize: LocalizeFunc, - formatEntityAttributeName: formatEntityAttributeNameFunc, entityId: string | undefined, - stateObj: HassEntity | undefined, hideState: boolean ) => [ @@ -183,41 +125,10 @@ export class HuiTileCardEditor { name: "state_content", selector: { - select: { - mode: "dropdown", - reorder: true, - custom_value: true, - multiple: true, - options: [ - { - label: localize( - `ui.panel.lovelace.editor.card.tile.state_content_options.state` - ), - value: "state", - }, - { - label: localize( - `ui.panel.lovelace.editor.card.tile.state_content_options.last-changed` - ), - value: "last-changed", - }, - { - label: localize( - `ui.panel.lovelace.editor.card.tile.state_content_options.last-updated` - ), - value: "last-updated", - }, - ...Object.keys(stateObj?.attributes ?? {}) - .filter((a) => !HIDDEN_ATTRIBUTES.includes(a)) - .map((attribute) => ({ - value: attribute, - label: formatEntityAttributeName( - stateObj!, - attribute - ), - })), - ], - }, + ui_state_content: {}, + }, + context: { + filter_entity: "entity", }, }, ] as const satisfies readonly HaFormSchema[]) @@ -268,9 +179,7 @@ export class HuiTileCardEditor const schema = this._schema( this.hass!.localize, - this.hass.formatEntityAttributeName, this._config.entity, - stateObj, this._config.hide_state ?? false ); @@ -287,10 +196,7 @@ export class HuiTileCardEditor `; } - const data = { - ...this._config, - state_content: ensureArray(this._config.state_content), - }; + const data = this._config; return html`
- + >
`; diff --git a/src/state-display/state-display-timer.ts b/src/state-display/ha-timer-remaining-time.ts similarity index 92% rename from src/state-display/state-display-timer.ts rename to src/state-display/ha-timer-remaining-time.ts index 13960c088cc4..36d242f92a5e 100644 --- a/src/state-display/state-display-timer.ts +++ b/src/state-display/ha-timer-remaining-time.ts @@ -4,8 +4,8 @@ import { customElement, property, state } from "lit/decorators"; import { computeDisplayTimer, timerTimeRemaining } from "../data/timer"; import type { HomeAssistant } from "../types"; -@customElement("state-display-timer") -class StateDisplayTimer extends ReactiveElement { +@customElement("ha-timer-remaining-time") +class HaTimerRemainingTime extends ReactiveElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public stateObj!: HassEntity; @@ -69,6 +69,6 @@ class StateDisplayTimer extends ReactiveElement { declare global { interface HTMLElementTagNameMap { - "state-display-timer": StateDisplayTimer; + "ha-timer-remaining-time": HaTimerRemainingTime; } } diff --git a/src/state-display/state-display.ts b/src/state-display/state-display.ts new file mode 100644 index 000000000000..a1042681a8a2 --- /dev/null +++ b/src/state-display/state-display.ts @@ -0,0 +1,174 @@ +import type { HassEntity } from "home-assistant-js-websocket"; +import { html, LitElement, nothing, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import { ensureArray } from "../common/array/ensure-array"; +import { computeStateDomain } from "../common/entity/compute_state_domain"; +import "../components/ha-relative-time"; +import { isUnavailableState } from "../data/entity"; +import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../data/sensor"; +import { computeUpdateStateDisplay, UpdateEntity } from "../data/update"; +import "../panels/lovelace/components/hui-timestamp-display"; +import type { HomeAssistant } from "../types"; + +const TIMESTAMP_STATE_DOMAINS = ["button", "input_button", "scene"]; + +export const STATE_DISPLAY_SPECIAL_CONTENT = [ + "remaining_time", + "install_status", +] as const; + +// Special handling of state attributes per domain +export const STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS: Record< + string, + (typeof STATE_DISPLAY_SPECIAL_CONTENT)[number][] +> = { + timer: ["remaining_time"], + update: ["install_status"], +}; + +// Attributes that should not be shown if their value is 0 */ +export const HIDDEN_ZERO_ATTRIBUTES_DOMAINS: Record = { + valve: ["current_position"], + cover: ["current_position"], + fan: ["percentage"], + light: ["brightness"], +}; + +type StateContent = string | string[]; + +export const DEFAULT_STATE_CONTENT_DOMAINS: Record = { + climate: ["state", "current_temperature"], + cover: ["state", "current_position"], + fan: "percentage", + humidifier: ["state", "current_humidity"], + light: "brightness", + timer: "remaining_time", + update: "install_status", + valve: ["state", "current_position"], +}; + +@customElement("state-display") +class StateDisplay extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj!: HassEntity; + + @property({ attribute: false }) public content?: StateContent; + + protected createRenderRoot() { + return this; + } + + private get _content(): StateContent { + const domain = computeStateDomain(this.stateObj); + return this.content ?? DEFAULT_STATE_CONTENT_DOMAINS[domain] ?? "state"; + } + + private _computeContent( + content: string + ): TemplateResult<1> | string | undefined { + const stateObj = this.stateObj; + const domain = computeStateDomain(stateObj); + + if (content === "state") { + if ( + (stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP || + TIMESTAMP_STATE_DOMAINS.includes(domain)) && + !isUnavailableState(stateObj.state) + ) { + return html` + + `; + } + + return this.hass!.formatEntityState(stateObj); + } + // Check last-changed for backwards compatibility + if (content === "last_changed" || content === "last-changed") { + return html` + + `; + } + // Check last_updated for backwards compatibility + if (content === "last_updated" || content === "last-updated") { + return html` + + `; + } + if (content === "last_triggered") { + return html` + + `; + } + + const specialContent = (STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain] ?? + []) as string[]; + + if (specialContent.includes(content)) { + if (content === "install_status") { + return html` + ${computeUpdateStateDisplay(stateObj as UpdateEntity, this.hass!)} + `; + } + if (content === "remaining_time") { + import("./ha-timer-remaining-time"); + return html` + + `; + } + } + + const attribute = stateObj.attributes[content]; + + if ( + attribute == null || + (HIDDEN_ZERO_ATTRIBUTES_DOMAINS[domain]?.includes(content) && !attribute) + ) { + return undefined; + } + return this.hass!.formatEntityAttributeValue(stateObj, content); + } + + protected render() { + const stateObj = this.stateObj; + const contents = ensureArray(this._content); + + const values = contents + .map((content) => this._computeContent(content)) + .filter(Boolean); + + if (!values.length) { + return html`${this.hass!.formatEntityState(stateObj)}`; + } + + return html` + ${values.map( + (value, index, array) => + html`${value}${index < array.length - 1 ? " ⸱ " : nothing}` + )} + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "state-display": StateDisplay; + } +} diff --git a/src/state-summary/state-card-timer.ts b/src/state-summary/state-card-timer.ts index 134ebdb89c61..661ce17bdc6b 100644 --- a/src/state-summary/state-card-timer.ts +++ b/src/state-summary/state-card-timer.ts @@ -3,7 +3,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import "../components/entity/state-info"; import { haStyle } from "../resources/styles"; -import "../state-display/state-display-timer"; +import "../state-display/ha-timer-remaining-time"; import { HomeAssistant } from "../types"; @customElement("state-card-timer") @@ -23,10 +23,10 @@ class StateCardTimer extends LitElement { .inDialog=${this.inDialog} >
- + >
`; diff --git a/src/translations/en.json b/src/translations/en.json index 5b9c9762909c..1a2f6d871fa5 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1018,6 +1018,13 @@ }, "yaml-editor": { "copy_to_clipboard": "[%key:ui::panel::config::automation::editor::copy_to_clipboard%]" + }, + "state-content-picker": { + "state": "State", + "last_changed": "Last changed", + "last_updated": "Last updated", + "remaining_time": "Remaining time", + "install_status": "Install status" } }, "dialogs": { @@ -5981,12 +5988,7 @@ "show_entity_picture": "Show entity picture", "vertical": "Vertical", "hide_state": "Hide state", - "state_content": "State content", - "state_content_options": { - "state": "State", - "last-changed": "Last changed", - "last-updated": "Last updated" - } + "state_content": "State content" }, "vertical-stack": { "name": "Vertical stack",