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
This commit is contained in:
Paul Bottein 2024-07-15 14:08:00 +02:00 committed by GitHub
parent 2890d5c8cf
commit f87296d978
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 585 additions and 266 deletions

View File

@ -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`
<ha-sortable
no-style
@item-moved=${this._moveItem}
.disabled=${this.disabled}
>
<ha-chip-set>
${repeat(
this._value,
(item) => item,
(item, idx) => {
const label =
options.find((option) => option.value === item)?.label ||
item;
return html`
<ha-input-chip
.idx=${idx}
@remove=${this._removeItem}
.label=${label}
selected
>
<ha-svg-icon
slot="icon"
.path=${mdiDrag}
data-handle
></ha-svg-icon>
${label}
</ha-input-chip>
`;
}
)}
</ha-chip-set>
</ha-sortable>
`
: nothing}
<ha-combo-box
item-value-path="value"
item-label-path="label"
.hass=${this.hass}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required && !value.length}
.value=${""}
.items=${optionItems}
allow-custom-value
@filter-changed=${this._filterChanged}
@value-changed=${this._comboBoxValueChanged}
@opened-changed=${this._openedChanged}
></ha-combo-box>
`;
}
private get _value() {
return !this.value ? [] : ensureArray(this.value);
}
private _openedChanged(ev: ValueChangedEvent<boolean>) {
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;
}
}

View File

@ -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`
<ha-entity-state-content-picker
.hass=${this.hass}
.entityId=${this.selector.ui_state_content?.entity_id ||
this.context?.filter_entity}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
></ha-entity-state-content-picker>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-ui_state_content": HaSelectorUiStateContent;
}
}

View File

@ -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"]);

View File

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

View File

@ -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`
<hui-timestamp-display
.hass=${this.hass}
.ts=${new Date(stateObj.state)}
format="relative"
capitalize
></hui-timestamp-display>
`;
}
return this.hass!.formatEntityState(stateObj);
}
if (content === "last-changed") {
return html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.last_changed}
></ha-relative-time>
`;
}
if (content === "last-updated") {
return html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.last_updated}
></ha-relative-time>
`;
}
if (content === "last_triggered") {
return html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.attributes.last_triggered}
></ha-relative-time>
`;
}
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`
<state-display-timer
.hass=${this.hass}
.stateObj=${stateObj}
></state-display-timer>
`;
}
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`
<state-display
.stateObj=${stateObj}
.hass=${this.hass}
.content=${this._config.state_content}
>
</state-display>
`;
const style = {
"--tile-color": color,
};

View File

@ -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`
<ha-form
@ -327,12 +233,8 @@ export class HuiTileCardEditor
delete config.state_content;
}
if (config.state_content) {
if (config.state_content.length === 0) {
delete config.state_content;
} else if (config.state_content.length === 1) {
config.state_content = config.state_content[0];
}
if (!config.state_content) {
delete config.state_content;
}
fireEvent(this, "config-changed", { config });

View File

@ -1,6 +1,6 @@
import { LitElement, PropertyValues, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../state-display/state-display-timer";
import "../../../state-display/ha-timer-remaining-time";
import { HomeAssistant } from "../../../types";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../components/hui-generic-entity-row";
@ -38,10 +38,10 @@ class HuiTimerEntityRow extends LitElement {
return html`
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
<div class="text-content">
<state-display-timer
<ha-timer-remaining-time
.hass=${this.hass}
.stateObj=${stateObj}
></state-display-timer>
></ha-timer-remaining-time>
</div>
</hui-generic-entity-row>
`;

View File

@ -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;
}
}

View File

@ -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<string, string[]> = {
valve: ["current_position"],
cover: ["current_position"],
fan: ["percentage"],
light: ["brightness"],
};
type StateContent = string | string[];
export const DEFAULT_STATE_CONTENT_DOMAINS: Record<string, StateContent> = {
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`
<hui-timestamp-display
.hass=${this.hass}
.ts=${new Date(stateObj.state)}
format="relative"
capitalize
></hui-timestamp-display>
`;
}
return this.hass!.formatEntityState(stateObj);
}
// Check last-changed for backwards compatibility
if (content === "last_changed" || content === "last-changed") {
return html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.last_changed}
></ha-relative-time>
`;
}
// Check last_updated for backwards compatibility
if (content === "last_updated" || content === "last-updated") {
return html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.last_updated}
></ha-relative-time>
`;
}
if (content === "last_triggered") {
return html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.attributes.last_triggered}
></ha-relative-time>
`;
}
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`
<ha-timer-remaining-time
.hass=${this.hass}
.stateObj=${stateObj}
></ha-timer-remaining-time>
`;
}
}
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;
}
}

View File

@ -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}
></state-info>
<div class="state">
<state-display-timer
<ha-timer-remaining-time
.hass=${this.hass}
.stateObj=${this.stateObj}
></state-display-timer>
></ha-timer-remaining-time>
</div>
</div>
`;

View File

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