Allow overriding a sensor's display precision (#15363)

* Allow overriding a sensor's display precision

* Update demo + gallery

* Lint

* Fix state not updated in the UI

* Use formatNumber for options

* Feedbacks

* Add default precision and minimumFractionDigits

* Remove useless undefined

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
This commit is contained in:
Erik Montnemery 2023-02-08 18:20:58 +01:00 committed by GitHub
parent 1550895d86
commit 050ed145bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 163 additions and 17 deletions

View File

@ -71,6 +71,7 @@ class HaDemo extends HomeAssistantAppEl {
entity_category: null, entity_category: null,
has_entity_name: false, has_entity_name: false,
unique_id: "co2_intensity", unique_id: "co2_intensity",
options: null,
}, },
{ {
config_entry_id: "co2signal", config_entry_id: "co2signal",
@ -86,6 +87,7 @@ class HaDemo extends HomeAssistantAppEl {
entity_category: null, entity_category: null,
has_entity_name: false, has_entity_name: false,
unique_id: "grid_fossil_fuel_percentage", unique_id: "grid_fossil_fuel_percentage",
options: null,
}, },
]); ]);

View File

@ -197,6 +197,7 @@ const createEntityRegistryEntries = (
platform: "updater", platform: "updater",
has_entity_name: false, has_entity_name: false,
unique_id: "updater", unique_id: "updater",
options: null,
}, },
]; ];

View File

