1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-02-14 23:18:21 +00:00

Normalize SI unit prefixes in distribution card proportions (#29539)

* Normalize SI unit prefixes in distribution card proportions

* Extract SI prefix normalization to shared utility with tests

Moves normalizeValueBySIPrefix to src/common/number/ so it can be
reused. Replaces the inline method in the distribution card and the
switch statement in getPowerFromState (energy.ts).
This commit is contained in:
Petar Petrov
2026-02-10 19:16:02 +01:00
committed by GitHub
parent 3c4c3e39e5
commit 4ebc334298
4 changed files with 94 additions and 22 deletions

View File

@@ -0,0 +1,28 @@
const SI_PREFIX_MULTIPLIERS: Record<string, number> = {
T: 1e12,
G: 1e9,
M: 1e6,
k: 1e3,
m: 1e-3,
"\u00B5": 1e-6, // µ (micro sign)
"\u03BC": 1e-6, // μ (greek small letter mu)
};
/**
* Normalize a numeric value by detecting SI unit prefixes (T, G, M, k, m, µ).
* Only applies when the unit is longer than 1 character and starts with a
* recognized prefix, avoiding false positives on standalone units like "m" (meters).
*/
export const normalizeValueBySIPrefix = (
value: number,
unit: string | undefined
): number => {
if (!unit || unit.length <= 1) {
return value;
}
const prefix = unit[0];
if (prefix in SI_PREFIX_MULTIPLIERS) {
return value * SI_PREFIX_MULTIPLIERS[prefix];
}
return value;
};

View File

@@ -14,6 +14,7 @@ import {
import type { Collection, HassEntity } from "home-assistant-js-websocket";
import { getCollection } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import { normalizeValueBySIPrefix } from "../common/number/normalize-by-si-prefix";
import {
calcDate,
calcDateProperty,
@@ -1431,26 +1432,10 @@ export const getPowerFromState = (stateObj: HassEntity): number | undefined => {
return undefined;
}
// Normalize to watts (W) based on unit of measurement (case-sensitive)
// Supported units: GW, kW, MW, mW, TW, W
const unit = stateObj.attributes.unit_of_measurement;
switch (unit) {
case "W":
return value;
case "kW":
return value * 1000;
case "mW":
return value / 1000;
case "MW":
return value * 1_000_000;
case "GW":
return value * 1_000_000_000;
case "TW":
return value * 1_000_000_000_000;
default:
// Assume value is in watts (W) if no unit or an unsupported unit is provided
return value;
}
return normalizeValueBySIPrefix(
value,
stateObj.attributes.unit_of_measurement
);
};
/**

View File

@@ -9,6 +9,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { getGraphColorByIndex } from "../../../common/color/colors";
import { computeCssColor } from "../../../common/color/compute-color";
import { computeDomain } from "../../../common/entity/compute_domain";
import { normalizeValueBySIPrefix } from "../../../common/number/normalize-by-si-prefix";
import { MobileAwareMixin } from "../../../mixins/mobile-aware-mixin";
import type { EntityNameItem } from "../../../common/entity/compute_entity_name_display";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
@@ -230,8 +231,12 @@ export class HuiDistributionCard
const stateObj = this.hass!.states[entity.entity];
if (!stateObj) return;
const value = Number(stateObj.state);
if (value <= 0 || isNaN(value)) return;
const rawValue = Number(stateObj.state);
if (rawValue <= 0 || isNaN(rawValue)) return;
const value = normalizeValueBySIPrefix(
rawValue,
stateObj.attributes.unit_of_measurement
);
const color = entity.color
? computeCssColor(entity.color)

View File

@@ -0,0 +1,54 @@
import { assert, describe, it } from "vitest";
import { normalizeValueBySIPrefix } from "../../../src/common/number/normalize-by-si-prefix";
describe("normalizeValueBySIPrefix", () => {
it("Applies kilo prefix (k)", () => {
assert.equal(normalizeValueBySIPrefix(11, "kW"), 11000);
assert.equal(normalizeValueBySIPrefix(2.5, "kWh"), 2500);
});
it("Applies mega prefix (M)", () => {
assert.equal(normalizeValueBySIPrefix(3, "MW"), 3_000_000);
});
it("Applies giga prefix (G)", () => {
assert.equal(normalizeValueBySIPrefix(1, "GW"), 1_000_000_000);
});
it("Applies tera prefix (T)", () => {
assert.equal(normalizeValueBySIPrefix(2, "TW"), 2_000_000_000_000);
});
it("Applies milli prefix (m)", () => {
assert.equal(normalizeValueBySIPrefix(500, "mW"), 0.5);
});
it("Applies micro prefix (µ micro sign U+00B5)", () => {
assert.equal(normalizeValueBySIPrefix(1000, "\u00B5W"), 0.001);
});
it("Applies micro prefix (μ greek mu U+03BC)", () => {
assert.equal(normalizeValueBySIPrefix(1000, "\u03BCW"), 0.001);
});
it("Returns value unchanged for single-char units", () => {
assert.equal(normalizeValueBySIPrefix(100, "W"), 100);
assert.equal(normalizeValueBySIPrefix(5, "m"), 5);
assert.equal(normalizeValueBySIPrefix(22, "K"), 22);
});
it("Returns value unchanged for undefined unit", () => {
assert.equal(normalizeValueBySIPrefix(42, undefined), 42);
});
it("Returns value unchanged for unrecognized prefixes", () => {
assert.equal(normalizeValueBySIPrefix(20, "°C"), 20);
assert.equal(normalizeValueBySIPrefix(50, "dB"), 50);
assert.equal(normalizeValueBySIPrefix(1013, "hPa"), 1013);
});
it("Returns value unchanged for empty string", () => {
assert.equal(normalizeValueBySIPrefix(10, ""), 10);
});
});