diff --git a/src/common/entity/compute_attribute_display.ts b/src/common/entity/compute_attribute_display.ts index c1079d70c1..703236d601 100644 --- a/src/common/entity/compute_attribute_display.ts +++ b/src/common/entity/compute_attribute_display.ts @@ -1,28 +1,119 @@ import { HassEntity } from "home-assistant-js-websocket"; +import { html, TemplateResult } from "lit"; +import { until } from "lit/directives/until"; import { EntityRegistryDisplayEntry } from "../../data/entity_registry"; import { HomeAssistant } from "../../types"; +import checkValidDate from "../datetime/check_valid_date"; +import { formatDate } from "../datetime/format_date"; +import { formatDateTimeWithSeconds } from "../datetime/format_date_time"; +import { formatNumber } from "../number/format_number"; +import { capitalizeFirstLetter } from "../string/capitalize-first-letter"; +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; export const computeAttributeValueDisplay = ( localize: LocalizeFunc, stateObj: HassEntity, + locale: FrontendLocaleData, entities: HomeAssistant["entities"], attribute: string, value?: any -): string => { - const entityId = stateObj.entity_id; - const deviceClass = stateObj.attributes.device_class; +): string | TemplateResult => { const attributeValue = value !== undefined ? value : stateObj.attributes[attribute]; + + // Null value, return dash + if (attributeValue === null) { + return "—"; + } + + // Number value, return formatted number + if (typeof attributeValue === "number") { + return formatNumber(attributeValue, locale); + } + + // 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`${attributeValue}`; + } catch (_) { + // Nothing to do here + } + } + + // Date handling + if (isDate(attributeValue, true)) { + // Timestamp handling + if (isTimestamp(attributeValue)) { + const date = new Date(attributeValue); + if (checkValidDate(date)) { + return formatDateTimeWithSeconds(date, locale); + } + } + + // Value was not a timestamp, so only do date formatting + const date = new Date(attributeValue); + if (checkValidDate(date)) { + return formatDate(date, locale); + } + } + } + + // Values are objects, render object + 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`
${until(yaml, "")}
`; + } + + // If this is an array, try to determine the display value for each item + if (Array.isArray(attributeValue)) { + return attributeValue + .map((item) => + computeAttributeValueDisplay( + localize, + stateObj, + locale, + entities, + attribute, + item + ) + ) + .join(", "); + } + + // We've explored all known value handling, so now we'll try to find a + // translation for the value. + const entityId = stateObj.entity_id; const domain = computeDomain(entityId); - const entity = entities[entityId] as EntityRegistryDisplayEntry | undefined; - const translationKey = entity?.translation_key; + const deviceClass = stateObj.attributes.device_class; + const registryEntry = entities[entityId] as + | EntityRegistryDisplayEntry + | undefined; + const translationKey = registryEntry?.translation_key; return ( (translationKey && localize( - `component.${entity.platform}.entity.${domain}.${translationKey}.state_attributes.${attribute}.state.${attributeValue}` + `component.${registryEntry.platform}.entity.${domain}.${translationKey}.state_attributes.${attribute}.state.${attributeValue}` )) || (deviceClass && localize( @@ -59,6 +150,13 @@ export const computeAttributeNameDisplay = ( localize( `component.${domain}.entity_component._.state_attributes.${attribute}.name` ) || - attribute + capitalizeFirstLetter( + attribute + .replace(/_/g, " ") + .replace(/\bid\b/g, "ID") + .replace(/\bip\b/g, "IP") + .replace(/\bmac\b/g, "MAC") + .replace(/\bgps\b/g, "GPS") + ) ); }; diff --git a/src/components/entity/ha-entity-attribute-picker.ts b/src/components/entity/ha-entity-attribute-picker.ts index 1e4c790712..799e89d7a4 100644 --- a/src/components/entity/ha-entity-attribute-picker.ts +++ b/src/components/entity/ha-entity-attribute-picker.ts @@ -1,7 +1,7 @@ import { HassEntity } from "home-assistant-js-websocket"; import { html, LitElement, PropertyValues, nothing } from "lit"; import { customElement, property, query } from "lit/decorators"; -import { formatAttributeName } from "../../data/entity_attributes"; +import { computeAttributeNameDisplay } from "../../common/entity/compute_attribute_display"; import { PolymerChangedEvent } from "../../polymer-types"; import { HomeAssistant } from "../../types"; import "../ha-combo-box"; @@ -54,7 +54,12 @@ class HaEntityAttributePicker extends LitElement { .filter((key) => !this.hideAttributes?.includes(key)) .map((key) => ({ value: key, - label: formatAttributeName(key), + label: computeAttributeNameDisplay( + this.hass.localize, + state, + this.hass.entities, + key + ), })) : []; } @@ -68,7 +73,14 @@ class HaEntityAttributePicker extends LitElement { return html` html`
-
${formatAttributeName(attribute)}
+
+ ${computeAttributeNameDisplay( + this.hass.localize, + this.stateObj!, + this.hass.entities, + attribute + )} +
- ${this.formatAttribute(attribute)} + ${computeAttributeValueDisplay( + this.hass.localize, + this.stateObj!, + this.hass.locale, + this.hass.entities, + attribute + )}
` @@ -128,14 +134,6 @@ class HaAttributes extends LitElement { ); } - private formatAttribute(attribute: string): string | TemplateResult { - if (!this.stateObj) { - return "—"; - } - const value = this.stateObj.attributes[attribute]; - return formatAttributeValue(this.hass, value); - } - private expandedChanged(ev) { this._expanded = ev.detail.expanded; } diff --git a/src/components/ha-climate-state.ts b/src/components/ha-climate-state.ts index c4264f947b..7c8be97a6e 100644 --- a/src/components/ha-climate-state.ts +++ b/src/components/ha-climate-state.ts @@ -27,6 +27,7 @@ class HaClimateState extends LitElement { ${computeAttributeValueDisplay( this.hass.localize, this.stateObj, + this.hass.locale, this.hass.entities, "preset_mode" )}` @@ -142,6 +143,7 @@ class HaClimateState extends LitElement { ? `${computeAttributeValueDisplay( this.hass.localize, this.stateObj, + this.hass.locale, this.hass.entities, "hvac_action" )} (${stateString})` diff --git a/src/data/automation_i18n.ts b/src/data/automation_i18n.ts index 876e2cbef3..f25b62465b 100644 --- a/src/data/automation_i18n.ts +++ b/src/data/automation_i18n.ts @@ -10,7 +10,7 @@ import { localizeDeviceAutomationCondition, localizeDeviceAutomationTrigger, } from "./device_automation"; -import { formatAttributeName } from "./entity_attributes"; +import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display"; const describeDuration = (forTime: number | string | ForDict) => { let duration: string | null; @@ -67,7 +67,12 @@ export const describeTrigger = ( const entity = stateObj ? computeStateName(stateObj) : trigger.entity_id; if (trigger.attribute) { - base += ` ${formatAttributeName(trigger.attribute)} from`; + base += ` ${computeAttributeNameDisplay( + hass.localize, + stateObj, + hass.entities, + trigger.attribute + )} from`; } base += ` ${entity} is`; @@ -98,11 +103,18 @@ export const describeTrigger = ( if (trigger.platform === "state") { let base = "When"; let entities = ""; - const states = hass.states; if (trigger.attribute) { - base += ` ${formatAttributeName(trigger.attribute)} from`; + const stateObj = Array.isArray(trigger.entity_id) + ? hass.states[trigger.entity_id[0]] + : hass.states[trigger.entity_id]; + base += ` ${computeAttributeNameDisplay( + hass.localize, + stateObj, + hass.entities, + trigger.attribute + )} from`; } if (Array.isArray(trigger.entity_id)) { diff --git a/src/data/entity_attributes.ts b/src/data/entity_attributes.ts index faa42e8fab..dc082e0b63 100644 --- a/src/data/entity_attributes.ts +++ b/src/data/entity_attributes.ts @@ -1,16 +1,3 @@ -import { html, TemplateResult } from "lit"; -import { until } from "lit/directives/until"; -import checkValidDate from "../common/datetime/check_valid_date"; -import { formatDate } from "../common/datetime/format_date"; -import { formatDateTimeWithSeconds } from "../common/datetime/format_date_time"; -import { formatNumber } from "../common/number/format_number"; -import { capitalizeFirstLetter } from "../common/string/capitalize-first-letter"; -import { isDate } from "../common/string/is_date"; -import { isTimestamp } from "../common/string/is_timestamp"; -import { HomeAssistant } from "../types"; - -let jsYamlPromise: Promise; - export const STATE_ATTRIBUTES = [ "assumed_state", "attribution", @@ -32,74 +19,3 @@ export const STATE_ATTRIBUTES = [ "supported_features", "unit_of_measurement", ]; - -// Convert from internal snake_case format to user-friendly format -export function formatAttributeName(value: string): string { - value = value - .replace(/_/g, " ") - .replace(/\bid\b/g, "ID") - .replace(/\bip\b/g, "IP") - .replace(/\bmac\b/g, "MAC") - .replace(/\bgps\b/g, "GPS"); - return capitalizeFirstLetter(value); -} - -export function formatAttributeValue( - hass: HomeAssistant, - value: any -): string | TemplateResult { - if (value === null) { - return "—"; - } - - // YAML handling - if ( - (Array.isArray(value) && value.some((val) => val instanceof Object)) || - (!Array.isArray(value) && value instanceof Object) - ) { - if (!jsYamlPromise) { - jsYamlPromise = import("../resources/js-yaml-dump"); - } - const yaml = jsYamlPromise.then((jsYaml) => jsYaml.dump(value)); - return html`
${until(yaml, "")}
`; - } - - if (typeof value === "number") { - return formatNumber(value, hass.locale); - } - - if (typeof value === "string") { - // URL handling - if (value.startsWith("http")) { - try { - // If invalid URL, exception will be raised - const url = new URL(value); - if (url.protocol === "http:" || url.protocol === "https:") - return html`${value}`; - } catch (_) { - // Nothing to do here - } - } - - // Date handling - if (isDate(value, true)) { - // Timestamp handling - if (isTimestamp(value)) { - const date = new Date(value); - if (checkValidDate(date)) { - return formatDateTimeWithSeconds(date, hass.locale); - } - } - - // Value was not a timestamp, so only do date formatting - const date = new Date(value); - if (checkValidDate(date)) { - return formatDate(date, hass.locale); - } - } - } - - return Array.isArray(value) ? value.join(", ") : value; -} diff --git a/src/dialogs/more-info/controls/more-info-climate.ts b/src/dialogs/more-info/controls/more-info-climate.ts index b4a2bd831b..52a4b0aa14 100644 --- a/src/dialogs/more-info/controls/more-info-climate.ts +++ b/src/dialogs/more-info/controls/more-info-climate.ts @@ -235,6 +235,7 @@ class MoreInfoClimate extends LitElement { ${computeAttributeValueDisplay( hass.localize, stateObj, + hass.locale, hass.entities, "preset_mode", mode @@ -268,6 +269,7 @@ class MoreInfoClimate extends LitElement { ${computeAttributeValueDisplay( hass.localize, stateObj, + hass.locale, hass.entities, "fan_mode", mode @@ -301,6 +303,7 @@ class MoreInfoClimate extends LitElement { ${computeAttributeValueDisplay( hass.localize, stateObj, + hass.locale, hass.entities, "swing_mode", mode diff --git a/src/panels/lovelace/cards/hui-entity-card.ts b/src/panels/lovelace/cards/hui-entity-card.ts index 46a97f8a80..4d948cd0d7 100644 --- a/src/panels/lovelace/cards/hui-entity-card.ts +++ b/src/panels/lovelace/cards/hui-entity-card.ts @@ -27,7 +27,7 @@ import "../../../components/ha-card"; import "../../../components/ha-icon"; import { HVAC_ACTION_TO_MODE } from "../../../data/climate"; import { isUnavailableState } from "../../../data/entity"; -import { formatAttributeValue } from "../../../data/entity_attributes"; +import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display"; import { LightEntity } from "../../../data/light"; import { HomeAssistant } from "../../../types"; import { computeCardSize } from "../common/compute-card-size"; @@ -159,9 +159,12 @@ export class HuiEntityCard extends LitElement implements LovelaceCard { ${"attribute" in this._config ? stateObj.attributes[this._config.attribute!] !== undefined - ? formatAttributeValue( - this.hass, - stateObj.attributes[this._config.attribute!] + ? computeAttributeValueDisplay( + this.hass.localize, + stateObj, + this.hass.locale, + this.hass.entities, + this._config.attribute! ) : this.hass.localize("state.default.unknown") : isNumericState(stateObj) || this._config.unit diff --git a/src/panels/lovelace/cards/hui-thermostat-card.ts b/src/panels/lovelace/cards/hui-thermostat-card.ts index e9daa3ccc5..b186cf8e59 100644 --- a/src/panels/lovelace/cards/hui-thermostat-card.ts +++ b/src/panels/lovelace/cards/hui-thermostat-card.ts @@ -234,6 +234,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { ? computeAttributeValueDisplay( this.hass.localize, stateObj, + this.hass.locale, this.hass.entities, "hvac_action" ) @@ -252,6 +253,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { ${computeAttributeValueDisplay( this.hass.localize, stateObj, + this.hass.locale, this.hass.entities, "preset_mode" )} diff --git a/src/panels/lovelace/special-rows/hui-attribute-row.ts b/src/panels/lovelace/special-rows/hui-attribute-row.ts index 8942ae41f8..b0e95de02f 100644 --- a/src/panels/lovelace/special-rows/hui-attribute-row.ts +++ b/src/panels/lovelace/special-rows/hui-attribute-row.ts @@ -8,14 +8,13 @@ import { } from "lit"; import { customElement, property, state } from "lit/decorators"; import checkValidDate from "../../../common/datetime/check_valid_date"; -import { formatNumber } from "../../../common/number/format_number"; -import { formatAttributeValue } from "../../../data/entity_attributes"; 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,10 +70,15 @@ class HuiAttributeRow extends LitElement implements LovelaceRow { .format=${this._config.format} capitalize >` - : typeof attribute === "number" - ? formatNumber(attribute, this.hass.locale) : attribute !== undefined - ? formatAttributeValue(this.hass, attribute) + ? computeAttributeValueDisplay( + this.hass.localize, + stateObj, + this.hass.locale, + this.hass.entities, + this._config.attribute, + attribute + ) : "—"} ${this._config.suffix}