diff --git a/src/common/number/normalize-by-si-prefix.ts b/src/common/number/normalize-by-si-prefix.ts new file mode 100644 index 0000000000..ccac81b952 --- /dev/null +++ b/src/common/number/normalize-by-si-prefix.ts @@ -0,0 +1,28 @@ +const SI_PREFIX_MULTIPLIERS: Record = { + 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; +}; diff --git a/src/data/energy.ts b/src/data/energy.ts index 7977557cfc..1a412e2b9a 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -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 + ); }; /** diff --git a/src/panels/lovelace/cards/hui-distribution-card.ts b/src/panels/lovelace/cards/hui-distribution-card.ts index 683b6ae30b..5b2e43e520 100644 --- a/src/panels/lovelace/cards/hui-distribution-card.ts +++ b/src/panels/lovelace/cards/hui-distribution-card.ts @@ -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) diff --git a/test/common/number/normalize-by-si-prefix.test.ts b/test/common/number/normalize-by-si-prefix.test.ts new file mode 100644 index 0000000000..9b72754f1b --- /dev/null +++ b/test/common/number/normalize-by-si-prefix.test.ts @@ -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); + }); +});