@ -49,6 +49,8 @@ export const computeStateDisplayFromEntityAttributes = (
return localize(`state.default.${state}`); 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` // Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
if (isNumericFromAttributes(attributes)) { if (isNumericFromAttributes(attributes)) {
// state is duration // state is duration
@ -82,7 +84,7 @@ export const computeStateDisplayFromEntityAttributes = (
return `${formatNumber( return `${formatNumber(
state, state,
locale, locale,
getNumberFormatOptions({ state, attributes } as HassEntity) getNumberFormatOptions({ state, attributes } as HassEntity, entity)
)}${unit}`; )}${unit}`;
} }
@ -160,7 +162,7 @@ export const computeStateDisplayFromEntityAttributes = (
return formatNumber( return formatNumber(
state, state,
locale, 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"); : localize("ui.card.update.up_to_date");
} }
const entity = entities[entityId] as EntityRegistryEntry | undefined;
return ( return (
(entity?.translation_key && (entity?.translation_key &&
localize( localize(

View File

@ -2,6 +2,7 @@ import {
HassEntity, HassEntity,
HassEntityAttributeBase, HassEntityAttributeBase,
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { FrontendLocaleData, NumberFormat } from "../../data/translation"; import { FrontendLocaleData, NumberFormat } from "../../data/translation";
import { round } from "./round"; import { round } from "./round";
@ -90,8 +91,18 @@ export const formatNumber = (
* @returns An `Intl.NumberFormatOptions` object with `maximumFractionDigits` set to 0, or `undefined` * @returns An `Intl.NumberFormatOptions` object with `maximumFractionDigits` set to 0, or `undefined`
*/ */
export const getNumberFormatOptions = ( export const getNumberFormatOptions = (
entityState: HassEntity entityState: HassEntity,
entity?: EntityRegistryEntry
): Intl.NumberFormatOptions | undefined => { ): 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 ( if (
Number.isInteger(Number(entityState.attributes?.step)) && Number.isInteger(Number(entityState.attributes?.step)) &&
Number.isInteger(Number(entityState.state)) Number.isInteger(Number(entityState.state))

View File

@ -186,7 +186,7 @@ export class HaStateLabelBadge extends LitElement {
? formatNumber( ? formatNumber(
entityState.state, entityState.state,
this.hass!.locale, this.hass!.locale,
getNumberFormatOptions(entityState) getNumberFormatOptions(entityState, entry)
) )
: computeStateDisplay( : computeStateDisplay(
this.hass!.localize, this.hass!.localize,

View File

@ -22,6 +22,7 @@ export interface EntityRegistryEntry {
original_name?: string; original_name?: string;
unique_id: string; unique_id: string;
translation_key?: string; translation_key?: string;
options: EntityRegistryOptions | null;
} }
export interface ExtEntityRegistryEntry extends EntityRegistryEntry { export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
@ -39,6 +40,8 @@ export interface UpdateEntityRegistryEntryResult {
} }
export interface SensorEntityOptions { export interface SensorEntityOptions {
display_precision?: number | null;
suggested_display_precision?: number | null;
unit_of_measurement?: string | null; unit_of_measurement?: string | null;
} }
@ -54,6 +57,12 @@ export interface WeatherEntityOptions {
wind_speed_unit?: string | null; wind_speed_unit?: string | null;
} }
export interface EntityRegistryOptions {
number?: NumberEntityOptions;
sensor?: SensorEntityOptions;
weather?: WeatherEntityOptions;
}
export interface EntityRegistryEntryUpdateParams { export interface EntityRegistryEntryUpdateParams {
name?: string | null; name?: string | null;
icon?: string | null; icon?: string | null;

View File

@ -63,6 +63,7 @@ import {
EntityRegistryEntry, EntityRegistryEntry,
EntityRegistryEntryUpdateParams, EntityRegistryEntryUpdateParams,
ExtEntityRegistryEntry, ExtEntityRegistryEntry,
SensorEntityOptions,
fetchEntityRegistry, fetchEntityRegistry,
removeEntityRegistryEntry, removeEntityRegistryEntry,
updateEntityRegistryEntry, updateEntityRegistryEntry,
@ -81,6 +82,7 @@ import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail"; import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
import { showAliasesDialog } from "../../../dialogs/aliases/show-dialog-aliases"; import { showAliasesDialog } from "../../../dialogs/aliases/show-dialog-aliases";
import { formatNumber } from "../../../common/number/format_number";
const OVERRIDE_DEVICE_CLASSES = { const OVERRIDE_DEVICE_CLASSES = {
cover: [ cover: [
@ -126,6 +128,8 @@ const OVERRIDE_WEATHER_UNITS = {
const SWITCH_AS_DOMAINS = ["cover", "fan", "light", "lock", "siren"]; const SWITCH_AS_DOMAINS = ["cover", "fan", "light", "lock", "siren"];
const PRECISIONS = [0, 1, 2, 3, 4, 5, 6];
@customElement("entity-registry-settings") @customElement("entity-registry-settings")
export class EntityRegistrySettings extends SubscribeMixin(LitElement) { export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @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 _unit_of_measurement?: string | null;
@state() private _precision?: number | null;
@state() private _precipitation_unit?: string | null; @state() private _precipitation_unit?: string | null;
@state() private _pressure_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; this._unit_of_measurement = stateObj?.attributes?.unit_of_measurement;
} }
if (domain === "sensor") {
this._precision = this.entry.options?.sensor?.display_precision;
}
if (domain === "weather") { if (domain === "weather") {
const stateObj: HassEntity | undefined = const stateObj: HassEntity | undefined =
this.hass.states[this.entry.entity_id]; 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<void> { protected async updated(changedProps: PropertyValues): Promise<void> {
if (changedProps.has("_deviceClass")) { if (changedProps.has("_deviceClass")) {
const domain = computeDomain(this.entry.entity_id); 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 invalidDomainUpdate = computeDomain(this._entityId.trim()) !== domain;
const defaultPrecision =
this.entry.options?.sensor?.suggested_display_precision ?? undefined;
return html` return html`
${!stateObj ${!stateObj
? html` ? html`
@ -468,6 +489,47 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
</ha-select> </ha-select>
` `
: ""} : ""}
${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`
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.precision"
)}
.value=${this._precision == null
? "default"
: this._precision.toString()}
naturalMenuWidth
fixedMenuPosition
@selected=${this._precisionChanged}
@closed=${stopPropagation}
>
<mwc-list-item value="default"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.precision_default",
{
value: this.precisionLabel(
defaultPrecision,
stateObj?.state
),
}
)}</mwc-list-item
>
${PRECISIONS.map(
(precision) => html`
<mwc-list-item .value=${precision.toString()}>
${this.precisionLabel(precision, stateObj?.state)}
</mwc-list-item>
`
)}
</ha-select>
`
: ""}
${domain === "weather" ${domain === "weather"
? html` ? html`
<ha-select <ha-select
@ -893,6 +955,12 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
this._precipitation_unit = ev.target.value; this._precipitation_unit = ev.target.value;
} }
private _precisionChanged(ev): void {
this._error = undefined;
this._precision =
ev.target.value === "default" ? null : Number(ev.target.value);
}
private _pressureUnitChanged(ev): void { private _pressureUnitChanged(ev): void {
this._error = undefined; this._error = undefined;
this._pressure_unit = ev.target.value; this._pressure_unit = ev.target.value;
@ -1088,7 +1156,17 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
stateObj?.attributes?.unit_of_measurement !== this._unit_of_measurement stateObj?.attributes?.unit_of_measurement !== this._unit_of_measurement
) { ) {
params.options_domain = domain; params.options_domain = domain;
params.options = { unit_of_measurement: this._unit_of_measurement }; params.options = this.entry.options?.[domain] || {};
params.options.unit_of_measurement = this._unit_of_measurement;
}
if (
domain === "sensor" &&
this.entry.options?.[domain]?.display_precision !== this._precision
) {
params.options_domain = domain;
params.options = params.options || this.entry.options?.[domain] || {};
(params.options as SensorEntityOptions).display_precision =
this._precision;
} }
if ( if (
domain === "weather" && domain === "weather" &&

View File

@ -728,6 +728,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
selectable: false, selectable: false,
entity_category: null, entity_category: null,
has_entity_name: false, has_entity_name: false,
options: null,
}); });
} }
if (changed) { if (changed) {

View File

@ -168,7 +168,10 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
? formatNumber( ? formatNumber(
stateObj.state, stateObj.state,
this.hass.locale, this.hass.locale,
getNumberFormatOptions(stateObj) getNumberFormatOptions(
stateObj,
this.hass.entities[this._config.entity]
)
) )
: computeStateDisplay( : computeStateDisplay(
this.hass.localize, this.hass.localize,

View File

@ -1,4 +1,6 @@
import { HassEntity } from "home-assistant-js-websocket";
import { PropertyValues } from "lit"; import { PropertyValues } from "lit";
import { EntityRegistryEntry } from "../../../data/entity_registry";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { processConfigEntities } from "./process-config-entities"; import { processConfigEntities } from "./process-config-entities";
@ -24,6 +26,37 @@ function hasConfigChanged(element: any, changedProps: PropertyValues): boolean {
return false; return false;
} }
function compareEntityState(
oldHass: HomeAssistant,
newHass: HomeAssistant,
entityId: string
) {
const oldState = oldHass.states[entityId] as HassEntity | undefined;
const newState = newHass.states[entityId] as HassEntity | undefined;
return oldState !== newState;
}
function compareEntityEntryOptions(
oldHass: HomeAssistant,
newHass: HomeAssistant,
entityId: string
) {
const oldEntry = oldHass.entities[entityId] as
| EntityRegistryEntry
| undefined;
const newEntry = newHass.entities[entityId] as
| EntityRegistryEntry
| undefined;
return (
oldEntry?.options?.sensor?.display_precision !==
newEntry?.options?.sensor?.display_precision ||
oldEntry?.options?.sensor?.suggested_display_precision !==
newEntry?.options?.sensor?.suggested_display_precision
);
}
// Check if config or Entity changed // Check if config or Entity changed
export function hasConfigOrEntityChanged( export function hasConfigOrEntityChanged(
element: any, element: any,
@ -34,10 +67,11 @@ export function hasConfigOrEntityChanged(
} }
const oldHass = changedProps.get("hass") as HomeAssistant; const oldHass = changedProps.get("hass") as HomeAssistant;
const newHass = element.hass as HomeAssistant;
return ( return (
oldHass.states[element._config!.entity] !== compareEntityState(oldHass, newHass, element._config!.entity) ||
element.hass!.states[element._config!.entity] compareEntityEntryOptions(oldHass, newHass, element._config!.entity)
); );
} }
@ -51,12 +85,18 @@ export function hasConfigOrEntitiesChanged(
} }
const oldHass = changedProps.get("hass") as HomeAssistant; const oldHass = changedProps.get("hass") as HomeAssistant;
const newHass = element.hass as HomeAssistant;
const entities = processConfigEntities(element._config!.entities, false); const entities = processConfigEntities(element._config!.entities, false);
return entities.some( return entities.some((entity) => {
(entity) => if (!("entity" in entity)) {
"entity" in entity && return false;
oldHass.states[entity.entity] !== element.hass!.states[entity.entity] }
);
return (
compareEntityState(oldHass, newHass, entity.entity) ||
compareEntityEntryOptions(oldHass, newHass, entity.entity)
);
});
} }

View File

@ -906,6 +906,8 @@
"entity_id": "Entity ID", "entity_id": "Entity ID",
"unit_of_measurement": "Unit of Measurement", "unit_of_measurement": "Unit of Measurement",
"precipitation_unit": "Precipitation unit", "precipitation_unit": "Precipitation unit",
"precision": "Display precision",
"precision_default": "Default ({value})",
"pressure_unit": "Barometric pressure unit", "pressure_unit": "Barometric pressure unit",
"temperature_unit": "Temperature unit", "temperature_unit": "Temperature unit",
"visibility_unit": "Visibility unit", "visibility_unit": "Visibility unit",

View File

@ -126,8 +126,7 @@ describe("formatNumber", () => {
getNumberFormatOptions({ getNumberFormatOptions({
state: "3.0", state: "3.0",
attributes: { step: 0.5 }, attributes: { step: 0.5 },
} as unknown as HassEntity), } as unknown as HassEntity)
undefined
); );
}); });