Bake numeric device classes into formatEntityState (#19878)

* Bake numeric device classes into formatEntityState

* Apply suggestions from code review

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
karwosts 2024-05-24 07:17:23 -07:00 committed by GitHub
parent 3c3d54243c
commit f617426808
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 190 additions and 65 deletions

View File

@ -368,6 +368,7 @@ export class DemoEntityState extends LitElement {
hass.localize, hass.localize,
entry.stateObj, entry.stateObj,
hass.locale, hass.locale,
[], // numericDeviceClasses
hass.config, hass.config,
hass.entities hass.entities
)}`, )}`,

View File

@ -19,28 +19,11 @@ import { blankBeforeUnit } from "../translations/blank_before_unit";
import { LocalizeFunc } from "../translations/localize"; import { LocalizeFunc } from "../translations/localize";
import { computeDomain } from "./compute_domain"; import { computeDomain } from "./compute_domain";
export const computeStateDisplaySingleEntity = (
localize: LocalizeFunc,
stateObj: HassEntity,
locale: FrontendLocaleData,
config: HassConfig,
entity: EntityRegistryDisplayEntry | undefined,
state?: string
): string =>
computeStateDisplayFromEntityAttributes(
localize,
locale,
config,
entity,
stateObj.entity_id,
stateObj.attributes,
state !== undefined ? state : stateObj.state
);
export const computeStateDisplay = ( export const computeStateDisplay = (
localize: LocalizeFunc, localize: LocalizeFunc,
stateObj: HassEntity, stateObj: HassEntity,
locale: FrontendLocaleData, locale: FrontendLocaleData,
sensorNumericDeviceClasses: string[],
config: HassConfig, config: HassConfig,
entities: HomeAssistant["entities"], entities: HomeAssistant["entities"],
state?: string state?: string
@ -52,6 +35,7 @@ export const computeStateDisplay = (
return computeStateDisplayFromEntityAttributes( return computeStateDisplayFromEntityAttributes(
localize, localize,
locale, locale,
sensorNumericDeviceClasses,
config, config,
entity, entity,
stateObj.entity_id, stateObj.entity_id,
@ -63,6 +47,7 @@ export const computeStateDisplay = (
export const computeStateDisplayFromEntityAttributes = ( export const computeStateDisplayFromEntityAttributes = (
localize: LocalizeFunc, localize: LocalizeFunc,
locale: FrontendLocaleData, locale: FrontendLocaleData,
sensorNumericDeviceClasses: string[],
config: HassConfig, config: HassConfig,
entity: EntityRegistryDisplayEntry | undefined, entity: EntityRegistryDisplayEntry | undefined,
entityId: string, entityId: string,
@ -73,8 +58,15 @@ export const computeStateDisplayFromEntityAttributes = (
return localize(`state.default.${state}`); return localize(`state.default.${state}`);
} }
const domain = computeDomain(entityId);
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber` // Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
if (isNumericFromAttributes(attributes)) { if (
isNumericFromAttributes(
attributes,
domain === "sensor" ? sensorNumericDeviceClasses : []
)
) {
// state is duration // state is duration
if ( if (
attributes.device_class === "duration" && attributes.device_class === "duration" &&
@ -120,8 +112,6 @@ export const computeStateDisplayFromEntityAttributes = (
return value; return value;
} }
const domain = computeDomain(entityId);
if (domain === "datetime") { if (domain === "datetime") {
const time = new Date(state); const time = new Date(state);
return formatDateTime(time, locale, config); return formatDateTime(time, locale, config);

View File

@ -14,8 +14,12 @@ export const isNumericState = (stateObj: HassEntity): boolean =>
isNumericFromAttributes(stateObj.attributes); isNumericFromAttributes(stateObj.attributes);
export const isNumericFromAttributes = ( export const isNumericFromAttributes = (
attributes: HassEntityAttributeBase attributes: HassEntityAttributeBase,
): boolean => !!attributes.unit_of_measurement || !!attributes.state_class; numericDeviceClasses?: string[]
): boolean =>
!!attributes.unit_of_measurement ||
!!attributes.state_class ||
(numericDeviceClasses || []).includes(attributes.device_class || "");
export const numberFormatToLocale = ( export const numberFormatToLocale = (
localeOptions: FrontendLocaleData localeOptions: FrontendLocaleData

View File

@ -21,7 +21,8 @@ export const computeFormatFunctions = async (
localize: LocalizeFunc, localize: LocalizeFunc,
locale: FrontendLocaleData, locale: FrontendLocaleData,
config: HassConfig, config: HassConfig,
entities: HomeAssistant["entities"] entities: HomeAssistant["entities"],
sensorNumericDeviceClasses: string[]
): Promise<{ ): Promise<{
formatEntityState: FormatEntityStateFunc; formatEntityState: FormatEntityStateFunc;
formatEntityAttributeValue: FormatEntityAttributeValueFunc; formatEntityAttributeValue: FormatEntityAttributeValueFunc;
@ -35,7 +36,15 @@ export const computeFormatFunctions = async (
return { return {
formatEntityState: (stateObj, state) => formatEntityState: (stateObj, state) =>
computeStateDisplay(localize, stateObj, locale, config, entities, state), computeStateDisplay(
localize,
stateObj,
locale,
sensorNumericDeviceClasses,
config,
entities,
state
),
formatEntityAttributeValue: (stateObj, attribute, value) => formatEntityAttributeValue: (stateObj, attribute, value) =>
computeAttributeValueDisplay( computeAttributeValueDisplay(
localize, localize,

View File

@ -297,6 +297,7 @@ const processTimelineEntity = (
state_localize: computeStateDisplayFromEntityAttributes( state_localize: computeStateDisplayFromEntityAttributes(
localize, localize,
locale, locale,
[], // numeric device classes not used for Timeline
config, config,
entities[entityId], entities[entityId],
entityId, entityId,

View File

@ -18,7 +18,9 @@ export type SensorNumericDeviceClasses = {
numeric_device_classes: string[]; numeric_device_classes: string[];
}; };
let sensorNumericDeviceClassesCache: SensorNumericDeviceClasses | undefined; let sensorNumericDeviceClassesCache:
| Promise<SensorNumericDeviceClasses>
| undefined;
export const getSensorNumericDeviceClasses = async ( export const getSensorNumericDeviceClasses = async (
hass: HomeAssistant hass: HomeAssistant
@ -26,7 +28,7 @@ export const getSensorNumericDeviceClasses = async (
if (sensorNumericDeviceClassesCache) { if (sensorNumericDeviceClassesCache) {
return sensorNumericDeviceClassesCache; return sensorNumericDeviceClassesCache;
} }
sensorNumericDeviceClassesCache = await hass.callWS({ sensorNumericDeviceClassesCache = hass.callWS({
type: "sensor/numeric_device_classes", type: "sensor/numeric_device_classes",
}); });
return sensorNumericDeviceClassesCache!; return sensorNumericDeviceClassesCache!;

View File

@ -119,7 +119,8 @@ export const provideHass = (
hass().localize, hass().localize,
hass().locale, hass().locale,
hass().config, hass().config,
hass().entities hass().entities,
[] // numericDeviceClasses
); );
hass().updateHass({ hass().updateHass({
formatEntityState, formatEntityState,

View File

@ -20,7 +20,6 @@ import { transform } from "../../../common/decorators/transform";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDisplaySingleEntity } from "../../../common/entity/compute_state_display";
import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import { import {
@ -231,13 +230,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
: ""} : ""}
${this._config.show_state && stateObj ${this._config.show_state && stateObj
? html`<span class="state"> ? html`<span class="state">
${computeStateDisplaySingleEntity( ${this.hass.formatEntityState(stateObj)}
this._localize,
stateObj,
this._locale,
this._hassConfig,
this._entity
)}
</span>` </span>`
: ""} : ""}
</ha-card> </ha-card>

View File

@ -1,7 +1,6 @@
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import "../components/entity/state-info"; import "../components/entity/state-info";
import HassMediaPlayerEntity from "../util/hass-media-player-model"; import HassMediaPlayerEntity from "../util/hass-media-player-model";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
@ -26,7 +25,7 @@ class StateCardMediaPlayer extends LitElement {
></state-info> ></state-info>
<div class="state"> <div class="state">
<div class="main-text" take-height=${!playerObj.secondaryTitle}> <div class="main-text" take-height=${!playerObj.secondaryTitle}>
${this._computePrimaryText(this.hass.localize, playerObj)} ${this._computePrimaryText(playerObj)}
</div> </div>
<div class="secondary-text">${playerObj.secondaryTitle}</div> <div class="secondary-text">${playerObj.secondaryTitle}</div>
</div> </div>
@ -34,16 +33,9 @@ class StateCardMediaPlayer extends LitElement {
`; `;
} }
private _computePrimaryText(localize, playerObj) { private _computePrimaryText(playerObj) {
return ( return (
playerObj.primaryTitle || playerObj.primaryTitle || this.hass.formatEntityState(playerObj.stateObj)
computeStateDisplay(
localize,
playerObj.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities
)
); );
} }

View File

@ -1,4 +1,5 @@
import { computeFormatFunctions } from "../common/translations/entity-state"; import { computeFormatFunctions } from "../common/translations/entity-state";
import { getSensorNumericDeviceClasses } from "../data/sensor";
import { Constructor, HomeAssistant } from "../types"; import { Constructor, HomeAssistant } from "../types";
import { HassBaseEl } from "./hass-base-mixin"; import { HassBaseEl } from "./hass-base-mixin";
@ -31,6 +32,10 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) => {
private _updateStateDisplay = async () => { private _updateStateDisplay = async () => {
if (!this.hass) return; if (!this.hass) return;
const { numeric_device_classes: sensorNumericDeviceClasses } =
await getSensorNumericDeviceClasses(this.hass);
const { const {
formatEntityState, formatEntityState,
formatEntityAttributeName, formatEntityAttributeName,
@ -39,7 +44,8 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) => {
this.hass.localize, this.hass.localize,
this.hass.locale, this.hass.locale,
this.hass.config, this.hass.config,
this.hass.entities this.hass.entities,
sensorNumericDeviceClasses
); );
this._updateHass({ this._updateHass({
formatEntityState, formatEntityState,

View File

@ -18,6 +18,8 @@ describe("computeStateDisplay", () => {
const localize = (message, ...args) => const localize = (message, ...args) =>
message + (args.length ? ": " + args.join(",") : ""); message + (args.length ? ": " + args.join(",") : "");
const numericDeviceClasses = [];
beforeEach(() => { beforeEach(() => {
localeData = { localeData = {
language: "en", language: "en",
@ -36,7 +38,14 @@ describe("computeStateDisplay", () => {
attributes: {}, attributes: {},
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData, demoConfig, {}), computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
"component.binary_sensor.entity_component._.state.off" "component.binary_sensor.entity_component._.state.off"
); );
}); });
@ -50,7 +59,14 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData, demoConfig, {}), computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
"component.binary_sensor.state.moisture.off" "component.binary_sensor.state.moisture.off"
); );
}); });
@ -70,7 +86,14 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(altLocalize, stateObj, localeData, demoConfig, {}), computeStateDisplay(
altLocalize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
"component.binary_sensor.state.invalid_device_class.off" "component.binary_sensor.state.invalid_device_class.off"
); );
}); });
@ -84,7 +107,14 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData, demoConfig, {}), computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
"123 m" "123 m"
); );
}); });
@ -98,7 +128,14 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData, demoConfig, {}), computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
"1,234.5 m" "1,234.5 m"
); );
}); });
@ -112,7 +149,14 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData, demoConfig, {}), computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
"1,234.5" "1,234.5"
); );
}); });
@ -132,7 +176,14 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(altLocalize, stateObj, localeData, demoConfig, {}), computeStateDisplay(
altLocalize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
"state.default.unknown" "state.default.unknown"
); );
}); });
@ -152,7 +203,14 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(altLocalize, stateObj, localeData, demoConfig, {}), computeStateDisplay(
altLocalize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
"state.default.unavailable" "state.default.unavailable"
); );
}); });
@ -172,7 +230,14 @@ describe("computeStateDisplay", () => {
attributes: {}, attributes: {},
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(altLocalize, stateObj, localeData, demoConfig, {}), computeStateDisplay(
altLocalize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
"component.sensor.entity_component._.state.custom_state" "component.sensor.entity_component._.state.custom_state"
); );
}); });
@ -194,14 +259,28 @@ describe("computeStateDisplay", () => {
}; };
it("Uses am/pm time format", () => { it("Uses am/pm time format", () => {
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData, demoConfig, {}), computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
"November 18, 2017 at 11:12 PM" "November 18, 2017 at 11:12 PM"
); );
}); });
it("Uses 24h time format", () => { it("Uses 24h time format", () => {
localeData.time_format = TimeFormat.twenty_four; localeData.time_format = TimeFormat.twenty_four;
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData, demoConfig, {}), computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
"November 18, 2017 at 23:12" "November 18, 2017 at 23:12"
); );
}); });
@ -223,7 +302,14 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData, demoConfig, {}), computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
"November 18, 2017" "November 18, 2017"
); );
}); });
@ -246,14 +332,28 @@ describe("computeStateDisplay", () => {
it("Uses am/pm time format", () => { it("Uses am/pm time format", () => {
localeData.time_format = TimeFormat.am_pm; localeData.time_format = TimeFormat.am_pm;
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData, demoConfig, {}), computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
"11:12 PM" "11:12 PM"
); );
}); });
it("Uses 24h time format", () => { it("Uses 24h time format", () => {
localeData.time_format = TimeFormat.twenty_four; localeData.time_format = TimeFormat.twenty_four;
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData, demoConfig, {}), computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
"23:12" "23:12"
); );
}); });
@ -280,6 +380,7 @@ describe("computeStateDisplay", () => {
localize, localize,
stateObj, stateObj,
localeData, localeData,
numericDeviceClasses,
demoConfig, demoConfig,
{}, {},
"2021-07-04 15:40:03" "2021-07-04 15:40:03"
@ -294,6 +395,7 @@ describe("computeStateDisplay", () => {
localize, localize,
stateObj, stateObj,
localeData, localeData,
numericDeviceClasses,
demoConfig, demoConfig,
{}, {},
"2021-07-04 15:40:03" "2021-07-04 15:40:03"
@ -323,6 +425,7 @@ describe("computeStateDisplay", () => {
localize, localize,
stateObj, stateObj,
localeData, localeData,
numericDeviceClasses,
demoConfig, demoConfig,
{}, {},
"2021-07-04" "2021-07-04"
@ -353,6 +456,7 @@ describe("computeStateDisplay", () => {
localize, localize,
stateObj, stateObj,
localeData, localeData,
numericDeviceClasses,
demoConfig, demoConfig,
{}, {},
"17:05:07" "17:05:07"
@ -367,6 +471,7 @@ describe("computeStateDisplay", () => {
localize, localize,
stateObj, stateObj,
localeData, localeData,
numericDeviceClasses,
demoConfig, demoConfig,
{}, {},
"17:05:07" "17:05:07"
@ -389,7 +494,14 @@ describe("computeStateDisplay", () => {
attributes: {}, attributes: {},
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(altLocalize, stateObj, localeData, demoConfig, {}), computeStateDisplay(
altLocalize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
"state.default.unavailable" "state.default.unavailable"
); );
}); });
@ -404,7 +516,14 @@ describe("computeStateDisplay", () => {
attributes: {}, attributes: {},
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(altLocalize, stateObj, localeData, demoConfig, {}), computeStateDisplay(
altLocalize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
"My Custom State" "My Custom State"
); );
}); });
@ -422,7 +541,14 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData, demoConfig, entities), computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
entities
),
"component.custom_integration.entity.sensor.custom_translation.state.custom_state" "component.custom_integration.entity.sensor.custom_translation.state.custom_state"
); );
}); });