Translate entity attribute names & attribute states (#15822)

This commit is contained in:
Franck Nijhof 2023-03-17 11:43:59 +01:00 committed by GitHub
parent dfd7acd713
commit 0232c11bc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 188 additions and 131 deletions

View File

@ -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<typeof import("../../resources/js-yaml-dump")>;
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`<a target="_blank" rel="noreferrer" href=${value}
>${attributeValue}</a
>`;
} 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`<pre>${until(yaml, "")}</pre>`;
}
// 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")
)
);
};

View File

@ -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`
<ha-combo-box
.hass=${this.hass}
.value=${this.value ? formatAttributeName(this.value) : ""}
.value=${this.value
? computeAttributeNameDisplay(
this.hass.localize,
this.hass.states[this.entityId!],
this.hass.entities,
this.value
)
: ""}
.autofocus=${this.autofocus}
.label=${this.label ??
this.hass.localize(

View File

@ -4,7 +4,7 @@ import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { computeStateDisplay } from "../../common/entity/compute_state_display";
import { getStates } from "../../common/entity/get_states";
import { formatAttributeValue } from "../../data/entity_attributes";
import { computeAttributeValueDisplay } from "../../common/entity/compute_attribute_display";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "../ha-combo-box";
@ -58,7 +58,14 @@ class HaEntityStatePicker extends LitElement {
this.hass.entities,
key
)
: formatAttributeValue(this.hass, key),
: computeAttributeValueDisplay(
this.hass.localize,
state,
this.hass.locale,
this.hass.entities,
this.attribute,
key
),
}))
: [];
}

View File

@ -1,18 +1,11 @@
import { HassEntity } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
html,
LitElement,
TemplateResult,
nothing,
} from "lit";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
formatAttributeName,
formatAttributeValue,
STATE_ATTRIBUTES,
} from "../data/entity_attributes";
computeAttributeNameDisplay,
computeAttributeValueDisplay,
} from "../common/entity/compute_attribute_display";
import { STATE_ATTRIBUTES } from "../data/entity_attributes";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
@ -56,9 +49,22 @@ class HaAttributes extends LitElement {
${attributes.map(
(attribute) => html`
<div class="data-entry">
<div class="key">${formatAttributeName(attribute)}</div>
<div class="key">
${computeAttributeNameDisplay(
this.hass.localize,
this.stateObj!,
this.hass.entities,
attribute
)}
</div>
<div class="value">
${this.formatAttribute(attribute)}
${computeAttributeValueDisplay(
this.hass.localize,
this.stateObj!,
this.hass.locale,
this.hass.entities,
attribute
)}
</div>
</div>
`
@ -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;
}

View File

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

View File

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

View File

@ -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<typeof import("../resources/js-yaml-dump")>;
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`<pre>${until(yaml, "")}</pre>`;
}
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`<a target="_blank" rel="noreferrer" href=${value}
>${value}</a
>`;
} 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;
}

View File

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

View File

@ -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 {
<span class="value"
>${"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

View File

@ -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"
)}

View File

@ -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
></hui-timestamp-display>`
: 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}
</hui-generic-entity-row>