mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Add format state/attribute to hass (#17249)
This commit is contained in:
parent
023f13cd12
commit
ebee8f670e
@ -1,7 +1,6 @@
|
||||
import { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, TemplateResult } from "lit";
|
||||
import { until } from "lit/directives/until";
|
||||
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import checkValidDate from "../datetime/check_valid_date";
|
||||
import { formatDate } from "../datetime/format_date";
|
||||
@ -12,9 +11,6 @@ import { isDate } from "../string/is_date";
|
||||
import { isTimestamp } from "../string/is_timestamp";
|
||||
import { LocalizeFunc } from "../translations/localize";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
|
||||
let jsYamlPromise: Promise<typeof import("../../resources/js-yaml-dump")>;
|
||||
|
||||
export const computeAttributeValueDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
@ -24,7 +20,7 @@ export const computeAttributeValueDisplay = (
|
||||
entities: HomeAssistant["entities"],
|
||||
attribute: string,
|
||||
value?: any
|
||||
): string | TemplateResult => {
|
||||
): string => {
|
||||
const attributeValue =
|
||||
value !== undefined ? value : stateObj.attributes[attribute];
|
||||
|
||||
@ -40,23 +36,6 @@ export const computeAttributeValueDisplay = (
|
||||
|
||||
// Special handling in case this is a string with an known format
|
||||
if (typeof attributeValue === "string") {
|
||||
// URL handling
|
||||
if (attributeValue.startsWith("http")) {
|
||||
try {
|
||||
// If invalid URL, exception will be raised
|
||||
const url = new URL(attributeValue);
|
||||
if (url.protocol === "http:" || url.protocol === "https:")
|
||||
return html`<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href=${attributeValue}
|
||||
>${attributeValue}</a
|
||||
>`;
|
||||
} catch (_) {
|
||||
// Nothing to do here
|
||||
}
|
||||
}
|
||||
|
||||
// Date handling
|
||||
if (isDate(attributeValue, true)) {
|
||||
// Timestamp handling
|
||||
@ -81,13 +60,8 @@ export const computeAttributeValueDisplay = (
|
||||
attributeValue.some((val) => val instanceof Object)) ||
|
||||
(!Array.isArray(attributeValue) && attributeValue instanceof Object)
|
||||
) {
|
||||
if (!jsYamlPromise) {
|
||||
jsYamlPromise = import("../../resources/js-yaml-dump");
|
||||
}
|
||||
const yaml = jsYamlPromise.then((jsYaml) => jsYaml.dump(attributeValue));
|
||||
return html`<pre>${until(yaml, "")}</pre>`;
|
||||
return JSON.stringify(attributeValue);
|
||||
}
|
||||
|
||||
// If this is an array, try to determine the display value for each item
|
||||
if (Array.isArray(attributeValue)) {
|
||||
return attributeValue
|
||||
|
47
src/common/translations/entity-state.ts
Normal file
47
src/common/translations/entity-state.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { LocalizeFunc } from "./localize";
|
||||
|
||||
export type FormatEntityStateFunc = {
|
||||
formatEntityState: (stateObj: HassEntity, state?: string) => string;
|
||||
formatEntityAttributeValue: (
|
||||
stateObj: HassEntity,
|
||||
attribute: string,
|
||||
value?: any
|
||||
) => string;
|
||||
formatEntityAttributeName: (
|
||||
stateObj: HassEntity,
|
||||
attribute: string
|
||||
) => string;
|
||||
};
|
||||
|
||||
export const computeFormatFunctions = async (
|
||||
localize: LocalizeFunc,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
entities: HomeAssistant["entities"]
|
||||
): Promise<FormatEntityStateFunc> => {
|
||||
const { computeStateDisplay } = await import(
|
||||
"../entity/compute_state_display"
|
||||
);
|
||||
const { computeAttributeValueDisplay, computeAttributeNameDisplay } =
|
||||
await import("../entity/compute_attribute_display");
|
||||
|
||||
return {
|
||||
formatEntityState: (stateObj, state) =>
|
||||
computeStateDisplay(localize, stateObj, locale, config, entities, state),
|
||||
formatEntityAttributeValue: (stateObj, attribute, value) =>
|
||||
computeAttributeValueDisplay(
|
||||
localize,
|
||||
stateObj,
|
||||
locale,
|
||||
config,
|
||||
entities,
|
||||
attribute,
|
||||
value
|
||||
),
|
||||
formatEntityAttributeName: (stateObj, attribute) =>
|
||||
computeAttributeNameDisplay(localize, stateObj, entities, attribute),
|
||||
};
|
||||
};
|
64
src/components/ha-attribute-value.ts
Normal file
64
src/components/ha-attribute-value.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
let jsYamlPromise: Promise<typeof import("../resources/js-yaml-dump")>;
|
||||
|
||||
@customElement("ha-attribute-value")
|
||||
class HaAttributeValue extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public stateObj?: HassEntity;
|
||||
|
||||
@property() public attribute!: string;
|
||||
|
||||
protected render() {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
const attributeValue = this.stateObj.attributes[this.attribute];
|
||||
if (typeof attributeValue === "string") {
|
||||
// URL handling
|
||||
if (attributeValue.startsWith("http")) {
|
||||
try {
|
||||
// If invalid URL, exception will be raised
|
||||
const url = new URL(attributeValue);
|
||||
if (url.protocol === "http:" || url.protocol === "https:")
|
||||
return html`
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href=${attributeValue}
|
||||
>
|
||||
${attributeValue}
|
||||
</a>
|
||||
`;
|
||||
} catch (_) {
|
||||
// Nothing to do here
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(Array.isArray(attributeValue) &&
|
||||
attributeValue.some((val) => val instanceof Object)) ||
|
||||
(!Array.isArray(attributeValue) && attributeValue instanceof Object)
|
||||
) {
|
||||
if (!jsYamlPromise) {
|
||||
jsYamlPromise = import("../resources/js-yaml-dump");
|
||||
}
|
||||
const yaml = jsYamlPromise.then((jsYaml) => jsYaml.dump(attributeValue));
|
||||
return html`<pre>${until(yaml, "")}</pre>`;
|
||||
}
|
||||
|
||||
return this.hass.formatEntityAttributeValue(this.stateObj!, this.attribute);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-attribute-value": HaAttributeValue;
|
||||
}
|
||||
}
|
@ -1,15 +1,12 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import {
|
||||
computeAttributeNameDisplay,
|
||||
computeAttributeValueDisplay,
|
||||
} from "../common/entity/compute_attribute_display";
|
||||
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
|
||||
import { STATE_ATTRIBUTES } from "../data/entity_attributes";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-attribute-value";
|
||||
|
||||
@customElement("ha-attributes")
|
||||
class HaAttributes extends LitElement {
|
||||
@ -58,14 +55,11 @@ class HaAttributes extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<div class="value">
|
||||
${computeAttributeValueDisplay(
|
||||
this.hass.localize,
|
||||
this.stateObj!,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
attribute
|
||||
)}
|
||||
<ha-attribute-value
|
||||
.hass=${this.hass}
|
||||
.attribute=${attribute}
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-attribute-value>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
@ -68,7 +68,8 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
willUpdate(changedProps: PropertyValues<this>) {
|
||||
protected willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
if (
|
||||
this._databaseMigration === undefined &&
|
||||
changedProps.has("hass") &&
|
||||
@ -79,7 +80,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
}
|
||||
}
|
||||
|
||||
update(changedProps: PropertyValues<this>) {
|
||||
protected update(changedProps: PropertyValues<this>) {
|
||||
if (
|
||||
this.hass?.states &&
|
||||
this.hass.config &&
|
||||
|
@ -12,13 +12,12 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display";
|
||||
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import {
|
||||
stateColorCss,
|
||||
stateColorBrightness,
|
||||
stateColorCss,
|
||||
} from "../../../common/entity/state_color";
|
||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||
import {
|
||||
@ -27,6 +26,7 @@ import {
|
||||
isNumericState,
|
||||
} from "../../../common/number/format_number";
|
||||
import { iconColorCSS } from "../../../common/style/icon_color_css";
|
||||
import "../../../components/ha-attribute-value";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../../data/climate";
|
||||
@ -157,14 +157,14 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
||||
<span class="value"
|
||||
>${"attribute" in this._config
|
||||
? stateObj.attributes[this._config.attribute!] !== undefined
|
||||
? computeAttributeValueDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
this._config.attribute!
|
||||
)
|
||||
? html`
|
||||
<ha-attribute-value
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.attribute=${this._config.attribute!}
|
||||
>
|
||||
</ha-attribute-value>
|
||||
`
|
||||
: this.hass.localize("state.default.unknown")
|
||||
: isNumericState(stateObj) || this._config.unit
|
||||
? formatNumber(
|
||||
|
@ -25,7 +25,6 @@ import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
|
||||
import { DOMAINS_TOGGLE } from "../../../common/const";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
||||
import { stateActive } from "../../../common/entity/state_active";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import { stateIconPath } from "../../../common/entity/state_icon_path";
|
||||
@ -234,13 +233,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
}
|
||||
|
||||
const stateDisplay = computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
stateObj,
|
||||
this.hass!.locale,
|
||||
this.hass!.config,
|
||||
this.hass!.entities
|
||||
);
|
||||
const stateDisplay = this.hass!.formatEntityState(stateObj);
|
||||
|
||||
if (domain === "cover") {
|
||||
const positionStateDisplay = computeCoverPositionStateDisplay(
|
||||
|
@ -1,20 +1,20 @@
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import checkValidDate from "../../../common/datetime/check_valid_date";
|
||||
import "../../../components/ha-attribute-value";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-generic-entity-row";
|
||||
import "../components/hui-timestamp-display";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import { AttributeRowConfig, LovelaceRow } from "../entity-rows/types";
|
||||
import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display";
|
||||
|
||||
@customElement("hui-attribute-row")
|
||||
class HuiAttributeRow extends LitElement implements LovelaceRow {
|
||||
@ -71,15 +71,14 @@ class HuiAttributeRow extends LitElement implements LovelaceRow {
|
||||
capitalize
|
||||
></hui-timestamp-display>`
|
||||
: attribute !== undefined
|
||||
? computeAttributeValueDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
this._config.attribute,
|
||||
attribute
|
||||
)
|
||||
? html`
|
||||
<ha-attribute-value
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.attribute=${this._config.attribute}
|
||||
>
|
||||
</ha-attribute-value>
|
||||
`
|
||||
: "—"}
|
||||
${this._config.suffix}
|
||||
</hui-generic-entity-row>
|
||||
|
@ -19,9 +19,9 @@ import { forwardHaptic } from "../data/haptics";
|
||||
import { DEFAULT_PANEL } from "../data/panel";
|
||||
import { serviceCallWillDisconnect } from "../data/service";
|
||||
import {
|
||||
DateFormat,
|
||||
FirstWeekday,
|
||||
NumberFormat,
|
||||
DateFormat,
|
||||
TimeFormat,
|
||||
TimeZone,
|
||||
} from "../data/translation";
|
||||
@ -176,6 +176,11 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
loadFragmentTranslation: (fragment) =>
|
||||
// @ts-ignore
|
||||
this._loadFragmentTranslations(this.hass?.language, fragment),
|
||||
formatEntityState: (stateObj, state) =>
|
||||
(state !== null ? state : stateObj.state) ?? "",
|
||||
formatEntityAttributeName: (_stateObj, attribute) => attribute,
|
||||
formatEntityAttributeValue: (stateObj, attribute, value) =>
|
||||
value !== null ? value : stateObj.attributes[attribute] ?? "",
|
||||
...getState(),
|
||||
...this._pendingHass,
|
||||
};
|
||||
|
@ -14,6 +14,7 @@ import { panelTitleMixin } from "./panel-title-mixin";
|
||||
import SidebarMixin from "./sidebar-mixin";
|
||||
import ThemesMixin from "./themes-mixin";
|
||||
import TranslationsMixin from "./translations-mixin";
|
||||
import StateDisplayMixin from "./state-display-mixin";
|
||||
import { urlSyncMixin } from "./url-sync-mixin";
|
||||
|
||||
const ext = <T extends Constructor>(baseClass: T, mixins): T =>
|
||||
@ -23,6 +24,7 @@ export class HassElement extends ext(HassBaseEl, [
|
||||
AuthMixin,
|
||||
ThemesMixin,
|
||||
TranslationsMixin,
|
||||
StateDisplayMixin,
|
||||
MoreInfoMixin,
|
||||
ActionMixin,
|
||||
SidebarMixin,
|
||||
|
52
src/state/state-display-mixin.ts
Normal file
52
src/state/state-display-mixin.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { computeFormatFunctions } from "../common/translations/entity-state";
|
||||
import { Constructor, HomeAssistant } from "../types";
|
||||
import { HassBaseEl } from "./hass-base-mixin";
|
||||
|
||||
export default <T extends Constructor<HassBaseEl>>(superClass: T) => {
|
||||
class StateDisplayMixin extends superClass {
|
||||
protected hassConnected() {
|
||||
super.hassConnected();
|
||||
this._updateStateDisplay();
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!changedProps.has("hass")) {
|
||||
return;
|
||||
}
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
|
||||
if (this.hass) {
|
||||
if (
|
||||
this.hass.localize !== oldHass?.localize ||
|
||||
this.hass.locale !== oldHass.locale ||
|
||||
this.hass.config !== oldHass.config ||
|
||||
this.hass.entities !== oldHass.entities
|
||||
) {
|
||||
this._updateStateDisplay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _updateStateDisplay = async () => {
|
||||
if (!this.hass) return;
|
||||
const {
|
||||
formatEntityState,
|
||||
formatEntityAttributeName,
|
||||
formatEntityAttributeValue,
|
||||
} = await computeFormatFunctions(
|
||||
this.hass.localize,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities
|
||||
);
|
||||
this._updateHass({
|
||||
formatEntityState,
|
||||
formatEntityAttributeName,
|
||||
formatEntityAttributeValue,
|
||||
});
|
||||
};
|
||||
}
|
||||
return StateDisplayMixin;
|
||||
};
|
@ -3,6 +3,7 @@ import {
|
||||
Connection,
|
||||
HassConfig,
|
||||
HassEntities,
|
||||
HassEntity,
|
||||
HassServices,
|
||||
HassServiceTarget,
|
||||
MessageBase,
|
||||
@ -256,6 +257,13 @@ export interface HomeAssistant {
|
||||
configFlow?: Parameters<typeof getHassTranslations>[4]
|
||||
): Promise<LocalizeFunc>;
|
||||
loadFragmentTranslation(fragment: string): Promise<LocalizeFunc | undefined>;
|
||||
formatEntityState(stateObj: HassEntity, state?: string): string;
|
||||
formatEntityAttributeValue(
|
||||
stateObj: HassEntity,
|
||||
attribute: string,
|
||||
value?: string
|
||||
): string;
|
||||
formatEntityAttributeName(stateObj: HassEntity, attribute: string): string;
|
||||
}
|
||||
|
||||
export interface Route {
|
||||
|
Loading…
x
Reference in New Issue
Block a user