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",