mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-16 22:06:34 +00:00
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,
|
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,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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))
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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" &&
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user