mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
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:
parent
2890d5c8cf
commit
f87296d978
314
src/components/entity/ha-entity-state-content-picker.ts
Normal file
314
src/components/entity/ha-entity-state-content-picker.ts
Normal 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;
|
||||
}
|
||||
}
|
48
src/components/ha-selector/ha-selector-ui-state-content.ts
Normal file
48
src/components/ha-selector/ha-selector-ui-state-content.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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"]);
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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 });
|
||||
|
@ -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>
|
||||
`;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
174
src/state-display/state-display.ts
Normal file
174
src/state-display/state-display.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
`;
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user