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,
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,
},
]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<void> {
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) {
</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"
? html`
<ha-select
@ -893,6 +955,12 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
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 {
this._error = undefined;
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
) {
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 (
domain === "weather" &&

View File

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

View File

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

View File

@ -1,4 +1,6 @@
import { HassEntity } from "home-assistant-js-websocket";
import { PropertyValues } from "lit";
import { EntityRegistryEntry } from "../../../data/entity_registry";
import { HomeAssistant } from "../../../types";
import { processConfigEntities } from "./process-config-entities";
@ -24,6 +26,37 @@ function hasConfigChanged(element: any, changedProps: PropertyValues): boolean {
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
export function hasConfigOrEntityChanged(
element: any,
@ -34,10 +67,11 @@ export function hasConfigOrEntityChanged(
}
const oldHass = changedProps.get("hass") as HomeAssistant;
const newHass = element.hass as HomeAssistant;
return (
oldHass.states[element._config!.entity] !==
element.hass!.states[element._config!.entity]
compareEntityState(oldHass, newHass, element._config!.entity) ||
compareEntityEntryOptions(oldHass, newHass, element._config!.entity)
);
}
@ -51,12 +85,18 @@ export function hasConfigOrEntitiesChanged(
}
const oldHass = changedProps.get("hass") as HomeAssistant;
const newHass = element.hass as HomeAssistant;
const entities = processConfigEntities(element._config!.entities, false);
return entities.some(
(entity) =>
"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)
);
});
}

View File

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

View File

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