diff --git a/gallery/src/pages/misc/entity-state.ts b/gallery/src/pages/misc/entity-state.ts index 242264b5da..ab395b71a1 100644 --- a/gallery/src/pages/misc/entity-state.ts +++ b/gallery/src/pages/misc/entity-state.ts @@ -368,6 +368,7 @@ export class DemoEntityState extends LitElement { hass.localize, entry.stateObj, hass.locale, + [], // numericDeviceClasses hass.config, hass.entities )}`, diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index 0db516e56f..01c137ee7c 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -19,28 +19,11 @@ import { blankBeforeUnit } from "../translations/blank_before_unit"; import { LocalizeFunc } from "../translations/localize"; 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 = ( localize: LocalizeFunc, stateObj: HassEntity, locale: FrontendLocaleData, + sensorNumericDeviceClasses: string[], config: HassConfig, entities: HomeAssistant["entities"], state?: string @@ -52,6 +35,7 @@ export const computeStateDisplay = ( return computeStateDisplayFromEntityAttributes( localize, locale, + sensorNumericDeviceClasses, config, entity, stateObj.entity_id, @@ -63,6 +47,7 @@ export const computeStateDisplay = ( export const computeStateDisplayFromEntityAttributes = ( localize: LocalizeFunc, locale: FrontendLocaleData, + sensorNumericDeviceClasses: string[], config: HassConfig, entity: EntityRegistryDisplayEntry | undefined, entityId: string, @@ -73,8 +58,15 @@ export const computeStateDisplayFromEntityAttributes = ( 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` - if (isNumericFromAttributes(attributes)) { + if ( + isNumericFromAttributes( + attributes, + domain === "sensor" ? sensorNumericDeviceClasses : [] + ) + ) { // state is duration if ( attributes.device_class === "duration" && @@ -120,8 +112,6 @@ export const computeStateDisplayFromEntityAttributes = ( return value; } - const domain = computeDomain(entityId); - if (domain === "datetime") { const time = new Date(state); return formatDateTime(time, locale, config); diff --git a/src/common/number/format_number.ts b/src/common/number/format_number.ts index ddbe1765d4..822dfc5411 100644 --- a/src/common/number/format_number.ts +++ b/src/common/number/format_number.ts @@ -14,8 +14,12 @@ export const isNumericState = (stateObj: HassEntity): boolean => isNumericFromAttributes(stateObj.attributes); export const isNumericFromAttributes = ( - attributes: HassEntityAttributeBase -): boolean => !!attributes.unit_of_measurement || !!attributes.state_class; + attributes: HassEntityAttributeBase, + numericDeviceClasses?: string[] +): boolean => + !!attributes.unit_of_measurement || + !!attributes.state_class || + (numericDeviceClasses || []).includes(attributes.device_class || ""); export const numberFormatToLocale = ( localeOptions: FrontendLocaleData diff --git a/src/common/translations/entity-state.ts b/src/common/translations/entity-state.ts index 36d2e58332..c9edbae99a 100644 --- a/src/common/translations/entity-state.ts +++ b/src/common/translations/entity-state.ts @@ -21,7 +21,8 @@ export const computeFormatFunctions = async ( localize: LocalizeFunc, locale: FrontendLocaleData, config: HassConfig, - entities: HomeAssistant["entities"] + entities: HomeAssistant["entities"], + sensorNumericDeviceClasses: string[] ): Promise<{ formatEntityState: FormatEntityStateFunc; formatEntityAttributeValue: FormatEntityAttributeValueFunc; @@ -35,7 +36,15 @@ export const computeFormatFunctions = async ( return { formatEntityState: (stateObj, state) => - computeStateDisplay(localize, stateObj, locale, config, entities, state), + computeStateDisplay( + localize, + stateObj, + locale, + sensorNumericDeviceClasses, + config, + entities, + state + ), formatEntityAttributeValue: (stateObj, attribute, value) => computeAttributeValueDisplay( localize, diff --git a/src/data/history.ts b/src/data/history.ts index 53b83896e8..c1bfd607b0 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -297,6 +297,7 @@ const processTimelineEntity = ( state_localize: computeStateDisplayFromEntityAttributes( localize, locale, + [], // numeric device classes not used for Timeline config, entities[entityId], entityId, diff --git a/src/data/sensor.ts b/src/data/sensor.ts index 9034715848..2be4a70024 100644 --- a/src/data/sensor.ts +++ b/src/data/sensor.ts @@ -18,7 +18,9 @@ export type SensorNumericDeviceClasses = { numeric_device_classes: string[]; }; -let sensorNumericDeviceClassesCache: SensorNumericDeviceClasses | undefined; +let sensorNumericDeviceClassesCache: + | Promise + | undefined; export const getSensorNumericDeviceClasses = async ( hass: HomeAssistant @@ -26,7 +28,7 @@ export const getSensorNumericDeviceClasses = async ( if (sensorNumericDeviceClassesCache) { return sensorNumericDeviceClassesCache; } - sensorNumericDeviceClassesCache = await hass.callWS({ + sensorNumericDeviceClassesCache = hass.callWS({ type: "sensor/numeric_device_classes", }); return sensorNumericDeviceClassesCache!; diff --git a/src/fake_data/provide_hass.ts b/src/fake_data/provide_hass.ts index 111ebf6e31..3723fa7321 100644 --- a/src/fake_data/provide_hass.ts +++ b/src/fake_data/provide_hass.ts @@ -119,7 +119,8 @@ export const provideHass = ( hass().localize, hass().locale, hass().config, - hass().entities + hass().entities, + [] // numericDeviceClasses ); hass().updateHass({ formatEntityState, diff --git a/src/panels/lovelace/cards/hui-button-card.ts b/src/panels/lovelace/cards/hui-button-card.ts index c36a9fa6a2..160e5c4277 100644 --- a/src/panels/lovelace/cards/hui-button-card.ts +++ b/src/panels/lovelace/cards/hui-button-card.ts @@ -20,7 +20,6 @@ import { transform } from "../../../common/decorators/transform"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { fireEvent } from "../../../common/dom/fire_event"; import { computeDomain } from "../../../common/entity/compute_domain"; -import { computeStateDisplaySingleEntity } from "../../../common/entity/compute_state_display"; import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { @@ -231,13 +230,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard { : ""} ${this._config.show_state && stateObj ? html` - ${computeStateDisplaySingleEntity( - this._localize, - stateObj, - this._locale, - this._hassConfig, - this._entity - )} + ${this.hass.formatEntityState(stateObj)} ` : ""} diff --git a/src/state-summary/state-card-media_player.ts b/src/state-summary/state-card-media_player.ts index eb3350504a..26b557cdc2 100644 --- a/src/state-summary/state-card-media_player.ts +++ b/src/state-summary/state-card-media_player.ts @@ -1,7 +1,6 @@ import type { HassEntity } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; -import { computeStateDisplay } from "../common/entity/compute_state_display"; import "../components/entity/state-info"; import HassMediaPlayerEntity from "../util/hass-media-player-model"; import { HomeAssistant } from "../types"; @@ -26,7 +25,7 @@ class StateCardMediaPlayer extends LitElement { >
- ${this._computePrimaryText(this.hass.localize, playerObj)} + ${this._computePrimaryText(playerObj)}
${playerObj.secondaryTitle}
@@ -34,16 +33,9 @@ class StateCardMediaPlayer extends LitElement { `; } - private _computePrimaryText(localize, playerObj) { + private _computePrimaryText(playerObj) { return ( - playerObj.primaryTitle || - computeStateDisplay( - localize, - playerObj.stateObj, - this.hass.locale, - this.hass.config, - this.hass.entities - ) + playerObj.primaryTitle || this.hass.formatEntityState(playerObj.stateObj) ); } diff --git a/src/state/state-display-mixin.ts b/src/state/state-display-mixin.ts index 4ba3243b51..f38849f298 100644 --- a/src/state/state-display-mixin.ts +++ b/src/state/state-display-mixin.ts @@ -1,4 +1,5 @@ import { computeFormatFunctions } from "../common/translations/entity-state"; +import { getSensorNumericDeviceClasses } from "../data/sensor"; import { Constructor, HomeAssistant } from "../types"; import { HassBaseEl } from "./hass-base-mixin"; @@ -31,6 +32,10 @@ export default >(superClass: T) => { private _updateStateDisplay = async () => { if (!this.hass) return; + + const { numeric_device_classes: sensorNumericDeviceClasses } = + await getSensorNumericDeviceClasses(this.hass); + const { formatEntityState, formatEntityAttributeName, @@ -39,7 +44,8 @@ export default >(superClass: T) => { this.hass.localize, this.hass.locale, this.hass.config, - this.hass.entities + this.hass.entities, + sensorNumericDeviceClasses ); this._updateHass({ formatEntityState, diff --git a/test/common/entity/compute_state_display.ts b/test/common/entity/compute_state_display.ts index 2cfc130a56..3bf6fdfe1c 100644 --- a/test/common/entity/compute_state_display.ts +++ b/test/common/entity/compute_state_display.ts @@ -18,6 +18,8 @@ describe("computeStateDisplay", () => { const localize = (message, ...args) => message + (args.length ? ": " + args.join(",") : ""); + const numericDeviceClasses = []; + beforeEach(() => { localeData = { language: "en", @@ -36,7 +38,14 @@ describe("computeStateDisplay", () => { attributes: {}, }; assert.strictEqual( - computeStateDisplay(localize, stateObj, localeData, demoConfig, {}), + computeStateDisplay( + localize, + stateObj, + localeData, + numericDeviceClasses, + demoConfig, + {} + ), "component.binary_sensor.entity_component._.state.off" ); }); @@ -50,7 +59,14 @@ describe("computeStateDisplay", () => { }, }; assert.strictEqual( - computeStateDisplay(localize, stateObj, localeData, demoConfig, {}), + computeStateDisplay( + localize, + stateObj, + localeData, + numericDeviceClasses, + demoConfig, + {} + ), "component.binary_sensor.state.moisture.off" ); }); @@ -70,7 +86,14 @@ describe("computeStateDisplay", () => { }, }; assert.strictEqual( - computeStateDisplay(altLocalize, stateObj, localeData, demoConfig, {}), + computeStateDisplay( + altLocalize, + stateObj, + localeData, + numericDeviceClasses, + demoConfig, + {} + ), "component.binary_sensor.state.invalid_device_class.off" ); }); @@ -84,7 +107,14 @@ describe("computeStateDisplay", () => { }, }; assert.strictEqual( - computeStateDisplay(localize, stateObj, localeData, demoConfig, {}), + computeStateDisplay( + localize, + stateObj, + localeData, + numericDeviceClasses, + demoConfig, + {} + ), "123 m" ); }); @@ -98,7 +128,14 @@ describe("computeStateDisplay", () => { }, }; assert.strictEqual( - computeStateDisplay(localize, stateObj, localeData, demoConfig, {}), + computeStateDisplay( + localize, + stateObj, + localeData, + numericDeviceClasses, + demoConfig, + {} + ), "1,234.5 m" ); }); @@ -112,7 +149,14 @@ describe("computeStateDisplay", () => { }, }; assert.strictEqual( - computeStateDisplay(localize, stateObj, localeData, demoConfig, {}), + computeStateDisplay( + localize, + stateObj, + localeData, + numericDeviceClasses, + demoConfig, + {} + ), "1,234.5" ); }); @@ -132,7 +176,14 @@ describe("computeStateDisplay", () => { }, }; assert.strictEqual( - computeStateDisplay(altLocalize, stateObj, localeData, demoConfig, {}), + computeStateDisplay( + altLocalize, + stateObj, + localeData, + numericDeviceClasses, + demoConfig, + {} + ), "state.default.unknown" ); }); @@ -152,7 +203,14 @@ describe("computeStateDisplay", () => { }, }; assert.strictEqual( - computeStateDisplay(altLocalize, stateObj, localeData, demoConfig, {}), + computeStateDisplay( + altLocalize, + stateObj, + localeData, + numericDeviceClasses, + demoConfig, + {} + ), "state.default.unavailable" ); }); @@ -172,7 +230,14 @@ describe("computeStateDisplay", () => { attributes: {}, }; assert.strictEqual( - computeStateDisplay(altLocalize, stateObj, localeData, demoConfig, {}), + computeStateDisplay( + altLocalize, + stateObj, + localeData, + numericDeviceClasses, + demoConfig, + {} + ), "component.sensor.entity_component._.state.custom_state" ); }); @@ -194,14 +259,28 @@ describe("computeStateDisplay", () => { }; it("Uses am/pm time format", () => { assert.strictEqual( - computeStateDisplay(localize, stateObj, localeData, demoConfig, {}), + computeStateDisplay( + localize, + stateObj, + localeData, + numericDeviceClasses, + demoConfig, + {} + ), "November 18, 2017 at 11:12 PM" ); }); it("Uses 24h time format", () => { localeData.time_format = TimeFormat.twenty_four; assert.strictEqual( - computeStateDisplay(localize, stateObj, localeData, demoConfig, {}), + computeStateDisplay( + localize, + stateObj, + localeData, + numericDeviceClasses, + demoConfig, + {} + ), "November 18, 2017 at 23:12" ); }); @@ -223,7 +302,14 @@ describe("computeStateDisplay", () => { }, }; assert.strictEqual( - computeStateDisplay(localize, stateObj, localeData, demoConfig, {}), + computeStateDisplay( + localize, + stateObj, + localeData, + numericDeviceClasses, + demoConfig, + {} + ), "November 18, 2017" ); }); @@ -246,14 +332,28 @@ describe("computeStateDisplay", () => { it("Uses am/pm time format", () => { localeData.time_format = TimeFormat.am_pm; assert.strictEqual( - computeStateDisplay(localize, stateObj, localeData, demoConfig, {}), + computeStateDisplay( + localize, + stateObj, + localeData, + numericDeviceClasses, + demoConfig, + {} + ), "11:12 PM" ); }); it("Uses 24h time format", () => { localeData.time_format = TimeFormat.twenty_four; assert.strictEqual( - computeStateDisplay(localize, stateObj, localeData, demoConfig, {}), + computeStateDisplay( + localize, + stateObj, + localeData, + numericDeviceClasses, + demoConfig, + {} + ), "23:12" ); }); @@ -280,6 +380,7 @@ describe("computeStateDisplay", () => { localize, stateObj, localeData, + numericDeviceClasses, demoConfig, {}, "2021-07-04 15:40:03" @@ -294,6 +395,7 @@ describe("computeStateDisplay", () => { localize, stateObj, localeData, + numericDeviceClasses, demoConfig, {}, "2021-07-04 15:40:03" @@ -323,6 +425,7 @@ describe("computeStateDisplay", () => { localize, stateObj, localeData, + numericDeviceClasses, demoConfig, {}, "2021-07-04" @@ -353,6 +456,7 @@ describe("computeStateDisplay", () => { localize, stateObj, localeData, + numericDeviceClasses, demoConfig, {}, "17:05:07" @@ -367,6 +471,7 @@ describe("computeStateDisplay", () => { localize, stateObj, localeData, + numericDeviceClasses, demoConfig, {}, "17:05:07" @@ -389,7 +494,14 @@ describe("computeStateDisplay", () => { attributes: {}, }; assert.strictEqual( - computeStateDisplay(altLocalize, stateObj, localeData, demoConfig, {}), + computeStateDisplay( + altLocalize, + stateObj, + localeData, + numericDeviceClasses, + demoConfig, + {} + ), "state.default.unavailable" ); }); @@ -404,7 +516,14 @@ describe("computeStateDisplay", () => { attributes: {}, }; assert.strictEqual( - computeStateDisplay(altLocalize, stateObj, localeData, demoConfig, {}), + computeStateDisplay( + altLocalize, + stateObj, + localeData, + numericDeviceClasses, + demoConfig, + {} + ), "My Custom State" ); }); @@ -422,7 +541,14 @@ describe("computeStateDisplay", () => { }, }; 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" ); });