Add format state/attribute to hass (#17249)

This commit is contained in:
Paul Bottein 2023-08-10 09:57:56 +02:00 committed by GitHub
parent 023f13cd12
commit ebee8f670e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 214 additions and 75 deletions

View File

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

View 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),
};
};

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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