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:
parent
1550895d86
commit
050ed145bf
|
@ -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,
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
|
@ -197,6 +197,7 @@ const createEntityRegistryEntries = (
|
|||
platform: "updater",
|
||||
has_entity_name: false,
|
||||
unique_id: "updater",
|
||||
options: null,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -186,7 +186,7 @@ export class HaStateLabelBadge extends LitElement {
|
|||
? formatNumber(
|
||||
entityState.state,
|
||||
this.hass!.locale,
|
||||
getNumberFormatOptions(entityState)
|
||||
getNumberFormatOptions(entityState, entry)
|
||||
)
|
||||
: computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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" &&
|
||||
|
|
|
@ -728,6 +728,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||
selectable: false,
|
||||
entity_category: null,
|
||||
has_entity_name: false,
|
||||
options: null,
|
||||
});
|
||||
}
|
||||
if (changed) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -126,8 +126,7 @@ describe("formatNumber", () => {
|
|||
getNumberFormatOptions({
|
||||
state: "3.0",
|
||||
attributes: { step: 0.5 },
|
||||
} as unknown as HassEntity),
|
||||
undefined
|
||||
} as unknown as HassEntity)
|
||||
);
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue