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..040ee0fd741f
--- /dev/null
+++ b/src/components/entity/ha-entity-state-content-picker.ts
@@ -0,0 +1,313 @@
+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",
+ "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",
+];
+
+@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[content]?.includes(domain)
+ ).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/editor/config-elements/hui-tile-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts
index 5709072eba29..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 {
@@ -37,61 +34,6 @@ import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditSubElementEvent, SubElementEditorConfig } from "../types";
import { configElementStyle } from "./config-elements-style";
import "./hui-card-features-editor";
-import { STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS } from "../../../../state-display/state-display";
-import { computeDomain } from "../../../../common/entity/compute_domain";
-
-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,
@@ -129,13 +71,10 @@ export class HuiTileCardEditor
private _schema = memoizeOne(
(
localize: LocalizeFunc,
- formatEntityAttributeName: formatEntityAttributeNameFunc,
entityId: string | undefined,
- stateObj: HassEntity | undefined,
hideState: boolean
- ) => {
- const domain = entityId ? computeDomain(entityId) : undefined;
- return [
+ ) =>
+ [
{ name: "entity", selector: { entity: {} } },
{
name: "",
@@ -186,56 +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",
- },
- ...(domain
- ? Object.keys(STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS)
- .filter((content) =>
- STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[
- content
- ]?.includes(domain)
- )
- .map((content) => ({
- label:
- localize(
- `ui.panel.lovelace.editor.card.tile.state_content_options.${content}`
- ) || content,
- value: content,
- }))
- : []),
- ...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,8 +161,7 @@ export class HuiTileCardEditor
},
],
},
- ] as const satisfies readonly HaFormSchema[];
- }
+ ] as const satisfies readonly HaFormSchema[]
);
private _context = memoizeOne(
@@ -287,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
);
@@ -306,10 +196,7 @@ export class HuiTileCardEditor
`;
}
- const data = {
- ...this._config,
- state_content: ensureArray(this._config.state_content),
- };
+ const data = this._config;
return html`
= {
+export const STATE_DISPLAY_SPECIAL_CONTENT = [
+ "timer_status",
+ "install_status",
+] as const;
+
+export const STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS: Record<
+ (typeof STATE_DISPLAY_SPECIAL_CONTENT)[number],
+ string[]
+> = {
timer_status: ["timer"],
install_status: ["update"],
};
diff --git a/src/translations/en.json b/src/translations/en.json
index fd3e3b2f41a1..55d6d6cfa910 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",
+ "timer_status": "Timer status",
+ "install_status": "Install status"
}
},
"dialogs": {
@@ -5981,14 +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",
- "timer_status": "Timer status",
- "install_status": "Install status"
- }
+ "state_content": "State content"
},
"vertical-stack": {
"name": "Vertical stack",