diff --git a/demo/src/ha-demo.ts b/demo/src/ha-demo.ts index e911afce1f..4b527df487 100644 --- a/demo/src/ha-demo.ts +++ b/demo/src/ha-demo.ts @@ -71,6 +71,7 @@ class HaDemo extends HomeAssistantAppEl { entity_category: null, has_entity_name: false, unique_id: "co2_intensity", + options: null, }, { config_entry_id: "co2signal", @@ -86,6 +87,7 @@ class HaDemo extends HomeAssistantAppEl { entity_category: null, has_entity_name: false, unique_id: "grid_fossil_fuel_percentage", + options: null, }, ]); diff --git a/gallery/src/pages/misc/integration-card.ts b/gallery/src/pages/misc/integration-card.ts index 56de4308e8..168344e51b 100644 --- a/gallery/src/pages/misc/integration-card.ts +++ b/gallery/src/pages/misc/integration-card.ts @@ -197,6 +197,7 @@ const createEntityRegistryEntries = ( platform: "updater", has_entity_name: false, unique_id: "updater", + options: null, }, ]; diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index 18a05de5ac..c469585a9d 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -49,6 +49,8 @@ export const computeStateDisplayFromEntityAttributes = ( return localize(`state.default.${state}`); } + const entity = entities[entityId] as EntityRegistryEntry | undefined; + // Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber` if (isNumericFromAttributes(attributes)) { // state is duration @@ -82,7 +84,7 @@ export const computeStateDisplayFromEntityAttributes = ( return `${formatNumber( state, locale, - getNumberFormatOptions({ state, attributes } as HassEntity) + getNumberFormatOptions({ state, attributes } as HassEntity, entity) )}${unit}`; } @@ -160,7 +162,7 @@ export const computeStateDisplayFromEntityAttributes = ( return formatNumber( state, locale, - getNumberFormatOptions({ state, attributes } as HassEntity) + getNumberFormatOptions({ state, attributes } as HassEntity, entity) ); } @@ -199,8 +201,6 @@ export const computeStateDisplayFromEntityAttributes = ( : localize("ui.card.update.up_to_date"); } - const entity = entities[entityId] as EntityRegistryEntry | undefined; - return ( (entity?.translation_key && localize( diff --git a/src/common/number/format_number.ts b/src/common/number/format_number.ts index e3f78f1b79..2e461ab822 100644 --- a/src/common/number/format_number.ts +++ b/src/common/number/format_number.ts @@ -2,6 +2,7 @@ import { HassEntity, HassEntityAttributeBase, } from "home-assistant-js-websocket"; +import { EntityRegistryEntry } from "../../data/entity_registry"; import { FrontendLocaleData, NumberFormat } from "../../data/translation"; import { round } from "./round"; @@ -90,8 +91,18 @@ export const formatNumber = ( * @returns An `Intl.NumberFormatOptions` object with `maximumFractionDigits` set to 0, or `undefined` */ export const getNumberFormatOptions = ( - entityState: HassEntity + entityState: HassEntity, + entity?: EntityRegistryEntry ): Intl.NumberFormatOptions | undefined => { + const precision = + entity?.options?.sensor?.display_precision ?? + entity?.options?.sensor?.suggested_display_precision; + if (precision != null) { + return { + maximumFractionDigits: precision, + minimumFractionDigits: precision, + }; + } if ( Number.isInteger(Number(entityState.attributes?.step)) && Number.isInteger(Number(entityState.state)) diff --git a/src/components/entity/ha-state-label-badge.ts b/src/components/entity/ha-state-label-badge.ts index 6af024518d..03079a331f 100644 --- a/src/components/entity/ha-state-label-badge.ts +++ b/src/components/entity/ha-state-label-badge.ts @@ -186,7 +186,7 @@ export class HaStateLabelBadge extends LitElement { ? formatNumber( entityState.state, this.hass!.locale, - getNumberFormatOptions(entityState) + getNumberFormatOptions(entityState, entry) ) : computeStateDisplay( this.hass!.localize, diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index cbc5459b3b..ede328f7e1 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -22,6 +22,7 @@ export interface EntityRegistryEntry { original_name?: string; unique_id: string; translation_key?: string; + options: EntityRegistryOptions | null; } export interface ExtEntityRegistryEntry extends EntityRegistryEntry { @@ -39,6 +40,8 @@ export interface UpdateEntityRegistryEntryResult { } export interface SensorEntityOptions { + display_precision?: number | null; + suggested_display_precision?: number | null; unit_of_measurement?: string | null; } @@ -54,6 +57,12 @@ export interface WeatherEntityOptions { wind_speed_unit?: string | null; } +export interface EntityRegistryOptions { + number?: NumberEntityOptions; + sensor?: SensorEntityOptions; + weather?: WeatherEntityOptions; +} + export interface EntityRegistryEntryUpdateParams { name?: string | null; icon?: string | null; diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index d1afbb6b50..db7fbf479b 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -63,6 +63,7 @@ import { EntityRegistryEntry, EntityRegistryEntryUpdateParams, ExtEntityRegistryEntry, + SensorEntityOptions, fetchEntityRegistry, removeEntityRegistryEntry, updateEntityRegistryEntry, @@ -81,6 +82,7 @@ import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail"; import { showAliasesDialog } from "../../../dialogs/aliases/show-dialog-aliases"; +import { formatNumber } from "../../../common/number/format_number"; const OVERRIDE_DEVICE_CLASSES = { cover: [ @@ -126,6 +128,8 @@ const OVERRIDE_WEATHER_UNITS = { const SWITCH_AS_DOMAINS = ["cover", "fan", "light", "lock", "siren"]; +const PRECISIONS = [0, 1, 2, 3, 4, 5, 6]; + @customElement("entity-registry-settings") export class EntityRegistrySettings extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @@ -154,6 +158,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { @state() private _unit_of_measurement?: string | null; + @state() private _precision?: number | null; + @state() private _precipitation_unit?: string | null; @state() private _pressure_unit?: string | null; @@ -251,6 +257,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { this._unit_of_measurement = stateObj?.attributes?.unit_of_measurement; } + if (domain === "sensor") { + this._precision = this.entry.options?.sensor?.display_precision; + } + if (domain === "weather") { const stateObj: HassEntity | undefined = this.hass.states[this.entry.entity_id]; @@ -277,6 +287,14 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { } } + private precisionLabel(precision?: number, stateValue?: string) { + const value = stateValue ?? 0; + return formatNumber(value, this.hass.locale, { + minimumFractionDigits: precision, + maximumFractionDigits: precision, + }); + } + protected async updated(changedProps: PropertyValues): Promise { if (changedProps.has("_deviceClass")) { const domain = computeDomain(this.entry.entity_id); @@ -313,6 +331,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { const invalidDomainUpdate = computeDomain(this._entityId.trim()) !== domain; + const defaultPrecision = + this.entry.options?.sensor?.suggested_display_precision ?? undefined; + return html` ${!stateObj ? html` @@ -468,6 +489,47 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { ` : ""} + ${domain === "sensor" && + // Allow customizing the precision for a sensor with numerical device class, + // a unit of measurement or state class + ((this._deviceClass && + !["date", "enum", "timestamp"].includes(this._deviceClass)) || + stateObj?.attributes.unit_of_measurement || + stateObj?.attributes.state_class) + ? html` + + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.precision_default", + { + value: this.precisionLabel( + defaultPrecision, + stateObj?.state + ), + } + )} + ${PRECISIONS.map( + (precision) => html` + + ${this.precisionLabel(precision, stateObj?.state)} + + ` + )} + + ` + : ""} ${domain === "weather" ? html` - "entity" in entity && - oldHass.states[entity.entity] !== element.hass!.states[entity.entity] - ); + return entities.some((entity) => { + if (!("entity" in entity)) { + return false; + } + + return ( + compareEntityState(oldHass, newHass, entity.entity) || + compareEntityEntryOptions(oldHass, newHass, entity.entity) + ); + }); } diff --git a/src/translations/en.json b/src/translations/en.json index 176cd12b34..f42e7ff028 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -906,6 +906,8 @@ "entity_id": "Entity ID", "unit_of_measurement": "Unit of Measurement", "precipitation_unit": "Precipitation unit", + "precision": "Display precision", + "precision_default": "Default ({value})", "pressure_unit": "Barometric pressure unit", "temperature_unit": "Temperature unit", "visibility_unit": "Visibility unit", diff --git a/test/common/string/format_number.ts b/test/common/string/format_number.ts index e8f402e3a4..c8c4c135eb 100644 --- a/test/common/string/format_number.ts +++ b/test/common/string/format_number.ts @@ -126,8 +126,7 @@ describe("formatNumber", () => { getNumberFormatOptions({ state: "3.0", attributes: { step: 0.5 }, - } as unknown as HassEntity), - undefined + } as unknown as HassEntity) ); });